diff --git a/components.json b/components.json index 4b9e1f37..6a92383f 100644 --- a/components.json +++ b/components.json @@ -11,6 +11,9 @@ }, "aliases": { "components": "@/components", - "utils": "@/lib/utils" + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/lib/hooks" } -} \ No newline at end of file +} diff --git a/components/layouts/app.tsx b/components/layouts/app.tsx index cf5b227a..a2970226 100644 --- a/components/layouts/app.tsx +++ b/components/layouts/app.tsx @@ -1,19 +1,72 @@ -import Sidebar from "../Sidebar"; +import { AppSidebar } from "@/components/sidebar/app-sidebar"; + +import { + Breadcrumb, + BreadcrumbItem, + BreadcrumbLink, + BreadcrumbList, + BreadcrumbPage, + BreadcrumbSeparator, +} from "../ui/breadcrumb"; +import { Separator } from "../ui/separator"; +import { SidebarInset, SidebarTrigger } from "../ui/sidebar"; +import { SidebarProvider } from "../ui/sidebar"; import TrialBanner from "./trial-banner"; -export default function AppLayout({ children }: { children: React.ReactNode }) { +export default function AppLayout({ + breadcrumbs, + children, +}: { + breadcrumbs?: { title: string; href: string }[]; + children: React.ReactNode; +}) { return ( -
- -
- {/* Trial banner shown only on trial */} - -
-
+ +
+ + +
+
+ + {breadcrumbs && ( + <> + + + + {breadcrumbs.length > 1 && + breadcrumbs.slice(0, -1).map((breadcrumb, index) => ( + + + {breadcrumb.title} + + + ))} + {breadcrumbs.length > 1 && ( + + )} + + + {breadcrumbs[breadcrumbs.length - 1].title} + + + + + + )} +
+
+ {/* Trial banner shown only on trial */} + +
+ {/*
*/} {children} -
-
+ {/*
*/} +
+
-
+ ); } diff --git a/components/sidebar/app-sidebar.tsx b/components/sidebar/app-sidebar.tsx new file mode 100644 index 00000000..cf58f218 --- /dev/null +++ b/components/sidebar/app-sidebar.tsx @@ -0,0 +1,248 @@ +"use client"; + +import Link from "next/link"; +import { useRouter } from "next/router"; + +import * as React from "react"; +import { useEffect, useState } from "react"; + +import { TeamContextType, initialState, useTeam } from "@/context/team-context"; +import Cookies from "js-cookie"; +import { + AudioWaveform, + BookOpen, + Bot, + CogIcon, + Command, + ContactIcon, + FolderIcon, + Frame, + GalleryVerticalEnd, + LifeBuoy, + Loader, + Map, + PaletteIcon, + PieChart, + Send, + ServerIcon, + Settings2, + SquareTerminal, +} from "lucide-react"; + +import { NavMain } from "@/components/sidebar/nav-main"; +import { NavUser } from "@/components/sidebar/nav-user"; +import { TeamSwitcher } from "@/components/sidebar/team-switcher"; +import { + Sidebar, + SidebarContent, + SidebarFooter, + SidebarHeader, + SidebarMenu, + SidebarMenuButton, + SidebarMenuItem, + SidebarTrigger, +} from "@/components/ui/sidebar"; + +import { usePlan } from "@/lib/swr/use-billing"; +import useLimits from "@/lib/swr/use-limits"; +import { nFormatter } from "@/lib/utils"; + +import ProBanner from "../billing/pro-banner"; +import { Progress } from "../ui/progress"; + +export function AppSidebar({ ...props }: React.ComponentProps) { + const router = useRouter(); + const [showProBanner, setShowProBanner] = useState(null); + const { currentTeam, teams, setCurrentTeam, isLoading }: TeamContextType = + useTeam() || initialState; + const { plan: userPlan, trial: userTrial } = usePlan(); + const { limits } = useLimits(); + const linksLimit = limits?.links; + const documentsLimit = limits?.documents; + + useEffect(() => { + if (Cookies.get("hideProBanner") !== "pro-banner") { + setShowProBanner(true); + } else { + setShowProBanner(false); + } + }, []); + + const data = { + navMain: [ + { + title: "All Documents", + url: "/documents", + icon: FolderIcon, + current: + router.pathname.includes("documents") && + !router.pathname.includes("tree") && + !router.pathname.includes("datarooms"), + }, + { + title: "All Datarooms", + url: "/datarooms", + icon: ServerIcon, + current: router.pathname.includes("datarooms"), + }, + { + title: "Visitors", + url: "/visitors", + icon: ContactIcon, + current: router.pathname.includes("visitors"), + }, + { + title: "Branding", + url: "/branding", + icon: PaletteIcon, + current: + router.pathname.includes("branding") && + !router.pathname.includes("datarooms"), + }, + { + title: "Settings", + url: "/settings/general", + icon: CogIcon, + isActive: + router.pathname.includes("settings") && + !router.pathname.includes("branding") && + !router.pathname.includes("datarooms") && + !router.pathname.includes("documents"), + items: [ + { + title: "General", + url: "/settings/general", + current: router.pathname.includes("settings/general"), + }, + { + title: "People", + url: "/settings/people", + current: router.pathname.includes("settings/people"), + }, + { + title: "Domains", + url: "/settings/domains", + current: router.pathname.includes("settings/domains"), + }, + { + title: "Billing", + url: "/settings/billing", + current: router.pathname.includes("settings/billing"), + }, + ], + }, + ], + }; + + return ( + + +

+ P +

+

+ Papermark + {userPlan && userPlan != "free" ? ( + + {userPlan.charAt(0).toUpperCase() + userPlan.slice(1)} + + ) : null} + {userTrial ? ( + + Trial + + ) : null} +

+ {isLoading ? ( +
+ Loading teams... +
+ ) : ( + + )} +
+ + + {/* */} + {/* */} + + + + +
+ {/* + * if user is free and showProBanner is true show pro banner + */} + {userPlan === "free" && showProBanner ? ( + + ) : null} + +
+ {linksLimit ? ( + + ) : null} + {documentsLimit ? ( + + ) : null} + {linksLimit || documentsLimit ? ( +

+ Change plan to increase usage limits +

+ ) : null} +
+
+
+
+ +
+
+ ); +} + +function UsageProgress(data: { + title: string; + unit: string; + usage?: number; + usageLimit?: number; +}) { + let { title, unit, usage, usageLimit } = data; + let usagePercentage = 0; + if (usage !== undefined && usageLimit !== undefined) { + usagePercentage = (usage / usageLimit) * 100; + } + + return ( +
+
+ {usage !== undefined && usageLimit !== undefined ? ( +

+ {nFormatter(usage)} / {nFormatter(usageLimit)} {unit} +

+ ) : ( +
+ )} + +
+
+ ); +} diff --git a/components/sidebar/nav-main.tsx b/components/sidebar/nav-main.tsx new file mode 100644 index 00000000..8564afd0 --- /dev/null +++ b/components/sidebar/nav-main.tsx @@ -0,0 +1,92 @@ +"use client"; + +import { ChevronRight, type LucideIcon } from "lucide-react"; + +import { + Collapsible, + CollapsibleContent, + CollapsibleTrigger, +} from "@/components/ui/collapsible"; +import { + SidebarGroup, + SidebarGroupLabel, + SidebarMenu, + SidebarMenuAction, + SidebarMenuButton, + SidebarMenuItem, + SidebarMenuSub, + SidebarMenuSubButton, + SidebarMenuSubItem, +} from "@/components/ui/sidebar"; + +import { cn } from "@/lib/utils"; + +export function NavMain({ + items, +}: { + items: { + title: string; + url: string; + icon: LucideIcon; + current?: boolean; + isActive?: boolean; + items?: { + title: string; + url: string; + current?: boolean; + }[]; + }[]; +}) { + return ( + + + {items.map((item) => ( + + + + + + {item.title} + + + {item.items?.length ? ( + <> + + + + Toggle + + + + + {item.items?.map((subItem) => ( + + + + {subItem.title} + + + + ))} + + + + ) : null} + + + ))} + + + ); +} diff --git a/components/sidebar/nav-user.tsx b/components/sidebar/nav-user.tsx new file mode 100644 index 00000000..21794fb3 --- /dev/null +++ b/components/sidebar/nav-user.tsx @@ -0,0 +1,126 @@ +"use client"; + +import { + BadgeCheck, + Bell, + ChevronsUpDown, + CreditCard, + LifeBuoyIcon, + LogOut, + MailIcon, + Settings2, + Sparkles, +} from "lucide-react"; +import { useSession } from "next-auth/react"; + +import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuGroup, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { + SidebarMenu, + SidebarMenuButton, + SidebarMenuItem, + useSidebar, +} from "@/components/ui/sidebar"; + +import { ModeToggle } from "../theme-toggle"; + +export function NavUser() { + const { data: session, status } = useSession(); + const { isMobile } = useSidebar(); + + return ( + + + + + + + + + {session?.user?.name?.charAt(0) || + session?.user?.email?.charAt(0)} + + +
+ + {session?.user?.name || ""} + + + {session?.user?.email || ""} + +
+ +
+
+ + +
+ + + + {session?.user?.name?.charAt(0) || + session?.user?.email?.charAt(0)} + + +
+ + {session?.user?.name || ""} + + + {session?.user?.email || ""} + +
+
+
+ + + + + + User Settings + + + + + + + Help Center + + + + Contact Support + + + + + + Log out + +
+
+
+
+ ); +} diff --git a/components/sidebar/team-switcher.tsx b/components/sidebar/team-switcher.tsx new file mode 100644 index 00000000..170fd211 --- /dev/null +++ b/components/sidebar/team-switcher.tsx @@ -0,0 +1,114 @@ +"use client"; + +import * as React from "react"; + +import { ChevronsUpDown, GalleryVerticalEndIcon, Plus } from "lucide-react"; + +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuShortcut, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { + SidebarMenu, + SidebarMenuButton, + SidebarMenuItem, + useSidebar, +} from "@/components/ui/sidebar"; + +import { Team } from "@/lib/types"; +import { cn } from "@/lib/utils"; + +import { Avatar, AvatarFallback } from "../ui/avatar"; + +export function TeamSwitcher({ + currentTeam: activeTeam, + teams, + setCurrentTeam, +}: { + currentTeam: Team | null; + teams: Team[]; + setCurrentTeam: (team: Team) => void; +}) { + const { isMobile } = useSidebar(); + + const switchTeam = (team: Team) => { + localStorage.setItem("currentTeamId", team.id); + setCurrentTeam(team); + }; + + if (!activeTeam) return null; + + const plan = activeTeam.plan?.split("+")[0]; + const isTrial = activeTeam.plan?.includes("trial"); + + return ( + + + + + + + + {activeTeam?.name?.slice(0, 2).toUpperCase()} + + +
+ {activeTeam.name} + + {isTrial ? "Data Room Trial" : plan} + +
+ +
+
+ + + Teams + + {teams.map((team, index) => ( + switchTeam(team)} + className={cn( + "gap-2 p-2", + team.id === activeTeam.id && "bg-muted font-medium", + )} + > + {/*
+ +
*/} + + + {team?.name?.slice(0, 2).toUpperCase()} + + + {team.name} + {/* ⌘{index + 1} */} +
+ ))} + {/* + +
+ +
+
Add team
+
*/} +
+
+
+
+ ); +} diff --git a/components/theme-toggle.tsx b/components/theme-toggle.tsx index 49b66d03..7543ea77 100644 --- a/components/theme-toggle.tsx +++ b/components/theme-toggle.tsx @@ -21,11 +21,11 @@ export function ModeToggle() { return ( - - Themes + + Change Theme - + 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 ( + + +
{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 ( + + ); +}); +SidebarTrigger.displayName = "SidebarTrigger"; + +const SidebarRail = React.forwardRef< + HTMLButtonElement, + React.ComponentProps<"button"> +>(({ className, ...props }, ref) => { + const { toggleSidebar } = useSidebar(); + + return ( + + )}
@@ -261,7 +268,7 @@ export default function Billing() { )} {team?.users.map((member, index) => (
  • @@ -276,7 +283,7 @@ export default function Billing() {
    - + {getUserDocumentCount(member.userId)}{" "} {getUserDocumentCount(member.userId) === 1 ? "document" diff --git a/styles/globals.css b/styles/globals.css index 2478003c..a96f0dab 100644 --- a/styles/globals.css +++ b/styles/globals.css @@ -37,6 +37,15 @@ --ring: 217.9 10.6% 64.9%; /* gray-400 */ --radius: 0.5rem; /* md */ + + --sidebar-background: 0 0% 98%; /* white */ + --sidebar-foreground: 240 5.3% 26.1%; /* gray-900 */ + --sidebar-primary: 240 5.9% 10%; /* gray-950 */ + --sidebar-primary-foreground: 0 0% 98%; /* white */ + --sidebar-accent: 240 4.8% 95.9%; /* gray-100 */ + --sidebar-accent-foreground: 240 5.9% 10%; /* gray-950 */ + --sidebar-border: 220 13% 91%; /* gray-200 */ + --sidebar-ring: 217.2 91.2% 59.8%; /* gray-400 */ } .dark { @@ -71,6 +80,15 @@ --warning-foreground: 38 92% 95%; /* amber-50 */ --ring: 215 27.9% 16.9%; /* gray-800 */ + + --sidebar-background: 240 5.9% 10%; /* gray-950 */ + --sidebar-foreground: 240 4.8% 95.9%; /* gray-100 */ + --sidebar-primary: 224.3 76.3% 48%; /* blue-500 */ + --sidebar-primary-foreground: 0 0% 100%; /* white */ + --sidebar-accent: 240 3.7% 15.9%; /* gray-200 */ + --sidebar-accent-foreground: 240 4.8% 95.9%; /* gray-950 */ + --sidebar-border: 240 3.7% 15.9%; /* gray-200 */ + --sidebar-ring: 217.2 91.2% 59.8%; /* gray-400 */ } } diff --git a/tailwind.config.js b/tailwind.config.js index 2b3af614..04ba6bed 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -9,211 +9,226 @@ module.exports = { "./node_modules/@tremor/**/*.{js,ts,jsx,tsx}", // TREMOR ], theme: { - container: { - center: true, - padding: "2rem", - screens: { - "2xl": "1400px", - }, - }, - extend: { - colors: { - border: "hsl(var(--border))", - input: "hsl(var(--input))", - ring: "hsl(var(--ring))", - background: "hsl(var(--background))", - foreground: "hsl(var(--foreground))", - primary: { - DEFAULT: "hsl(var(--primary))", - foreground: "hsl(var(--primary-foreground))", - }, - secondary: { - DEFAULT: "hsl(var(--secondary))", - foreground: "hsl(var(--secondary-foreground))", - }, - destructive: { - DEFAULT: "hsl(var(--destructive))", - foreground: "hsl(var(--destructive-foreground))", - }, - warning: { - DEFAULT: "hsl(var(--warning))", - foreground: "hsl(var(--warning-foreground))", - }, - muted: { - DEFAULT: "hsl(var(--muted))", - foreground: "hsl(var(--muted-foreground))", - }, - accent: { - DEFAULT: "hsl(var(--accent))", - foreground: "hsl(var(--accent-foreground))", - }, - popover: { - DEFAULT: "hsl(var(--popover))", - foreground: "hsl(var(--popover-foreground))", - }, - card: { - DEFAULT: "hsl(var(--card))", - foreground: "hsl(var(--card-foreground))", - }, - - //** START: TREMOR **// - - // light mode - tremor: { - brand: { - faint: "#eff6ff", // blue-50 - muted: "#bfdbfe", // blue-200 - subtle: "#60a5fa", // blue-400 - DEFAULT: "#3b82f6", // blue-500 - emphasis: "#1d4ed8", // blue-700 - inverted: "#ffffff", // white - }, - background: { - muted: "#f9fafb", // gray-50 - subtle: "#f3f4f6", // gray-100 - DEFAULT: "#ffffff", // white - emphasis: "#374151", // gray-700 - }, - border: { - DEFAULT: "#e5e7eb", // gray-200 - }, - ring: { - DEFAULT: "#e5e7eb", // gray-200 - }, - content: { - subtle: "#9ca3af", // gray-400 - DEFAULT: "#6b7280", // gray-500 - emphasis: "#374151", // gray-700 - strong: "#111827", // gray-900 - inverted: "#ffffff", // white - }, - }, - // dark mode - "dark-tremor": { - brand: { - faint: "#0B1229", // custom - muted: "#172554", // blue-950 - subtle: "#1e40af", // blue-800 - DEFAULT: "#3b82f6", // blue-500 - emphasis: "#60a5fa", // blue-400 - inverted: "#030712", // gray-950 - }, - background: { - muted: "#131A2B", // custom - subtle: "#1f2937", // gray-800 - DEFAULT: "#111827", // gray-900 - emphasis: "#d1d5db", // gray-300 - }, - border: { - DEFAULT: "#1f2937", // gray-800 - }, - ring: { - DEFAULT: "#1f2937", // gray-800 - }, - content: { - subtle: "#4b5563", // gray-600 - DEFAULT: "#6b7280", // gray-600 - emphasis: "#e5e7eb", // gray-200 - strong: "#f9fafb", // gray-50 - inverted: "#000000", // black - }, - }, - //** END: TREMOR **// - }, - boxShadow: { - //** START: TREMOR boxShadow **// - // light - "tremor-input": "0 1px 2px 0 rgb(0 0 0 / 0.05)", - "tremor-card": - "0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1)", - "tremor-dropdown": - "0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)", - // dark - "dark-tremor-input": "0 1px 2px 0 rgb(0 0 0 / 0.05)", - "dark-tremor-card": - "0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1)", - "dark-tremor-dropdown": - "0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)", - //** END: TREMOR boxShadow **// - }, - borderRadius: { - //** START: TREMOR borderRadius **// - "tremor-small": "0.375rem", - "tremor-default": "0.5rem", - "tremor-full": "9999px", - //** END: TREMOR borderRadius **// - lg: "var(--radius)", - md: "calc(var(--radius) - 2px)", - sm: "calc(var(--radius) - 4px)", - }, - fontSize: { - //** START: TREMOR fontSize **// - "tremor-label": ["0.75rem"], - "tremor-default": ["0.875rem", { lineHeight: "1.25rem" }], - "tremor-title": ["1.125rem", { lineHeight: "1.75rem" }], - "tremor-metric": ["1.875rem", { lineHeight: "2.25rem" }], - //** END: TREMOR fontSize **// - }, - keyframes: { - // Modal - "scale-in": { - "0%": { transform: "scale(0.95)" }, - "100%": { transform: "scale(1)" }, - }, - "fade-in": { - "0%": { opacity: "0" }, - "100%": { opacity: "1" }, - }, - // Others - gauge_fadeIn: { - from: { opacity: "0" }, - to: { opacity: "1" }, - }, - gauge_fill: { - from: { "stroke-dashoffset": "332", opacity: "0" }, - to: { opacity: "1" }, - }, - flyEmoji: { - "0%": { - transform: "translateY(0) scale(1)", - opacity: "0.7", - }, - "100%": { - transform: "translateY(-150px) scale(2)", - opacity: "0", - }, - }, - "accordion-down": { - from: { height: "0" }, - to: { height: "var(--radix-accordion-content-height)" }, - }, - "accordion-up": { - from: { height: "var(--radix-accordion-content-height)" }, - to: { height: "0" }, - }, - // input-otp - "caret-blink": { - "0%,70%,100%": { opacity: "1" }, - "20%,50%": { opacity: "0" }, - }, - }, - gridTemplateColumns: { - 16: "repeat(16, minmax(0, 1fr))", - }, - animation: { - // Modal - "scale-in": "scale-in 0.2s cubic-bezier(0.16, 1, 0.3, 1)", - "fade-in": "fade-in 0.2s ease-out forwards", - // Others - gauge_fadeIn: "gauge_fadeIn 1s ease forwards", - gauge_fill: "gauge_fill 1s ease forwards", - flyEmoji: "flyEmoji 1s forwards", - "accordion-down": "accordion-down 0.2s ease-out", - "accordion-up": "accordion-up 0.2s ease-out", - // input-otp - "caret-blink": "caret-blink 1.25s ease-out infinite", - }, - }, + container: { + center: 'true', + padding: '2rem', + screens: { + '2xl': '1400px' + } + }, + extend: { + colors: { + border: 'hsl(var(--border))', + input: 'hsl(var(--input))', + ring: 'hsl(var(--ring))', + background: 'hsl(var(--background))', + foreground: 'hsl(var(--foreground))', + primary: { + DEFAULT: 'hsl(var(--primary))', + foreground: 'hsl(var(--primary-foreground))' + }, + secondary: { + DEFAULT: 'hsl(var(--secondary))', + foreground: 'hsl(var(--secondary-foreground))' + }, + destructive: { + DEFAULT: 'hsl(var(--destructive))', + foreground: 'hsl(var(--destructive-foreground))' + }, + warning: { + DEFAULT: 'hsl(var(--warning))', + foreground: 'hsl(var(--warning-foreground))' + }, + muted: { + DEFAULT: 'hsl(var(--muted))', + foreground: 'hsl(var(--muted-foreground))' + }, + accent: { + DEFAULT: 'hsl(var(--accent))', + foreground: 'hsl(var(--accent-foreground))' + }, + popover: { + DEFAULT: 'hsl(var(--popover))', + foreground: 'hsl(var(--popover-foreground))' + }, + card: { + DEFAULT: 'hsl(var(--card))', + foreground: 'hsl(var(--card-foreground))' + }, + tremor: { + brand: { + faint: '#eff6ff', + muted: '#bfdbfe', + subtle: '#60a5fa', + DEFAULT: '#3b82f6', + emphasis: '#1d4ed8', + inverted: '#ffffff' + }, + background: { + muted: '#f9fafb', + subtle: '#f3f4f6', + DEFAULT: '#ffffff', + emphasis: '#374151' + }, + border: { + DEFAULT: '#e5e7eb' + }, + ring: { + DEFAULT: '#e5e7eb' + }, + content: { + subtle: '#9ca3af', + DEFAULT: '#6b7280', + emphasis: '#374151', + strong: '#111827', + inverted: '#ffffff' + } + }, + 'dark-tremor': { + brand: { + faint: '#0B1229', + muted: '#172554', + subtle: '#1e40af', + DEFAULT: '#3b82f6', + emphasis: '#60a5fa', + inverted: '#030712' + }, + background: { + muted: '#131A2B', + subtle: '#1f2937', + DEFAULT: '#111827', + emphasis: '#d1d5db' + }, + border: { + DEFAULT: '#1f2937' + }, + ring: { + DEFAULT: '#1f2937' + }, + content: { + subtle: '#4b5563', + DEFAULT: '#6b7280', + emphasis: '#e5e7eb', + strong: '#f9fafb', + inverted: '#000000' + } + }, + 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))' + } + }, + boxShadow: { + 'tremor-input': '0 1px 2px 0 rgb(0 0 0 / 0.05)', + 'tremor-card': '0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1)', + 'tremor-dropdown': '0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)', + 'dark-tremor-input': '0 1px 2px 0 rgb(0 0 0 / 0.05)', + 'dark-tremor-card': '0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1)', + 'dark-tremor-dropdown': '0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)' + }, + borderRadius: { + 'tremor-small': '0.375rem', + 'tremor-default': '0.5rem', + 'tremor-full': '9999px', + lg: 'var(--radius)', + md: 'calc(var(--radius) - 2px)', + sm: 'calc(var(--radius) - 4px)' + }, + fontSize: { + 'tremor-label': ["0.75rem"], + 'tremor-default': ["0.875rem", { lineHeight: "1.25rem" }], + 'tremor-title': ["1.125rem", { lineHeight: "1.75rem" }], + 'tremor-metric': ["1.875rem", { lineHeight: "2.25rem" }] + }, + keyframes: { + 'scale-in': { + '0%': { + transform: 'scale(0.95)' + }, + '100%': { + transform: 'scale(1)' + } + }, + 'fade-in': { + '0%': { + opacity: '0' + }, + '100%': { + opacity: '1' + } + }, + gauge_fadeIn: { + from: { + opacity: '0' + }, + to: { + opacity: '1' + } + }, + gauge_fill: { + from: { + 'stroke-dashoffset': '332', + opacity: '0' + }, + to: { + opacity: '1' + } + }, + flyEmoji: { + '0%': { + transform: 'translateY(0) scale(1)', + opacity: '0.7' + }, + '100%': { + transform: 'translateY(-150px) scale(2)', + opacity: '0' + } + }, + 'accordion-down': { + from: { + height: '0' + }, + to: { + height: 'var(--radix-accordion-content-height)' + } + }, + 'accordion-up': { + from: { + height: 'var(--radix-accordion-content-height)' + }, + to: { + height: '0' + } + }, + 'caret-blink': { + '0%,70%,100%': { + opacity: '1' + }, + '20%,50%': { + opacity: '0' + } + } + }, + gridTemplateColumns: { + '16': 'repeat(16, minmax(0, 1fr))' + }, + animation: { + 'scale-in': 'scale-in 0.2s cubic-bezier(0.16, 1, 0.3, 1)', + 'fade-in': 'fade-in 0.2s ease-out forwards', + gauge_fadeIn: 'gauge_fadeIn 1s ease forwards', + gauge_fill: 'gauge_fill 1s ease forwards', + flyEmoji: 'flyEmoji 1s forwards', + 'accordion-down': 'accordion-down 0.2s ease-out', + 'accordion-up': 'accordion-up 0.2s ease-out', + 'caret-blink': 'caret-blink 1.25s ease-out infinite' + } + } }, //** START: TREMOR safelist **// safelist: [