From 14c222090492fca91fdd7b710494de1f1409e763 Mon Sep 17 00:00:00 2001 From: Giancarlo Buomprisco Date: Fri, 25 Oct 2024 09:43:34 +0200 Subject: [PATCH] Update Shadcn Sidebar (#73) Migrated Sidebar to use Shadcn UI's --- apps/e2e/package.json | 2 +- .../site-header-account-section.tsx | 3 - apps/web/app/(marketing)/docs/layout.tsx | 2 +- .../app/admin/_components/admin-sidebar.tsx | 78 +- .../_components/home-account-selector.tsx | 6 +- .../_components/home-mobile-navigation.tsx | 9 - .../home/(user)/_components/home-sidebar.tsx | 68 +- .../team-account-layout-mobile-navigation.tsx | 9 - ...team-account-layout-sidebar-navigation.tsx | 50 +- .../team-account-layout-sidebar.tsx | 41 +- .../personal-account-navigation.config.tsx | 42 +- .../config/team-account-navigation.config.tsx | 13 +- apps/web/package.json | 12 +- apps/web/public/locales/en/common.json | 3 +- apps/web/styles/globals.css | 20 + packages/analytics/package.json | 2 +- packages/billing/gateway/package.json | 2 +- packages/billing/lemon-squeezy/package.json | 2 +- packages/billing/stripe/package.json | 2 +- packages/cms/core/package.json | 2 +- packages/cms/keystatic/package.json | 4 +- packages/cms/wordpress/package.json | 2 +- packages/features/accounts/package.json | 4 +- .../src/components/account-selector.tsx | 18 +- .../components/personal-account-dropdown.tsx | 23 +- packages/features/admin/package.json | 4 +- packages/features/auth/package.json | 4 +- packages/features/notifications/package.json | 2 +- packages/features/team-accounts/package.json | 4 +- packages/i18n/package.json | 4 +- packages/mailers/core/package.json | 2 +- packages/mailers/resend/package.json | 2 +- packages/next/package.json | 2 +- packages/supabase/package.json | 4 +- packages/ui/package.json | 7 +- packages/ui/src/hooks/use-mobile.tsx | 21 + .../src/makerkit/navigation-config.schema.ts | 44 +- packages/ui/src/makerkit/page.tsx | 9 +- packages/ui/src/makerkit/profile-avatar.tsx | 14 +- packages/ui/src/makerkit/sidebar.tsx | 16 +- packages/ui/src/shadcn/collapsible.tsx | 11 + packages/ui/src/shadcn/sheet.tsx | 13 +- packages/ui/src/shadcn/sidebar.tsx | 1033 +++++++++++++++++ packages/ui/src/shadcn/skeleton.tsx | 15 + pnpm-lock.yaml | 762 +++++++----- tooling/eslint/package.json | 2 +- tooling/prettier/index.mjs | 2 +- tooling/tailwind/index.ts | 10 + 48 files changed, 1863 insertions(+), 543 deletions(-) create mode 100644 packages/ui/src/hooks/use-mobile.tsx create mode 100644 packages/ui/src/shadcn/collapsible.tsx create mode 100644 packages/ui/src/shadcn/sidebar.tsx create mode 100644 packages/ui/src/shadcn/skeleton.tsx diff --git a/apps/e2e/package.json b/apps/e2e/package.json index 1061aeb0d..3344c2162 100644 --- a/apps/e2e/package.json +++ b/apps/e2e/package.json @@ -13,7 +13,7 @@ "license": "ISC", "devDependencies": { "@playwright/test": "^1.48.1", - "@types/node": "^22.7.8", + "@types/node": "^22.7.9", "node-html-parser": "^6.1.13" } } diff --git a/apps/web/app/(marketing)/_components/site-header-account-section.tsx b/apps/web/app/(marketing)/_components/site-header-account-section.tsx index 783dda21a..560f1a63c 100644 --- a/apps/web/app/(marketing)/_components/site-header-account-section.tsx +++ b/apps/web/app/(marketing)/_components/site-header-account-section.tsx @@ -22,9 +22,6 @@ const ModeToggle = dynamic( import('@kit/ui/mode-toggle').then((mod) => ({ default: mod.ModeToggle, })), - { - ssr: false, - }, ); const paths = { diff --git a/apps/web/app/(marketing)/docs/layout.tsx b/apps/web/app/(marketing)/docs/layout.tsx index 4981d372c..c4337487d 100644 --- a/apps/web/app/(marketing)/docs/layout.tsx +++ b/apps/web/app/(marketing)/docs/layout.tsx @@ -11,7 +11,7 @@ async function DocsLayout({ children }: React.PropsWithChildren) { const pages = await getDocs(resolvedLanguage); return ( -
+
{children} diff --git a/apps/web/app/admin/_components/admin-sidebar.tsx b/apps/web/app/admin/_components/admin-sidebar.tsx index 6a9047e90..b72cb045f 100644 --- a/apps/web/app/admin/_components/admin-sidebar.tsx +++ b/apps/web/app/admin/_components/admin-sidebar.tsx @@ -1,40 +1,70 @@ -import { Home, Users } from 'lucide-react'; +'use client'; + +import Link from 'next/link'; +import { usePathname } from 'next/navigation'; + +import { LayoutDashboard, Users } from 'lucide-react'; import { Sidebar, SidebarContent, + SidebarFooter, SidebarGroup, - SidebarItem, -} from '@kit/ui/sidebar'; + SidebarGroupContent, + SidebarGroupLabel, + SidebarHeader, + SidebarMenu, + SidebarMenuButton, + SidebarProvider, +} from '@kit/ui/shadcn-sidebar'; import { AppLogo } from '~/components/app-logo'; import { ProfileAccountDropdownContainer } from '~/components/personal-account-dropdown-container'; export function AdminSidebar() { + const path = usePathname(); + return ( - - - - + + + + + - - - }> - Home - + + + Admin - } - > - Accounts - - - + + + + + + Dashboard + + - - - - + + + + Accounts + + + + + + + + + + + + ); } diff --git a/apps/web/app/home/(user)/_components/home-account-selector.tsx b/apps/web/app/home/(user)/_components/home-account-selector.tsx index 7cdb55803..814f05773 100644 --- a/apps/web/app/home/(user)/_components/home-account-selector.tsx +++ b/apps/web/app/home/(user)/_components/home-account-selector.tsx @@ -5,7 +5,7 @@ import { useContext } from 'react'; import { useRouter } from 'next/navigation'; import { AccountSelector } from '@kit/accounts/account-selector'; -import { SidebarContext } from '@kit/ui/sidebar'; +import { SidebarContext } from '@kit/ui/shadcn-sidebar'; import featureFlagsConfig from '~/config/feature-flags.config'; import pathsConfig from '~/config/paths.config'; @@ -25,11 +25,11 @@ export function HomeAccountSelector(props: { collisionPadding?: number; }) { const router = useRouter(); - const { collapsed } = useContext(SidebarContext); + const context = useContext(SidebarContext); return ( ; } - - return ( - - ); }); return ( diff --git a/apps/web/app/home/(user)/_components/home-sidebar.tsx b/apps/web/app/home/(user)/_components/home-sidebar.tsx index 5bfef3825..93156501f 100644 --- a/apps/web/app/home/(user)/_components/home-sidebar.tsx +++ b/apps/web/app/home/(user)/_components/home-sidebar.tsx @@ -1,5 +1,12 @@ import { If } from '@kit/ui/if'; -import { Sidebar, SidebarContent, SidebarNavigation } from '@kit/ui/sidebar'; +import { + Sidebar, + SidebarContent, + SidebarFooter, + SidebarHeader, + SidebarNavigation, + SidebarProvider, +} from '@kit/ui/shadcn-sidebar'; import { cn } from '@kit/ui/utils'; import { AppLogo } from '~/components/app-logo'; @@ -16,43 +23,44 @@ interface HomeSidebarProps { workspace: UserWorkspace; } +const minimized = personalAccountNavigationConfig.sidebarCollapsed; + export function HomeSidebar(props: HomeSidebarProps) { const { workspace, user, accounts } = props.workspace; - const collapsed = personalAccountNavigationConfig.sidebarCollapsed; return ( - - -
- - } - > - - + + + +
+ + } + > + + -
- +
+ +
-
- +
- - - - -
- + -
-
+ + + + + +
); } diff --git a/apps/web/app/home/[account]/_components/team-account-layout-mobile-navigation.tsx b/apps/web/app/home/[account]/_components/team-account-layout-mobile-navigation.tsx index 945188da9..ac644fb1f 100644 --- a/apps/web/app/home/[account]/_components/team-account-layout-mobile-navigation.tsx +++ b/apps/web/app/home/[account]/_components/team-account-layout-mobile-navigation.tsx @@ -65,15 +65,6 @@ export const TeamAccountLayoutMobileNavigation = ( if ('divider' in item) { return ; } - - return ( - - ); }, ); diff --git a/apps/web/app/home/[account]/_components/team-account-layout-sidebar-navigation.tsx b/apps/web/app/home/[account]/_components/team-account-layout-sidebar-navigation.tsx index e484f2f78..2163fe5b8 100644 --- a/apps/web/app/home/[account]/_components/team-account-layout-sidebar-navigation.tsx +++ b/apps/web/app/home/[account]/_components/team-account-layout-sidebar-navigation.tsx @@ -1,5 +1,4 @@ -import { SidebarDivider, SidebarGroup, SidebarItem } from '@kit/ui/sidebar'; -import { Trans } from '@kit/ui/trans'; +import { SidebarNavigation } from '@kit/ui/shadcn-sidebar'; import { getTeamAccountSidebarConfig } from '~/config/team-account-navigation.config'; @@ -8,50 +7,7 @@ export function TeamAccountLayoutSidebarNavigation({ }: React.PropsWithChildren<{ account: string; }>) { - const routes = getTeamAccountSidebarConfig(account).routes; + const routes = getTeamAccountSidebarConfig(account); - return ( - <> - {routes.map((item, index) => { - if ('divider' in item) { - return ; - } - - if ('children' in item) { - return ( - } - collapsible={item.collapsible} - collapsed={item.collapsed} - > - {item.children.map((child) => { - return ( - - - - ); - })} - - ); - } - - return ( - - - - ); - })} - - ); + return ; } diff --git a/apps/web/app/home/[account]/_components/team-account-layout-sidebar.tsx b/apps/web/app/home/[account]/_components/team-account-layout-sidebar.tsx index eed3eb8c4..a416bb0fb 100644 --- a/apps/web/app/home/[account]/_components/team-account-layout-sidebar.tsx +++ b/apps/web/app/home/[account]/_components/team-account-layout-sidebar.tsx @@ -1,10 +1,15 @@ 'use client'; -import { useContext } from 'react'; - import type { User } from '@supabase/supabase-js'; -import { Sidebar, SidebarContent, SidebarContext } from '@kit/ui/sidebar'; +import { + Sidebar, + SidebarContent, + SidebarFooter, + SidebarHeader, + SidebarProvider, + useSidebar, +} from '@kit/ui/shadcn-sidebar'; import { cn } from '@kit/ui/utils'; import { ProfileAccountDropdownContainer } from '~/components//personal-account-dropdown-container'; @@ -26,17 +31,17 @@ export function TeamAccountLayoutSidebar(props: { accounts: AccountModel[]; user: User; }) { - const collapsed = getTeamAccountSidebarConfig(props.account).sidebarCollapsed; + const minimized = getTeamAccountSidebarConfig(props.account).sidebarCollapsed; return ( - + - + ); } @@ -48,48 +53,44 @@ function SidebarContainer(props: { }) { const { account, accounts, user } = props; const userId = user.id; - const { collapsed } = useContext(SidebarContext); + const { minimized } = useSidebar(); const className = cn( 'flex max-w-full items-center justify-between space-x-4', { - 'w-full justify-start space-x-0': collapsed, + 'w-full justify-start space-x-0': minimized, }, ); return ( - <> - + +
-
+
- + -
+ -
- + + ); } diff --git a/apps/web/config/personal-account-navigation.config.tsx b/apps/web/config/personal-account-navigation.config.tsx index d98702ab6..466962f0b 100644 --- a/apps/web/config/personal-account-navigation.config.tsx +++ b/apps/web/config/personal-account-navigation.config.tsx @@ -1,4 +1,5 @@ import { CreditCard, Home, User } from 'lucide-react'; +import { z } from 'zod'; import { NavigationConfigSchema } from '@kit/ui/navigation-schema'; @@ -9,25 +10,34 @@ const iconClasses = 'w-4'; const routes = [ { - label: 'common:routes.home', - path: pathsConfig.app.home, - Icon: , - end: true, + label: 'common:routes.application', + children: [ + { + label: 'common:routes.home', + path: pathsConfig.app.home, + Icon: , + end: true, + }, + ], }, { - label: 'common:routes.account', - path: pathsConfig.app.personalAccountSettings, - Icon: , + label: 'common:routes.settings', + children: [ + { + label: 'common:routes.profile', + path: pathsConfig.app.personalAccountSettings, + Icon: , + }, + featureFlagsConfig.enablePersonalAccountBilling + ? { + label: 'common:routes.billing', + path: pathsConfig.app.personalAccountBilling, + Icon: , + } + : undefined, + ].filter(route => !!route), }, -]; - -if (featureFlagsConfig.enablePersonalAccountBilling) { - routes.push({ - label: 'common:routes.billing', - path: pathsConfig.app.personalAccountBilling, - Icon: , - }); -} +] satisfies z.infer['routes']; export const personalAccountNavigationConfig = NavigationConfigSchema.parse({ routes, diff --git a/apps/web/config/team-account-navigation.config.tsx b/apps/web/config/team-account-navigation.config.tsx index e95522fd9..2daeffc8b 100644 --- a/apps/web/config/team-account-navigation.config.tsx +++ b/apps/web/config/team-account-navigation.config.tsx @@ -9,10 +9,15 @@ const iconClasses = 'w-4'; const getRoutes = (account: string) => [ { - label: 'common:routes.dashboard', - path: pathsConfig.app.accountHome.replace('[account]', account), - Icon: , - end: true, + label: 'common:routes.application', + children: [ + { + label: 'common:routes.dashboard', + path: pathsConfig.app.accountHome.replace('[account]', account), + Icon: , + end: true, + }, + ], }, { label: 'common:routes.settings', diff --git a/apps/web/package.json b/apps/web/package.json index 81315cb29..5fb3e85dd 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -31,7 +31,7 @@ "supabase:db:dump:local": "supabase db dump --local --data-only" }, "dependencies": { - "@edge-csrf/nextjs": "2.5.0", + "@edge-csrf/nextjs": "2.5.1", "@hookform/resolvers": "^3.9.0", "@kit/accounts": "workspace:^", "@kit/admin": "workspace:^", @@ -56,11 +56,11 @@ "@marsidev/react-turnstile": "^1.0.2", "@radix-ui/react-icons": "^1.3.0", "@supabase/supabase-js": "^2.45.6", - "@tanstack/react-query": "5.59.15", + "@tanstack/react-query": "5.59.16", "@tanstack/react-table": "^8.20.5", "date-fns": "^4.1.0", "lucide-react": "^0.453.0", - "next": "15.0.0", + "next": "15.0.1", "next-sitemap": "^4.2.3", "next-themes": "0.3.0", "react": "19.0.0-rc-69d4b800-20241021", @@ -77,9 +77,9 @@ "@kit/prettier-config": "workspace:^", "@kit/tailwind-config": "workspace:^", "@kit/tsconfig": "workspace:^", - "@next/bundle-analyzer": "15.0.0", + "@next/bundle-analyzer": "15.0.1", "@types/mdx": "^2.0.13", - "@types/node": "^22.7.8", + "@types/node": "^22.7.9", "@types/react": "npm:types-react@19.0.0-rc.1", "@types/react-dom": "npm:types-react-dom@19.0.0-rc.1", "autoprefixer": "^10.4.20", @@ -89,7 +89,7 @@ "import-in-the-middle": "1.11.2", "prettier": "^3.3.3", "require-in-the-middle": "7.4.0", - "supabase": "^1.207.8", + "supabase": "^1.207.9", "tailwindcss": "3.4.14", "typescript": "^5.6.3" }, diff --git a/apps/web/public/locales/en/common.json b/apps/web/public/locales/en/common.json index b06cb9073..ecea6e78d 100644 --- a/apps/web/public/locales/en/common.json +++ b/apps/web/public/locales/en/common.json @@ -62,7 +62,8 @@ "billing": "Billing", "dashboard": "Dashboard", "settings": "Settings", - "profile": "Profile" + "profile": "Profile", + "application": "Application" }, "roles": { "owner": { diff --git a/apps/web/styles/globals.css b/apps/web/styles/globals.css index 4d1a906c9..0ecf3915f 100644 --- a/apps/web/styles/globals.css +++ b/apps/web/styles/globals.css @@ -24,11 +24,21 @@ --input: 214.3 31.8% 91.4%; --ring: 224 71.4% 4.1%; --radius: 0.5rem; + --chart-1: 12 76% 61%; --chart-2: 173 58% 39%; --chart-3: 197 37% 24%; --chart-4: 43 74% 66%; --chart-5: 27 87% 67%; + + --sidebar-background: 0 0% 98%; + --sidebar-foreground: 240 5.3% 26.1%; + --sidebar-primary: 240 5.9% 10%; + --sidebar-primary-foreground: 0 0% 98%; + --sidebar-accent: 240 4.8% 95.9%; + --sidebar-accent-foreground: 240 5.9% 10%; + --sidebar-border: 220 13% 91%; + --sidebar-ring: 217.2 91.2% 59.8%; } .dark { @@ -51,11 +61,21 @@ --border: 215 27.9% 13%; --input: 215 27.9% 13%; --ring: 216 12.2% 83.9%; + --chart-1: 220 70% 50%; --chart-2: 160 60% 45%; --chart-3: 30 80% 55%; --chart-4: 280 65% 60%; --chart-5: 340 75% 55%; + + --sidebar-background: 224 71.4% 4.1%; + --sidebar-foreground: 240 4.8% 95.9%; + --sidebar-primary: 224.3 76.3% 48%; + --sidebar-primary-foreground: 0 0% 100%; + --sidebar-accent: 215 27.9% 13%; + --sidebar-accent-foreground: 240 4.8% 95.9%; + --sidebar-border: 240 3.7% 15.9%; + --sidebar-ring: 217.2 91.2% 59.8%; } } diff --git a/packages/analytics/package.json b/packages/analytics/package.json index c33399106..fdb921121 100644 --- a/packages/analytics/package.json +++ b/packages/analytics/package.json @@ -17,7 +17,7 @@ "@kit/prettier-config": "workspace:*", "@kit/tailwind-config": "workspace:*", "@kit/tsconfig": "workspace:*", - "@types/node": "^22.7.8" + "@types/node": "^22.7.9" }, "eslintConfig": { "root": true, diff --git a/packages/billing/gateway/package.json b/packages/billing/gateway/package.json index 871df2e76..70382c349 100644 --- a/packages/billing/gateway/package.json +++ b/packages/billing/gateway/package.json @@ -31,7 +31,7 @@ "@types/react": "^18.3.11", "date-fns": "^4.1.0", "lucide-react": "^0.453.0", - "next": "15.0.0", + "next": "15.0.1", "react": "19.0.0-rc-69d4b800-20241021", "react-hook-form": "^7.53.1", "react-i18next": "^15.1.0", diff --git a/packages/billing/lemon-squeezy/package.json b/packages/billing/lemon-squeezy/package.json index 9a3b6d7a4..93844b46b 100644 --- a/packages/billing/lemon-squeezy/package.json +++ b/packages/billing/lemon-squeezy/package.json @@ -26,7 +26,7 @@ "@kit/tsconfig": "workspace:*", "@kit/ui": "workspace:^", "@types/react": "^18.3.11", - "next": "15.0.0", + "next": "15.0.1", "react": "19.0.0-rc-69d4b800-20241021", "zod": "^3.23.8" }, diff --git a/packages/billing/stripe/package.json b/packages/billing/stripe/package.json index 2b60506e5..b3a5851df 100644 --- a/packages/billing/stripe/package.json +++ b/packages/billing/stripe/package.json @@ -30,7 +30,7 @@ "@kit/ui": "workspace:^", "@types/react": "^18.3.11", "date-fns": "^4.1.0", - "next": "15.0.0", + "next": "15.0.1", "react": "19.0.0-rc-69d4b800-20241021", "zod": "^3.23.8" }, diff --git a/packages/cms/core/package.json b/packages/cms/core/package.json index 5ccc5ec8a..231ec5a6c 100644 --- a/packages/cms/core/package.json +++ b/packages/cms/core/package.json @@ -19,7 +19,7 @@ "@kit/prettier-config": "workspace:*", "@kit/tsconfig": "workspace:*", "@kit/wordpress": "workspace:^", - "@types/node": "^22.7.8" + "@types/node": "^22.7.9" }, "eslintConfig": { "root": true, diff --git a/packages/cms/keystatic/package.json b/packages/cms/keystatic/package.json index db5c44ec5..5e16da809 100644 --- a/packages/cms/keystatic/package.json +++ b/packages/cms/keystatic/package.json @@ -16,7 +16,7 @@ "./route-handler": "./src/keystatic-route-handler.ts" }, "dependencies": { - "@keystatic/core": "0.5.38", + "@keystatic/core": "0.5.39", "@keystatic/next": "^5.0.1", "@markdoc/markdoc": "^0.4.0" }, @@ -26,7 +26,7 @@ "@kit/prettier-config": "workspace:*", "@kit/tsconfig": "workspace:*", "@kit/ui": "workspace:^", - "@types/node": "^22.7.8", + "@types/node": "^22.7.9", "@types/react": "^18.3.11", "react": "19.0.0-rc-69d4b800-20241021", "zod": "^3.23.8" diff --git a/packages/cms/wordpress/package.json b/packages/cms/wordpress/package.json index 4cb1d693d..0e0f16601 100644 --- a/packages/cms/wordpress/package.json +++ b/packages/cms/wordpress/package.json @@ -20,7 +20,7 @@ "@kit/prettier-config": "workspace:*", "@kit/tsconfig": "workspace:*", "@kit/ui": "workspace:^", - "@types/node": "^22.7.8", + "@types/node": "^22.7.9", "@types/react": "^18.3.11", "wp-types": "^4.66.1" }, diff --git a/packages/features/accounts/package.json b/packages/features/accounts/package.json index bc8a4bede..d41b89653 100644 --- a/packages/features/accounts/package.json +++ b/packages/features/accounts/package.json @@ -35,11 +35,11 @@ "@kit/ui": "workspace:^", "@radix-ui/react-icons": "^1.3.0", "@supabase/supabase-js": "^2.45.6", - "@tanstack/react-query": "5.59.15", + "@tanstack/react-query": "5.59.16", "@types/react": "^18.3.11", "@types/react-dom": "^18.3.1", "lucide-react": "^0.453.0", - "next": "15.0.0", + "next": "15.0.1", "next-themes": "0.3.0", "react": "19.0.0-rc-69d4b800-20241021", "react-dom": "19.0.0-rc-69d4b800-20241021", diff --git a/packages/features/accounts/src/components/account-selector.tsx b/packages/features/accounts/src/components/account-selector.tsx index 2dd037733..ce00fbf3b 100644 --- a/packages/features/accounts/src/components/account-selector.tsx +++ b/packages/features/accounts/src/components/account-selector.tsx @@ -86,7 +86,7 @@ export function AccountSelector({ pictureUrl ? ( ) : ( - + ); return ( @@ -103,7 +103,7 @@ export function AccountSelector({ 'dark:shadow-primary/10 group w-full min-w-0 px-2 lg:w-auto lg:max-w-fit', { 'justify-start': !collapsed, - 'm-auto justify-center px-4 lg:w-full': collapsed, + 'm-auto justify-center px-2 lg:w-full': collapsed, }, className, )} @@ -111,7 +111,7 @@ export function AccountSelector({ + {(account) => ( - - + + - + {account.label ? account.label[0] : ''} @@ -210,11 +210,11 @@ export function AccountSelector({ }} >
- + + ); diff --git a/packages/features/accounts/src/components/personal-account-dropdown.tsx b/packages/features/accounts/src/components/personal-account-dropdown.tsx index f167b334c..29dcaf2be 100644 --- a/packages/features/accounts/src/components/personal-account-dropdown.tsx +++ b/packages/features/accounts/src/components/personal-account-dropdown.tsx @@ -7,7 +7,7 @@ import Link from 'next/link'; import type { User } from '@supabase/supabase-js'; import { - EllipsisVertical, + ChevronsUpDown, Home, LogOut, MessageCircleQuestion, @@ -85,16 +85,17 @@ export function PersonalAccountDropdown({ aria-label="Open your profile menu" data-test={'account-dropdown-trigger'} className={cn( - 'animate-in fade-in group flex cursor-pointer items-center focus:outline-none', + 'animate-in fade-in focus:outline-primary flex cursor-pointer items-center duration-500 group-data-[minimized=true]:px-0', className ?? '', { - ['active:bg-secondary/50 items-center space-x-2.5 rounded-md' + + ['active:bg-secondary/50 items-center space-x-4 rounded-md' + ' hover:bg-secondary p-2 transition-colors']: showProfileName, }, )} > @@ -102,7 +103,7 @@ export function PersonalAccountDropdown({
-
- +
( + 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/makerkit/navigation-config.schema.ts b/packages/ui/src/makerkit/navigation-config.schema.ts index 396a1c3f0..5b6786760 100644 --- a/packages/ui/src/makerkit/navigation-config.schema.ts +++ b/packages/ui/src/makerkit/navigation-config.schema.ts @@ -5,6 +5,30 @@ const RouteMatchingEnd = z .default(false) .optional(); +const Divider = z.object({ + divider: z.literal(true), +}); + +const RouteChildren = z.array( + z.object({ + label: z.string(), + path: z.string(), + Icon: z.custom(), + end: RouteMatchingEnd, + children: z + .array( + z.object({ + label: z.string(), + path: z.string(), + Icon: z.custom(), + end: RouteMatchingEnd, + }), + ) + .default([]) + .optional(), + }), +); + export const NavigationConfigSchema = z.object({ style: z.enum(['custom', 'sidebar', 'header']).default('sidebar'), sidebarCollapsed: z @@ -14,28 +38,14 @@ export const NavigationConfigSchema = z.object({ .transform((value) => value === `true`), routes: z.array( z.union([ - z.object({ - label: z.string(), - path: z.string(), - Icon: z.custom(), - end: RouteMatchingEnd, - }), z.object({ label: z.string(), collapsible: z.boolean().optional(), collapsed: z.boolean().optional(), - children: z.array( - z.object({ - label: z.string(), - path: z.string(), - Icon: z.custom(), - end: RouteMatchingEnd, - }), - ), - }), - z.object({ - divider: z.literal(true), + children: RouteChildren, + renderAction: z.custom().optional(), }), + Divider, ]), ), }); diff --git a/packages/ui/src/makerkit/page.tsx b/packages/ui/src/makerkit/page.tsx index 132ea2c99..fa2c4e680 100644 --- a/packages/ui/src/makerkit/page.tsx +++ b/packages/ui/src/makerkit/page.tsx @@ -29,12 +29,7 @@ function PageWithSidebar(props: PageProps) { const { Navigation, Children, MobileNavigation } = getSlotsFromPage(props); return ( -
+
{Navigation}
{Children} diff --git a/packages/ui/src/makerkit/profile-avatar.tsx b/packages/ui/src/makerkit/profile-avatar.tsx index b1485527c..318fd82b9 100644 --- a/packages/ui/src/makerkit/profile-avatar.tsx +++ b/packages/ui/src/makerkit/profile-avatar.tsx @@ -12,6 +12,7 @@ type TextProps = { type ProfileAvatarProps = (SessionProps | TextProps) & { className?: string; + fallbackClassName?: string; }; export function ProfileAvatar(props: ProfileAvatarProps) { @@ -23,8 +24,13 @@ export function ProfileAvatar(props: ProfileAvatarProps) { if ('text' in props) { return ( - - {props.text.slice(0, 1)} + + {props.text.slice(0, 1)} ); @@ -36,7 +42,9 @@ export function ProfileAvatar(props: ProfileAvatarProps) { - + {initials} diff --git a/packages/ui/src/makerkit/sidebar.tsx b/packages/ui/src/makerkit/sidebar.tsx index c38a6c54e..bf082f77a 100644 --- a/packages/ui/src/makerkit/sidebar.tsx +++ b/packages/ui/src/makerkit/sidebar.tsx @@ -26,6 +26,11 @@ export type SidebarConfig = z.infer; export { SidebarContext }; +/** + * @deprecated + * This component is deprecated and will be removed in a future version. + * Please use the Shadcn Sidebar component instead. + */ export function Sidebar(props: { collapsed?: boolean; expandOnHover?: boolean; @@ -338,17 +343,6 @@ export function SidebarNavigation({ ); } - - return ( - - - - ); })} ); diff --git a/packages/ui/src/shadcn/collapsible.tsx b/packages/ui/src/shadcn/collapsible.tsx new file mode 100644 index 000000000..9fa48946a --- /dev/null +++ b/packages/ui/src/shadcn/collapsible.tsx @@ -0,0 +1,11 @@ +"use client" + +import * as CollapsiblePrimitive from "@radix-ui/react-collapsible" + +const Collapsible = CollapsiblePrimitive.Root + +const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger + +const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent + +export { Collapsible, CollapsibleTrigger, CollapsibleContent } diff --git a/packages/ui/src/shadcn/sheet.tsx b/packages/ui/src/shadcn/sheet.tsx index 9cde8b7bb..816149d69 100644 --- a/packages/ui/src/shadcn/sheet.tsx +++ b/packages/ui/src/shadcn/sheet.tsx @@ -3,9 +3,8 @@ import * as React from 'react'; import * as SheetPrimitive from '@radix-ui/react-dialog'; -import { cva } from 'class-variance-authority'; -import type { VariantProps } from 'class-variance-authority'; -import { X } from 'lucide-react'; +import { Cross2Icon } from '@radix-ui/react-icons'; +import { type VariantProps, cva } from 'class-variance-authority'; import { cn } from '../lib/utils'; @@ -33,7 +32,7 @@ const SheetOverlay = React.forwardRef< SheetOverlay.displayName = SheetPrimitive.Overlay.displayName; const sheetVariants = cva( - 'fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500', + 'fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500 data-[state=open]:animate-in data-[state=closed]:animate-out', { variants: { side: { @@ -42,7 +41,7 @@ const sheetVariants = cva( 'inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom', left: 'inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm', right: - 'inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm', + 'inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm', }, }, defaultVariants: { @@ -66,11 +65,11 @@ const SheetContent = React.forwardRef< className={cn(sheetVariants({ side }), className)} {...props} > - {children} - + Close + {children} )); diff --git a/packages/ui/src/shadcn/sidebar.tsx b/packages/ui/src/shadcn/sidebar.tsx new file mode 100644 index 000000000..f3f9c9be1 --- /dev/null +++ b/packages/ui/src/shadcn/sidebar.tsx @@ -0,0 +1,1033 @@ +'use client'; + +import * as React from 'react'; +import { Fragment } from 'react'; + +import Link from 'next/link'; +import { usePathname } from 'next/navigation'; + +import { Slot } from '@radix-ui/react-slot'; +import { VariantProps, cva } from 'class-variance-authority'; +import { ChevronDown, PanelLeft } from 'lucide-react'; +import { useTranslation } from 'react-i18next'; + +import { useIsMobile } from '../hooks/use-mobile'; +import { cn, isRouteActive } from '../lib/utils'; +import { If } from '../makerkit/if'; +import type { SidebarConfig } from '../makerkit/sidebar'; +import { Trans } from '../makerkit/trans'; +import { Button } from './button'; +import { CollapsibleTrigger } from './collapsible'; +import { Input } from './input'; +import { Separator } from './separator'; +import { Sheet, SheetContent } from './sheet'; +import { Skeleton } from './skeleton'; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from './tooltip'; + +const SIDEBAR_COOKIE_NAME = 'sidebar:state'; +const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7; +const SIDEBAR_WIDTH = '16rem'; +const SIDEBAR_WIDTH_MOBILE = '18rem'; +const SIDEBAR_WIDTH_ICON = '3rem'; +const SIDEBAR_KEYBOARD_SHORTCUT = 'b'; +const SIDEBAR_MINIMIZED_WIDTH = '4rem'; + +type SidebarContext = { + state: 'expanded' | 'collapsed'; + open: boolean; + setOpen: (open: boolean) => void; + openMobile: boolean; + setOpenMobile: (open: boolean) => void; + setMinimized: (minimized: boolean) => void; + isMobile: boolean; + toggleSidebar: () => void; + minimized: boolean; + startMinimized: boolean; + expandOnHover: boolean; +}; + +export 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; + minimized?: boolean; + expandOnHover?: boolean; + } +>( + ( + { + defaultOpen = true, + minimized: isMinimized = false, + open: openProp, + onOpenChange: setOpenProp, + className, + style, + children, + ...props + }, + ref, + ) => { + const isMobile = useIsMobile(); + const [openMobile, setOpenMobile] = React.useState(false); + const [minimized, setMinimized] = React.useState(isMinimized); + + const expandOnHover = + props.expandOnHover ?? + process.env.NEXT_PUBLIC_EXPAND_SIDEBAR_ON_HOVER === 'true'; + + // 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)) => { + if (setOpenProp) { + return setOpenProp?.( + typeof value === 'function' ? value(open) : value, + ); + } + + _setOpen(value); + + // This sets the cookie to keep the sidebar state. + document.cookie = `${SIDEBAR_COOKIE_NAME}=${open}; 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 startMinimized = isMinimized; + + const contextValue = React.useMemo( + () => ({ + state, + open, + setOpen, + isMobile, + minimized, + setMinimized, + expandOnHover, + openMobile, + setOpenMobile, + toggleSidebar, + startMinimized, + }), + [ + state, + open, + setOpen, + isMobile, + openMobile, + setOpenMobile, + toggleSidebar, + expandOnHover, + minimized, + setMinimized, + startMinimized, + ], + ); + + 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, + minimized, + setMinimized, + expandOnHover, + startMinimized, + } = useSidebar(); + useSidebar(); + + const isExpandedRef = React.useRef(false); + + const onMouseEnter = + startMinimized && expandOnHover + ? () => { + setMinimized(false); + isExpandedRef.current = true; + } + : undefined; + + const onMouseLeave = + startMinimized && expandOnHover + ? () => { + if (!isRadixPopupOpen()) { + setMinimized(true); + isExpandedRef.current = false; + } else { + onRadixPopupClose(() => { + setMinimized(true); + isExpandedRef.current = false; + }); + } + } + : undefined; + + 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 ( +