diff --git a/.cursorrules b/.cursor/rules/general.mdc
similarity index 87%
rename from .cursorrules
rename to .cursor/rules/general.mdc
index 2117545..8162f5d 100644
--- a/.cursorrules
+++ b/.cursor/rules/general.mdc
@@ -1,11 +1,16 @@
+---
+description:
+globs:
+alwaysApply: false
+---
You are a Staff Engineer and an Expert in ReactJS, NextJS, JavaScript, TypeScript, HTML, CSS, NodeJS, Prisma, Postgres, and modern UI/UX frameworks (e.g., TailwindCSS, Shadcn, Radix). You are also great at scalling things. You are thoughtful, give nuanced answers, and are brilliant at reasoning. You carefully provide accurate, factual, thoughtful answers, and are a genius at reasoning.
-- Follow the user’s requirements carefully & to the letter.
+- Follow the user's requirements carefully & to the letter.
- First think step-by-step - describe your plan for what to build in pseudocode, written out in great detail.
- Always write correct, best practice, bug free, fully functional and working code also it should be aligned to listed rules down below at Code Implementation Guidelines .
- Focus on easy and readability code, over being performant.
- Fully implement all requested functionality.
-- Leave NO todo’s, placeholders or missing pieces.
+- Leave NO todo's, placeholders or missing pieces.
- Ensure code is complete! Verify thoroughly finalised.
- Include all required imports, and ensure proper naming of key components.
- Be concise Minimize any other prose.
diff --git a/apps/web/postcss.config.cjs b/apps/web/postcss.config.cjs
index 4cdb2f4..e305dd9 100644
--- a/apps/web/postcss.config.cjs
+++ b/apps/web/postcss.config.cjs
@@ -1,6 +1,7 @@
const config = {
plugins: {
tailwindcss: {},
+ autoprefixer: {},
},
};
diff --git a/apps/web/src/app/(dashboard)/campaigns/[campaignId]/page.tsx b/apps/web/src/app/(dashboard)/campaigns/[campaignId]/page.tsx
index ef52115..8c802eb 100644
--- a/apps/web/src/app/(dashboard)/campaigns/[campaignId]/page.tsx
+++ b/apps/web/src/app/(dashboard)/campaigns/[campaignId]/page.tsx
@@ -28,7 +28,7 @@ export default function CampaignDetailsPage({
if (isLoading) {
return (
-
+
);
}
@@ -94,7 +94,7 @@ export default function CampaignDetailsPage({
{card.status.toLowerCase()}
-
+
{card.count}
{card.status !== "total" ? (
diff --git a/apps/web/src/app/(dashboard)/campaigns/campaign-list.tsx b/apps/web/src/app/(dashboard)/campaigns/campaign-list.tsx
index 87f4082..2fae9c2 100644
--- a/apps/web/src/app/(dashboard)/campaigns/campaign-list.tsx
+++ b/apps/web/src/app/(dashboard)/campaigns/campaign-list.tsx
@@ -89,7 +89,7 @@ export default function CampaignList() {
);
}
-
-// "use client";
-
-// import {
-// Table,
-// TableHeader,
-// TableRow,
-// TableHead,
-// TableBody,
-// TableCell,
-// } from "@unsend/ui/src/table";
-// import { api } from "~/trpc/react";
-// import { useUrlState } from "~/hooks/useUrlState";
-// import { Button } from "@unsend/ui/src/button";
-// import Spinner from "@unsend/ui/src/spinner";
-// import { formatDistanceToNow } from "date-fns";
-// import { CampaignStatus } from "@prisma/client";
-// import DeleteCampaign from "./delete-campaign";
-// import Link from "next/link";
-// import DuplicateCampaign from "./duplicate-campaign";
-// import { motion } from "framer-motion";
-// import { useRouter } from "next/navigation";
-// import {
-// Select,
-// SelectTrigger,
-// SelectContent,
-// SelectItem,
-// } from "@unsend/ui/src/select";
-
-// export default function CampaignList() {
-// const [page, setPage] = useUrlState("page", "1");
-// const [status, setStatus] = useUrlState("status");
-
-// const pageNumber = Number(page);
-
-// const campaignsQuery = api.campaign.getCampaigns.useQuery({
-// page: pageNumber,
-// status: status as CampaignStatus | null,
-// });
-
-// const router = useRouter();
-
-// return (
-//
-//
-// setStatus(val === "all" ? null : val)}
-// >
-//
-// {status ? status.toLowerCase() : "All statuses"}
-//
-//
-//
-// All statuses
-//
-//
-// Draft
-//
-//
-// Scheduled
-//
-//
-// Sent
-//
-//
-//
-//
-
-// {campaignsQuery.isLoading ? (
-//
-//
-//
-// ) : (
-//
-// {campaignsQuery.data?.campaigns.map((campaign) => (
-//
-//
-//
-//
-//
-
-//
-//
router.push(`/campaigns/${campaign.id}`)}
-// >
-//
-//
-//
-// {campaign.name}
-//
-//
-// {campaign.status.toLowerCase()}
-//
-//
-//
-// {formatDistanceToNow(campaign.createdAt, {
-// addSuffix: true,
-// })}
-//
-//
-//
-
-//
-//
-//
-//
-//
-//
-//
-// ))}
-//
-// )}
-
-// {campaignsQuery.data?.totalPage && campaignsQuery.data.totalPage > 1 ? (
-//
-// setPage((pageNumber - 1).toString())}
-// disabled={pageNumber === 1}
-// >
-// Previous
-//
-// setPage((pageNumber + 1).toString())}
-// disabled={pageNumber >= (campaignsQuery.data?.totalPage ?? 0)}
-// >
-// Next
-//
-//
-// ) : null}
-//
-// );
-// }
diff --git a/apps/web/src/app/(dashboard)/campaigns/delete-campaign.tsx b/apps/web/src/app/(dashboard)/campaigns/delete-campaign.tsx
index 763a76a..584b1c1 100644
--- a/apps/web/src/app/(dashboard)/campaigns/delete-campaign.tsx
+++ b/apps/web/src/app/(dashboard)/campaigns/delete-campaign.tsx
@@ -83,8 +83,10 @@ export const DeleteCampaign: React.FC<{
Delete Campaign
Are you sure you want to delete{" "}
- {campaign.name} ?
- You can't reverse this.
+
+ {campaign.name}
+
+ ? You can't reverse this.
diff --git a/apps/web/src/app/(dashboard)/campaigns/duplicate-campaign.tsx b/apps/web/src/app/(dashboard)/campaigns/duplicate-campaign.tsx
index 17079be..90fa3b2 100644
--- a/apps/web/src/app/(dashboard)/campaigns/duplicate-campaign.tsx
+++ b/apps/web/src/app/(dashboard)/campaigns/duplicate-campaign.tsx
@@ -54,7 +54,10 @@ export const DuplicateCampaign: React.FC<{
Duplicate Campaign
Are you sure you want to duplicate{" "}
- {campaign.name} ?
+
+ {campaign.name}
+
+ ?
diff --git a/apps/web/src/app/(dashboard)/contacts/[contactBookId]/delete-contact.tsx b/apps/web/src/app/(dashboard)/contacts/[contactBookId]/delete-contact.tsx
index a26c914..1f03843 100644
--- a/apps/web/src/app/(dashboard)/contacts/[contactBookId]/delete-contact.tsx
+++ b/apps/web/src/app/(dashboard)/contacts/[contactBookId]/delete-contact.tsx
@@ -87,8 +87,10 @@ export const DeleteContact: React.FC<{
Delete Contact
Are you sure you want to delete{" "}
- {contact.email} ?
- You can't reverse this.
+
+ {contact.email}
+
+ ? You can't reverse this.
diff --git a/apps/web/src/app/(dashboard)/contacts/[contactBookId]/edit-contact.tsx b/apps/web/src/app/(dashboard)/contacts/[contactBookId]/edit-contact.tsx
index dc42a54..909dfe0 100644
--- a/apps/web/src/app/(dashboard)/contacts/[contactBookId]/edit-contact.tsx
+++ b/apps/web/src/app/(dashboard)/contacts/[contactBookId]/edit-contact.tsx
@@ -145,7 +145,7 @@ export const EditContact: React.FC<{
diff --git a/apps/web/src/app/(dashboard)/contacts/delete-contact-book.tsx b/apps/web/src/app/(dashboard)/contacts/delete-contact-book.tsx
index 90695cf..b29ac1e 100644
--- a/apps/web/src/app/(dashboard)/contacts/delete-contact-book.tsx
+++ b/apps/web/src/app/(dashboard)/contacts/delete-contact-book.tsx
@@ -86,7 +86,7 @@ export const DeleteContactBook: React.FC<{
Delete Contact Book
Are you sure you want to delete{" "}
-
+
{contactBook.name}
? You can't reverse this.
diff --git a/apps/web/src/app/(dashboard)/contacts/edit-contact-book.tsx b/apps/web/src/app/(dashboard)/contacts/edit-contact-book.tsx
index 94278c5..a63b8f3 100644
--- a/apps/web/src/app/(dashboard)/contacts/edit-contact-book.tsx
+++ b/apps/web/src/app/(dashboard)/contacts/edit-contact-book.tsx
@@ -78,7 +78,7 @@ export const EditContactBook: React.FC<{
className="p-0 hover:bg-transparent"
onClick={(e) => e.stopPropagation()}
>
-
+
diff --git a/apps/web/src/app/(dashboard)/dasboard-layout.tsx b/apps/web/src/app/(dashboard)/dasboard-layout.tsx
index ad8271d..a5efd6f 100644
--- a/apps/web/src/app/(dashboard)/dasboard-layout.tsx
+++ b/apps/web/src/app/(dashboard)/dasboard-layout.tsx
@@ -1,211 +1,26 @@
"use client";
-import { useSession } from "next-auth/react";
-import Link from "next/link";
-import { LogoutButton, NavButton } from "./nav-button";
-import {
- BookOpenText,
- BookUser,
- CircleUser,
- Code,
- Cog,
- Globe,
- Home,
- LayoutDashboard,
- LayoutTemplate,
- LineChart,
- Mail,
- Menu,
- Package,
- Package2,
- Server,
- Settings,
- ShoppingCart,
- Users,
- Volume2,
-} from "lucide-react";
-import { env } from "~/env";
-import { Sheet, SheetContent, SheetTrigger } from "@unsend/ui/src/sheet";
-import { Button } from "@unsend/ui/src/button";
-import {
- DropdownMenu,
- DropdownMenuContent,
- DropdownMenuItem,
- DropdownMenuLabel,
- DropdownMenuSeparator,
- DropdownMenuTrigger,
-} from "@unsend/ui/src/dropdown-menu";
-import { ThemeSwitcher } from "~/components/theme/ThemeSwitcher";
-import { isCloud, isSelfHosted } from "~/utils/common";
+import { AppSidebar } from "~/components/AppSideBar";
+import { SidebarInset, SidebarTrigger } from "@unsend/ui/src/sidebar";
+import { SidebarProvider } from "@unsend/ui/src/sidebar";
+import { useIsMobile } from "@unsend/ui/src/hooks/use-mobile";
export function DashboardLayout({ children }: { children: React.ReactNode }) {
- const { data: session } = useSession();
+ const isMobile = useIsMobile();
return (
-
-
-
-
-
- Unsend
-
-
- Early access
-
-
-
-
-
-
-
- Dashboard
-
-
-
-
- Emails
-
-
-
-
- Contacts
-
-
-
-
- Templates
-
-
-
-
- Campaigns
-
-
-
-
- Domains
-
-
-
-
- Developer settings
-
-
-
-
- Settings
-
-
- {isSelfHosted() || session?.user.isAdmin ? (
-
-
- Admin
-
- ) : null}
-
-
-
-
-
-
-
-
-
-
-
-
-
- Toggle navigation menu
-
-
-
-
-
-
- Acme Inc
-
-
-
- Dashboard
-
-
-
- Orders
-
-
-
- Products
-
-
-
- Customers
-
-
-
- Analytics
-
-
-
-
-
-
-
-
-
- Toggle user menu
-
-
-
- My Account
-
- Settings
- Support
-
- Logout
-
-
-
-
-
+
+
+
+
+
+ {isMobile ? (
+
+ ) : null}
{children}
-
-
-
+
+
+
);
}
diff --git a/apps/web/src/app/(dashboard)/dashboard/dashboard-chart.tsx b/apps/web/src/app/(dashboard)/dashboard/dashboard-chart.tsx
index abf08c5..336c288 100644
--- a/apps/web/src/app/(dashboard)/dashboard/dashboard-chart.tsx
+++ b/apps/web/src/app/(dashboard)/dashboard/dashboard-chart.tsx
@@ -235,11 +235,13 @@ const DashboardItemCard: React.FC
= ({
{status.toLowerCase()}
-
+
{count}
{status !== "total" ? (
-
{count > 0 ? (percentage * 100).toFixed(0) : 0}%
+
+ {count > 0 ? (percentage * 100).toFixed(0) : 0}%
+
) : null}
diff --git a/apps/web/src/app/(dashboard)/dev-settings/api-keys/delete-api-key.tsx b/apps/web/src/app/(dashboard)/dev-settings/api-keys/delete-api-key.tsx
index 4a66aaa..c95264e 100644
--- a/apps/web/src/app/(dashboard)/dev-settings/api-keys/delete-api-key.tsx
+++ b/apps/web/src/app/(dashboard)/dev-settings/api-keys/delete-api-key.tsx
@@ -83,8 +83,8 @@ export const DeleteApiKey: React.FC<{
Delete API key
Are you sure you want to delete{" "}
- {apiKey.name} ?
- You can't reverse this.
+ {apiKey.name}
+ ? You can't reverse this.
diff --git a/apps/web/src/app/(dashboard)/dev-settings/settings-nav-button.tsx b/apps/web/src/app/(dashboard)/dev-settings/settings-nav-button.tsx
index aa41063..1c350ac 100644
--- a/apps/web/src/app/(dashboard)/dev-settings/settings-nav-button.tsx
+++ b/apps/web/src/app/(dashboard)/dev-settings/settings-nav-button.tsx
@@ -15,9 +15,9 @@ export const SettingsNavButton: React.FC<{
if (comingSoon) {
return (
-
+
{children}
@@ -31,7 +31,7 @@ export const SettingsNavButton: React.FC<{
return (
{children}
diff --git a/apps/web/src/app/(dashboard)/domains/[domainId]/delete-domain.tsx b/apps/web/src/app/(dashboard)/domains/[domainId]/delete-domain.tsx
index c096145..8122cc6 100644
--- a/apps/web/src/app/(dashboard)/domains/[domainId]/delete-domain.tsx
+++ b/apps/web/src/app/(dashboard)/domains/[domainId]/delete-domain.tsx
@@ -5,9 +5,10 @@ import { Input } from "@unsend/ui/src/input";
import {
Dialog,
DialogContent,
- DialogDescription, DialogHeader,
+ DialogDescription,
+ DialogHeader,
DialogTitle,
- DialogTrigger
+ DialogTrigger,
} from "@unsend/ui/src/dialog";
import {
@@ -84,8 +85,8 @@ export const DeleteDomain: React.FC<{ domain: Domain }> = ({ domain }) => {
Delete domain
Are you sure you want to delete{" "}
- {domain.name} ?
- You can't reverse this.
+ {domain.name}
+ ? You can't reverse this.
@@ -288,7 +288,7 @@ const DomainSettings: React.FC<{ domain: Domain }> = ({ domain }) => {
diff --git a/apps/web/src/app/(dashboard)/domains/domain-list.tsx b/apps/web/src/app/(dashboard)/domains/domain-list.tsx
index 1dad6a8..2d86634 100644
--- a/apps/web/src/app/(dashboard)/domains/domain-list.tsx
+++ b/apps/web/src/app/(dashboard)/domains/domain-list.tsx
@@ -104,7 +104,7 @@ const DomainItem: React.FC<{ domain: Domain }> = ({ domain }) => {
@@ -112,7 +112,7 @@ const DomainItem: React.FC<{ domain: Domain }> = ({ domain }) => {
diff --git a/apps/web/src/app/(dashboard)/emails/email-details.tsx b/apps/web/src/app/(dashboard)/emails/email-details.tsx
index 1d70176..e409031 100644
--- a/apps/web/src/app/(dashboard)/emails/email-details.tsx
+++ b/apps/web/src/app/(dashboard)/emails/email-details.tsx
@@ -115,7 +115,7 @@ export default function EmailDetails({ emailId }: { emailId: string }) {
{formatDate(evt.createdAt, "MMM dd, hh:mm a")}
-
+
= ({ href, children, comingSoon }) => {
- const pathname = usePathname();
-
- const isActive = pathname?.startsWith(href);
-
- if (comingSoon) {
- return (
-
-
- {children}
-
-
- soon
-
-
- );
- }
-
- return (
-
- {children}
-
- );
-};
-
-export const LogoutButton: React.FC = () => {
- return (
- signOut()}
- >
-
- Logout
-
- );
-};
diff --git a/apps/web/src/app/(dashboard)/settings/team/delete-team-invite.tsx b/apps/web/src/app/(dashboard)/settings/team/delete-team-invite.tsx
index b0ff28a..d0480a0 100644
--- a/apps/web/src/app/(dashboard)/settings/team/delete-team-invite.tsx
+++ b/apps/web/src/app/(dashboard)/settings/team/delete-team-invite.tsx
@@ -55,7 +55,10 @@ export const DeleteTeamInvite: React.FC<{
Cancel Invite
Are you sure you want to cancel the invite for{" "}
- {invite.email} ?
+
+ {invite.email}
+
+ ?
diff --git a/apps/web/src/app/(dashboard)/templates/delete-template.tsx b/apps/web/src/app/(dashboard)/templates/delete-template.tsx
index de07ece..73ef6c2 100644
--- a/apps/web/src/app/(dashboard)/templates/delete-template.tsx
+++ b/apps/web/src/app/(dashboard)/templates/delete-template.tsx
@@ -83,8 +83,10 @@ export const DeleteTemplate: React.FC<{
Delete Template
Are you sure you want to delete{" "}
- {template.name} ?
- You can't reverse this.
+
+ {template.name}
+
+ ? You can't reverse this.
diff --git a/apps/web/src/app/(dashboard)/templates/duplicate-template.tsx b/apps/web/src/app/(dashboard)/templates/duplicate-template.tsx
index 7f4359d..cc11935 100644
--- a/apps/web/src/app/(dashboard)/templates/duplicate-template.tsx
+++ b/apps/web/src/app/(dashboard)/templates/duplicate-template.tsx
@@ -54,7 +54,10 @@ export const DuplicateTemplate: React.FC<{
Duplicate Template
Are you sure you want to duplicate{" "}
- {template.name} ?
+
+ {template.name}
+
+ ?
diff --git a/apps/web/src/app/(dashboard)/templates/template-list.tsx b/apps/web/src/app/(dashboard)/templates/template-list.tsx
index abaded7..9218d70 100644
--- a/apps/web/src/app/(dashboard)/templates/template-list.tsx
+++ b/apps/web/src/app/(dashboard)/templates/template-list.tsx
@@ -57,7 +57,7 @@ export default function TemplateList() {
{template.name}
diff --git a/apps/web/src/app/layout.tsx b/apps/web/src/app/layout.tsx
index 7060f9e..9651148 100644
--- a/apps/web/src/app/layout.tsx
+++ b/apps/web/src/app/layout.tsx
@@ -24,8 +24,8 @@ export default async function RootLayout({
children: React.ReactNode;
}) {
return (
-
-
+
+
{children}
diff --git a/apps/web/src/app/login/login-page.tsx b/apps/web/src/app/login/login-page.tsx
index 7540fbe..5941b05 100644
--- a/apps/web/src/app/login/login-page.tsx
+++ b/apps/web/src/app/login/login-page.tsx
@@ -130,7 +130,7 @@ export default function LoginPage({
{isSignup ? "Already have an account?" : "New to Unsend?"}
{isSignup ? "Sign in" : "Create new account"}
diff --git a/apps/web/src/components/AppSideBar.tsx b/apps/web/src/components/AppSideBar.tsx
new file mode 100644
index 0000000..247ef00
--- /dev/null
+++ b/apps/web/src/components/AppSideBar.tsx
@@ -0,0 +1,236 @@
+"use client";
+
+import {
+ BookUser,
+ Calendar,
+ Code,
+ Cog,
+ Globe,
+ Home,
+ Inbox,
+ LayoutDashboard,
+ LayoutTemplate,
+ LogOut,
+ Mail,
+ Search,
+ Server,
+ Settings,
+ Volume2,
+ BookOpenText,
+ ChartColumnBig,
+ ChartArea,
+} from "lucide-react";
+import { signOut } from "next-auth/react";
+
+import {
+ Sidebar,
+ SidebarContent,
+ SidebarFooter,
+ SidebarGroup,
+ SidebarGroupContent,
+ SidebarGroupLabel,
+ SidebarHeader,
+ SidebarMenu,
+ SidebarMenuButton,
+ SidebarMenuItem,
+ useSidebar,
+} from "@unsend/ui/src/sidebar";
+import Link from "next/link";
+import { MiniThemeSwitcher, ThemeSwitcher } from "./theme/ThemeSwitcher";
+import { useSession } from "next-auth/react";
+import { isSelfHosted } from "~/utils/common";
+import { usePathname } from "next/navigation";
+import { Badge } from "@unsend/ui/src/badge";
+
+// General items
+const generalItems = [
+ {
+ title: "Analytics",
+ url: "/dashboard",
+ icon: ChartColumnBig,
+ },
+ {
+ title: "Emails",
+ url: "/emails",
+ icon: Mail,
+ },
+ {
+ title: "Templates",
+ url: "/templates",
+ icon: LayoutTemplate,
+ },
+];
+
+// Marketing items
+const marketingItems = [
+ {
+ title: "Contacts",
+ url: "/contacts",
+ icon: BookUser,
+ },
+ {
+ title: "Campaigns",
+ url: "/campaigns",
+ icon: Volume2,
+ },
+];
+
+// Settings items
+const settingsItems = [
+ {
+ title: "Domains",
+ url: "/domains",
+ icon: Globe,
+ },
+ {
+ title: "Developer settings",
+ url: "/dev-settings",
+ icon: Code,
+ },
+ {
+ title: "Settings",
+ url: "/settings",
+ icon: Cog,
+ },
+ // TODO: Add conditional logic for Admin item based on isSelfHosted() || session?.user.isAdmin
+ {
+ title: "Admin",
+ url: "/admin",
+ icon: Server,
+ isAdmin: true,
+ isSelfHosted: true,
+ },
+];
+
+export function AppSidebar() {
+ const { data: session } = useSession();
+ const { state, open } = useSidebar();
+
+ const pathname = usePathname();
+
+ return (
+
+
+
+
+
+ Unsend
+
+ Beta
+
+
+
+
+
+
+ General
+
+
+
+ {generalItems.map((item) => {
+ const isActive = pathname?.startsWith(item.url);
+ return (
+
+
+
+
+ {item.title}
+
+
+
+ );
+ })}
+
+
+
+
+
+ Marketing
+
+
+
+ {marketingItems.map((item) => {
+ const isActive = pathname?.startsWith(item.url);
+ return (
+
+
+
+
+ {item.title}
+
+
+
+ );
+ })}
+
+
+
+
+
+ Settings
+
+
+
+ {settingsItems.map((item) => {
+ const isActive = pathname?.startsWith(item.url);
+
+ if (item.isAdmin && !session?.user.isAdmin) {
+ return null;
+ }
+ if (item.isSelfHosted && !isSelfHosted()) {
+ return null;
+ }
+ return (
+
+
+
+
+ {item.title}
+
+
+
+ );
+ })}
+
+
+
+
+
+
+
+
+ signOut()} tooltip="Logout">
+
+ Logout
+
+
+
+
+
+
+ Docs
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/apps/web/src/components/team/JoinTeam.tsx b/apps/web/src/components/team/JoinTeam.tsx
index b893961..df0fc5f 100644
--- a/apps/web/src/components/team/JoinTeam.tsx
+++ b/apps/web/src/components/team/JoinTeam.tsx
@@ -123,11 +123,11 @@ export default function JoinTeam({
Accept Team Invitation
Are you sure you want to join{" "}
-
+
{selectedInvite?.team.name}
? You will be added as a{" "}
-
+
{selectedInvite?.role.toLowerCase()}
.
diff --git a/apps/web/src/components/theme/ThemeSwitcher.tsx b/apps/web/src/components/theme/ThemeSwitcher.tsx
index 6099e9e..6c6948b 100644
--- a/apps/web/src/components/theme/ThemeSwitcher.tsx
+++ b/apps/web/src/components/theme/ThemeSwitcher.tsx
@@ -3,19 +3,22 @@ import { Button } from "@unsend/ui/src/button";
import { Monitor, Sun, Moon, SunMoonIcon } from "lucide-react";
export const ThemeSwitcher = () => {
- const { theme, setTheme, systemTheme } = useTheme();
+ const { theme, setTheme } = useTheme();
return (
-
-
+
+
Theme
-
+
setTheme("system")}
>
@@ -24,8 +27,8 @@ export const ThemeSwitcher = () => {
variant="ghost"
size="sm"
className={cn(
- "p-0.5 h-5 w-5",
- theme === "light" ? " bg-gray-200" : ""
+ "p-0.5 rounded-[0.20rem] h-5 w-5",
+ theme === "light" ? " bg-muted" : ""
)}
onClick={() => setTheme("light")}
>
@@ -34,7 +37,10 @@ export const ThemeSwitcher = () => {
setTheme("dark")}
>
@@ -43,3 +49,34 @@ export const ThemeSwitcher = () => {
);
};
+
+export const MiniThemeSwitcher = () => {
+ const { theme, setTheme } = useTheme();
+
+ const cycleTheme = () => {
+ if (theme === "light") {
+ setTheme("dark");
+ } else if (theme === "dark") {
+ setTheme("system");
+ } else {
+ setTheme("light");
+ }
+ };
+
+ const renderIcon = () => {
+ switch (theme) {
+ case "light":
+ return
;
+ case "dark":
+ return
;
+ default:
+ return
;
+ }
+ };
+
+ return (
+
+ {renderIcon()}
+
+ );
+};
diff --git a/packages/email-editor/src/nodes/image-resize.tsx b/packages/email-editor/src/nodes/image-resize.tsx
index 100b277..6d11edd 100644
--- a/packages/email-editor/src/nodes/image-resize.tsx
+++ b/packages/email-editor/src/nodes/image-resize.tsx
@@ -206,7 +206,7 @@ export function ResizableImageTemplate(props: NodeViewProps) {
className="flex items-center justify-center opacity-70"
/>
-
+
) : (
diff --git a/packages/email-editor/src/nodes/variable.tsx b/packages/email-editor/src/nodes/variable.tsx
index 4e62f58..1ae350a 100644
--- a/packages/email-editor/src/nodes/variable.tsx
+++ b/packages/email-editor/src/nodes/variable.tsx
@@ -180,7 +180,7 @@ export function VariableComponent(props: NodeViewProps) {
{
e.preventDefault();
diff --git a/packages/tailwind-config/tailwind.config.ts b/packages/tailwind-config/tailwind.config.ts
index 0782794..a211282 100644
--- a/packages/tailwind-config/tailwind.config.ts
+++ b/packages/tailwind-config/tailwind.config.ts
@@ -47,6 +47,20 @@ const config = {
DEFAULT: "hsl(var(--card))",
foreground: "hsl(var(--card-foreground))",
},
+ sidebar: {
+ DEFAULT: "hsl(var(--sidebar-background))",
+ foreground: "hsl(var(--sidebar-foreground))",
+ primary: "hsl(var(--sidebar-primary))",
+ "primary-foreground": "hsl(var(--sidebar-primary-foreground))",
+ accent: "hsl(var(--sidebar-accent))",
+ "accent-foreground": "hsl(var(--sidebar-accent-foreground))",
+ border: "hsl(var(--sidebar-border))",
+ ring: "hsl(var(--sidebar-ring))",
+ },
+ success: {
+ DEFAULT: "hsl(var(--success))",
+ foreground: "hsl(var(--success-foreground))",
+ },
},
borderRadius: {
lg: "var(--radius)",
diff --git a/packages/ui/src/badge.tsx b/packages/ui/src/badge.tsx
index 6b3134f..8e40395 100644
--- a/packages/ui/src/badge.tsx
+++ b/packages/ui/src/badge.tsx
@@ -9,7 +9,7 @@ const badgeVariants = cva(
variants: {
variant: {
default:
- "border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
+ "border-transparent bg-primary text-foreground-foreground hover:bg-primary/80",
secondary:
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
destructive:
diff --git a/packages/ui/src/dialog.tsx b/packages/ui/src/dialog.tsx
index 01dace8..e7c6cf2 100644
--- a/packages/ui/src/dialog.tsx
+++ b/packages/ui/src/dialog.tsx
@@ -21,7 +21,7 @@ const DialogOverlay = React.forwardRef<
(
+ undefined
+ );
+
+ React.useEffect(() => {
+ const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`);
+ const onChange = () => {
+ setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
+ };
+ mql.addEventListener("change", onChange);
+ setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
+ return () => mql.removeEventListener("change", onChange);
+ }, []);
+
+ return !!isMobile;
+}
diff --git a/packages/ui/src/select.tsx b/packages/ui/src/select.tsx
index 8e0cf4f..b32b4b2 100644
--- a/packages/ui/src/select.tsx
+++ b/packages/ui/src/select.tsx
@@ -75,7 +75,7 @@ const SelectContent = React.forwardRef<
>(({ className, ...props }, ref) => (
void;
+ openMobile: boolean;
+ setOpenMobile: (open: boolean) => void;
+ isMobile: boolean;
+ toggleSidebar: () => void;
+};
+
+const SidebarContext = React.createContext(null);
+
+function useSidebar() {
+ const context = React.useContext(SidebarContext);
+ if (!context) {
+ throw new Error("useSidebar must be used within a SidebarProvider.");
+ }
+
+ return context;
+}
+
+const SidebarProvider = React.forwardRef<
+ HTMLDivElement,
+ React.ComponentProps<"div"> & {
+ defaultOpen?: boolean;
+ open?: boolean;
+ onOpenChange?: (open: boolean) => void;
+ }
+>(
+ (
+ {
+ defaultOpen = true,
+ open: openProp,
+ onOpenChange: setOpenProp,
+ className,
+ style,
+ children,
+ ...props
+ },
+ ref
+ ) => {
+ const isMobile = useIsMobile();
+ const [openMobile, setOpenMobile] = React.useState(false);
+
+ // This is the internal state of the sidebar.
+ // We use openProp and setOpenProp for control from outside the component.
+ const [_open, _setOpen] = React.useState(defaultOpen);
+ const open = openProp ?? _open;
+ const setOpen = React.useCallback(
+ (value: boolean | ((value: boolean) => boolean)) => {
+ const openState = typeof value === "function" ? value(open) : value;
+ if (setOpenProp) {
+ setOpenProp(openState);
+ } else {
+ _setOpen(openState);
+ }
+
+ // This sets the cookie to keep the sidebar state.
+ document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`;
+ },
+ [setOpenProp, open]
+ );
+
+ // Helper to toggle the sidebar.
+ const toggleSidebar = React.useCallback(() => {
+ return isMobile
+ ? setOpenMobile((open) => !open)
+ : setOpen((open) => !open);
+ }, [isMobile, setOpen, setOpenMobile]);
+
+ // Adds a keyboard shortcut to toggle the sidebar.
+ React.useEffect(() => {
+ const handleKeyDown = (event: KeyboardEvent) => {
+ if (
+ event.key === SIDEBAR_KEYBOARD_SHORTCUT &&
+ (event.metaKey || event.ctrlKey)
+ ) {
+ event.preventDefault();
+ toggleSidebar();
+ }
+ };
+
+ window.addEventListener("keydown", handleKeyDown);
+ return () => window.removeEventListener("keydown", handleKeyDown);
+ }, [toggleSidebar]);
+
+ // We add a state so that we can do data-state="expanded" or "collapsed".
+ // This makes it easier to style the sidebar with Tailwind classes.
+ const state = open ? "expanded" : "collapsed";
+
+ const contextValue = React.useMemo(
+ () => ({
+ state,
+ open,
+ setOpen,
+ isMobile,
+ openMobile,
+ setOpenMobile,
+ toggleSidebar,
+ }),
+ [state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar]
+ );
+
+ return (
+
+
+
+ {children}
+
+
+
+ );
+ }
+);
+SidebarProvider.displayName = "SidebarProvider";
+
+const Sidebar = React.forwardRef<
+ HTMLDivElement,
+ React.ComponentProps<"div"> & {
+ side?: "left" | "right";
+ variant?: "sidebar" | "floating" | "inset";
+ collapsible?: "offcanvas" | "icon" | "none";
+ }
+>(
+ (
+ {
+ side = "left",
+ variant = "sidebar",
+ collapsible = "offcanvas",
+ className,
+ children,
+ ...props
+ },
+ ref
+ ) => {
+ const { isMobile, state, openMobile, setOpenMobile } = useSidebar();
+
+ if (collapsible === "none") {
+ return (
+
+ {children}
+
+ );
+ }
+
+ if (isMobile) {
+ return (
+
+
+
+ Sidebar
+ Displays the mobile sidebar.
+
+ {children}
+
+
+ );
+ }
+
+ return (
+
+ {/* This is what handles the sidebar gap on desktop */}
+
+
+
+ );
+ }
+);
+Sidebar.displayName = "Sidebar";
+
+const SidebarTrigger = React.forwardRef<
+ React.ElementRef,
+ React.ComponentProps
+>(({ className, onClick, ...props }, ref) => {
+ const { toggleSidebar } = useSidebar();
+
+ return (
+ {
+ onClick?.(event);
+ toggleSidebar();
+ }}
+ {...props}
+ >
+
+ Toggle Sidebar
+
+ );
+});
+SidebarTrigger.displayName = "SidebarTrigger";
+
+const SidebarRail = React.forwardRef<
+ HTMLButtonElement,
+ React.ComponentProps<"button">
+>(({ className, ...props }, ref) => {
+ const { toggleSidebar } = useSidebar();
+
+ return (
+
+ );
+});
+SidebarRail.displayName = "SidebarRail";
+
+const SidebarInset = React.forwardRef<
+ HTMLDivElement,
+ React.ComponentProps<"main">
+>(({ className, ...props }, ref) => {
+ return (
+
+ );
+});
+SidebarInset.displayName = "SidebarInset";
+
+const SidebarInput = React.forwardRef<
+ React.ElementRef,
+ React.ComponentProps
+>(({ className, ...props }, ref) => {
+ return (
+
+ );
+});
+SidebarInput.displayName = "SidebarInput";
+
+const SidebarHeader = React.forwardRef<
+ HTMLDivElement,
+ React.ComponentProps<"div">
+>(({ className, ...props }, ref) => {
+ return (
+
+ );
+});
+SidebarHeader.displayName = "SidebarHeader";
+
+const SidebarFooter = React.forwardRef<
+ HTMLDivElement,
+ React.ComponentProps<"div">
+>(({ className, ...props }, ref) => {
+ return (
+
+ );
+});
+SidebarFooter.displayName = "SidebarFooter";
+
+const SidebarSeparator = React.forwardRef<
+ React.ElementRef,
+ React.ComponentProps
+>(({ className, ...props }, ref) => {
+ return (
+
+ );
+});
+SidebarSeparator.displayName = "SidebarSeparator";
+
+const SidebarContent = React.forwardRef<
+ HTMLDivElement,
+ React.ComponentProps<"div">
+>(({ className, ...props }, ref) => {
+ return (
+
+ );
+});
+SidebarContent.displayName = "SidebarContent";
+
+const SidebarGroup = React.forwardRef<
+ HTMLDivElement,
+ React.ComponentProps<"div">
+>(({ className, ...props }, ref) => {
+ return (
+
+ );
+});
+SidebarGroup.displayName = "SidebarGroup";
+
+const SidebarGroupLabel = React.forwardRef<
+ HTMLDivElement,
+ React.ComponentProps<"div"> & { asChild?: boolean }
+>(({ className, asChild = false, ...props }, ref) => {
+ const Comp = asChild ? Slot : "div";
+
+ return (
+ svg]:size-4 [&>svg]:shrink-0",
+ "group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0",
+ className
+ )}
+ {...props}
+ />
+ );
+});
+SidebarGroupLabel.displayName = "SidebarGroupLabel";
+
+const SidebarGroupAction = React.forwardRef<
+ HTMLButtonElement,
+ React.ComponentProps<"button"> & { asChild?: boolean }
+>(({ className, asChild = false, ...props }, ref) => {
+ const Comp = asChild ? Slot : "button";
+
+ return (
+ svg]:size-4 [&>svg]:shrink-0",
+ // Increases the hit area of the button on mobile.
+ "after:absolute after:-inset-2 after:md:hidden",
+ "group-data-[collapsible=icon]:hidden",
+ className
+ )}
+ {...props}
+ />
+ );
+});
+SidebarGroupAction.displayName = "SidebarGroupAction";
+
+const SidebarGroupContent = React.forwardRef<
+ HTMLDivElement,
+ React.ComponentProps<"div">
+>(({ className, ...props }, ref) => (
+
+));
+SidebarGroupContent.displayName = "SidebarGroupContent";
+
+const SidebarMenu = React.forwardRef<
+ HTMLUListElement,
+ React.ComponentProps<"ul">
+>(({ className, ...props }, ref) => (
+
+));
+SidebarMenu.displayName = "SidebarMenu";
+
+const SidebarMenuItem = React.forwardRef<
+ HTMLLIElement,
+ React.ComponentProps<"li">
+>(({ className, ...props }, ref) => (
+
+));
+SidebarMenuItem.displayName = "SidebarMenuItem";
+
+const sidebarMenuButtonVariants = cva(
+ "peer/menu-button flex w-full items-center text-sidebar-foreground gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-none ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-[[data-sidebar=menu-action]]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:!size-8 group-data-[collapsible=icon]:!p-2 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0",
+ {
+ variants: {
+ variant: {
+ default: "hover:bg-sidebar-accent hover:text-sidebar-accent-foreground",
+ outline:
+ "bg-background shadow-[0_0_0_1px_hsl(var(--sidebar-border))] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_hsl(var(--sidebar-accent))]",
+ },
+ size: {
+ default: "h-8 text-sm",
+ sm: "h-7 text-xs",
+ lg: "h-12 text-sm group-data-[collapsible=icon]:!p-0",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ size: "default",
+ },
+ }
+);
+
+const SidebarMenuButton = React.forwardRef<
+ HTMLButtonElement,
+ React.ComponentProps<"button"> & {
+ asChild?: boolean;
+ isActive?: boolean;
+ tooltip?: string | React.ComponentProps;
+ } & VariantProps
+>(
+ (
+ {
+ asChild = false,
+ isActive = false,
+ variant = "default",
+ size = "default",
+ tooltip,
+ className,
+ ...props
+ },
+ ref
+ ) => {
+ const Comp = asChild ? Slot : "button";
+ const { isMobile, state } = useSidebar();
+
+ const button = (
+
+ );
+
+ if (!tooltip) {
+ return button;
+ }
+
+ if (typeof tooltip === "string") {
+ tooltip = {
+ children: tooltip,
+ };
+ }
+
+ return (
+
+ {button}
+
+
+ );
+ }
+);
+SidebarMenuButton.displayName = "SidebarMenuButton";
+
+const SidebarMenuAction = React.forwardRef<
+ HTMLButtonElement,
+ React.ComponentProps<"button"> & {
+ asChild?: boolean;
+ showOnHover?: boolean;
+ }
+>(({ className, asChild = false, showOnHover = false, ...props }, ref) => {
+ const Comp = asChild ? Slot : "button";
+
+ return (
+ svg]:size-4 [&>svg]:shrink-0",
+ // Increases the hit area of the button on mobile.
+ "after:absolute after:-inset-2 after:md:hidden",
+ "peer-data-[size=sm]/menu-button:top-1",
+ "peer-data-[size=default]/menu-button:top-1.5",
+ "peer-data-[size=lg]/menu-button:top-2.5",
+ "group-data-[collapsible=icon]:hidden",
+ showOnHover &&
+ "group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 peer-data-[active=true]/menu-button:text-sidebar-accent-foreground md:opacity-0",
+ className
+ )}
+ {...props}
+ />
+ );
+});
+SidebarMenuAction.displayName = "SidebarMenuAction";
+
+const SidebarMenuBadge = React.forwardRef<
+ HTMLDivElement,
+ React.ComponentProps<"div">
+>(({ className, ...props }, ref) => (
+
+));
+SidebarMenuBadge.displayName = "SidebarMenuBadge";
+
+const SidebarMenuSkeleton = React.forwardRef<
+ HTMLDivElement,
+ React.ComponentProps<"div"> & {
+ showIcon?: boolean;
+ }
+>(({ className, showIcon = false, ...props }, ref) => {
+ // Random width between 50 to 90%.
+ const width = React.useMemo(() => {
+ return `${Math.floor(Math.random() * 40) + 50}%`;
+ }, []);
+
+ return (
+
+ {showIcon && (
+
+ )}
+
+
+ );
+});
+SidebarMenuSkeleton.displayName = "SidebarMenuSkeleton";
+
+const SidebarMenuSub = React.forwardRef<
+ HTMLUListElement,
+ React.ComponentProps<"ul">
+>(({ className, ...props }, ref) => (
+
+));
+SidebarMenuSub.displayName = "SidebarMenuSub";
+
+const SidebarMenuSubItem = React.forwardRef<
+ HTMLLIElement,
+ React.ComponentProps<"li">
+>(({ ...props }, ref) => );
+SidebarMenuSubItem.displayName = "SidebarMenuSubItem";
+
+const SidebarMenuSubButton = React.forwardRef<
+ HTMLAnchorElement,
+ React.ComponentProps<"a"> & {
+ asChild?: boolean;
+ size?: "sm" | "md";
+ isActive?: boolean;
+ }
+>(({ asChild = false, size = "md", isActive, className, ...props }, ref) => {
+ const Comp = asChild ? Slot : "a";
+
+ return (
+ span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0 [&>svg]:text-sidebar-accent-foreground",
+ "data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground",
+ size === "sm" && "text-xs",
+ size === "md" && "text-sm",
+ "group-data-[collapsible=icon]:hidden",
+ className
+ )}
+ {...props}
+ />
+ );
+});
+SidebarMenuSubButton.displayName = "SidebarMenuSubButton";
+
+export {
+ Sidebar,
+ SidebarContent,
+ SidebarFooter,
+ SidebarGroup,
+ SidebarGroupAction,
+ SidebarGroupContent,
+ SidebarGroupLabel,
+ SidebarHeader,
+ SidebarInput,
+ SidebarInset,
+ SidebarMenu,
+ SidebarMenuAction,
+ SidebarMenuBadge,
+ SidebarMenuButton,
+ SidebarMenuItem,
+ SidebarMenuSkeleton,
+ SidebarMenuSub,
+ SidebarMenuSubButton,
+ SidebarMenuSubItem,
+ SidebarProvider,
+ SidebarRail,
+ SidebarSeparator,
+ SidebarTrigger,
+ useSidebar,
+};
diff --git a/packages/ui/src/skeleton.tsx b/packages/ui/src/skeleton.tsx
new file mode 100644
index 0000000..979edae
--- /dev/null
+++ b/packages/ui/src/skeleton.tsx
@@ -0,0 +1,15 @@
+import { cn } from "../lib/utils";
+
+function Skeleton({
+ className,
+ ...props
+}: React.HTMLAttributes) {
+ return (
+
+ );
+}
+
+export { Skeleton };
diff --git a/packages/ui/src/switch.tsx b/packages/ui/src/switch.tsx
index 78ca024..3687623 100644
--- a/packages/ui/src/switch.tsx
+++ b/packages/ui/src/switch.tsx
@@ -19,7 +19,7 @@ const Switch = React.forwardRef<
>
diff --git a/packages/ui/styles/globals.css b/packages/ui/styles/globals.css
index 8659a25..e3e0785 100644
--- a/packages/ui/styles/globals.css
+++ b/packages/ui/styles/globals.css
@@ -5,31 +5,34 @@
@layer base {
:root,
.light {
- --background: 0 0% 100%;
- --foreground: 222.2 84% 4.9%;
+ --background: 220 2% 96%;
+ --foreground: 234 16% 35%;
--card: 0 0% 100%;
--card-foreground: 222.2 84% 4.9%;
- --popover: 0 0% 100%;
+ --popover: 220 2% 96%;
--popover-foreground: 222.2 84% 4.9%;
- --primary: 222.2 47.4% 11.2%;
+ --primary: 200 65% 14%;
--primary-foreground: 210 40% 98%;
--secondary: 210 40% 96.1%;
--secondary-foreground: 222.2 47.4% 11.2%;
- --muted: 210 40% 96.1%;
+ --muted: 240 11% 88%;
--muted-foreground: 215.4 16.3% 46.9%;
- --accent: 210 40% 96.1%;
+ --accent: 240 11% 88%;
--accent-foreground: 222.2 47.4% 11.2%;
- --destructive: 0 84% 60%;
+ --destructive: 347 62% 55%;
--destructive-foreground: 210 40% 98%;
- --border: 220 14% 96%;
+ --success: 142 49% 44%;
+ --success-foreground: 210 40% 98%;
+
+ --border: 220 21% 89%;
--input: 214.3 31.8% 91.4%;
--ring: 222.2 84% 4.9%;
@@ -40,34 +43,46 @@
--chart-3: 197 37% 24%;
--chart-4: 43 74% 66%;
--chart-5: 27 87% 67%;
+
+ --sidebar-background: 225 3% 94%;
+ --sidebar-foreground: 240 5.3% 26.1%;
+ --sidebar-primary: 240 5.9% 10%;
+ --sidebar-primary-foreground: 0 0% 98%;
+ --sidebar-accent: 240 11% 88%;
+ --sidebar-accent-foreground: 240 5.9% 10%;
+ --sidebar-border: 240 11% 88%;
+ --sidebar-ring: 217.2 91.2% 59.8%;
}
.dark {
- --background: 223 3% 3%;
- --foreground: 210 3% 82%;
+ --background: 240 21% 15%;
+ --foreground: 226 64% 88%;
--card: 222.2 84% 4.9%;
--card-foreground: 210 40% 98%;
- --popover: 222.2 84% 4.9%;
+ --popover: 240 21% 15%;
--popover-foreground: 210 40% 98%;
- --primary: 210 40% 98%;
- --primary-foreground: 222.2 47.4% 11.2%;
+ --primary: 220 23% 95%;
+ --primary-foreground: 240 23% 9%;
--secondary: 217.2 32.6% 17.5%;
--secondary-foreground: 210 40% 98%;
- --muted: 217.2 32.6% 17.5%;
+ --muted: 237 16% 23%;
--muted-foreground: 215 20.2% 65.1%;
- --accent: 217.2 32.6% 17.5%;
+ --accent: 237 16% 23%;
--accent-foreground: 210 40% 98%;
- --destructive: 0 68% 41%;
- --destructive-foreground: 210 40% 98%;
+ --destructive: 343 81% 75%;
+ --destructive-foreground: 240 21% 15%;
- --border: 217.2 32.6% 17.5%;
+ --success: 115 54% 76%;
+ --success-foreground: 210 40% 98%;
+
+ --border: 237 16% 23%;
--input: 217.2 32.6% 17.5%;
--ring: 217.2 32.6% 17.5%;
@@ -76,6 +91,15 @@
--chart-3: 30 80% 55%;
--chart-4: 280 65% 60%;
--chart-5: 340 75% 55%;
+
+ --sidebar-background: 240 21% 12%;
+ --sidebar-foreground: 226 64% 88%;
+ --sidebar-primary: 224.3 76.3% 48%;
+ --sidebar-primary-foreground: 0 0% 100%;
+ --sidebar-accent: 237 17% 20%;
+ --sidebar-accent-foreground: 240 4.8% 95.9%;
+ --sidebar-border: 240 21% 15%;
+ --sidebar-ring: 217.2 91.2% 59.8%;
}
}