From b2c27eb25b7b8edce0229ca3da341930287953dd Mon Sep 17 00:00:00 2001 From: Giancarlo Buomprisco Date: Mon, 14 Oct 2024 11:31:18 +0200 Subject: [PATCH] Sidebar: make it possible to set the sidebar as collapsed (#72) * Sidebar: make it possible to set the sidebar as collapsed --- .../site-header-account-section.tsx | 1 + .../app/admin/_components/admin-sidebar.tsx | 4 +- .../_components/home-account-selector.tsx | 7 +- .../_components/home-menu-navigation.tsx | 2 - .../_components/home-mobile-navigation.tsx | 1 - .../home/(user)/_components/home-sidebar.tsx | 20 +- apps/web/app/home/(user)/layout.tsx | 3 +- .../team-account-accounts-selector.tsx | 4 +- .../team-account-layout-sidebar.tsx | 44 +++-- .../team-account-navigation-menu.tsx | 1 - apps/web/app/home/[account]/layout.tsx | 5 +- .../personal-account-dropdown-container.tsx | 20 +- .../personal-account-navigation.config.tsx | 1 + .../config/team-account-navigation.config.tsx | 1 + .../src/components/account-selector.tsx | 8 +- .../components/personal-account-dropdown.tsx | 5 +- .../src/makerkit/navigation-config.schema.ts | 5 + packages/ui/src/makerkit/page.tsx | 6 +- packages/ui/src/makerkit/sidebar.tsx | 183 +++++++++++++----- 19 files changed, 221 insertions(+), 100 deletions(-) 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 25d95b781..0e4690c99 100644 --- a/apps/web/app/(marketing)/_components/site-header-account-section.tsx +++ b/apps/web/app/(marketing)/_components/site-header-account-section.tsx @@ -52,6 +52,7 @@ function SuspendedPersonalAccountDropdown(props: { user: User | null }) { if (userData) { return ( - + }> Home @@ -35,7 +35,7 @@ export function AdminSidebar(props: { user: User }) { - + ); 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 de30212c0..beb5d1a81 100644 --- a/apps/web/app/home/(user)/_components/home-account-selector.tsx +++ b/apps/web/app/home/(user)/_components/home-account-selector.tsx @@ -1,8 +1,11 @@ 'use client'; +import { useContext } from 'react'; + import { useRouter } from 'next/navigation'; import { AccountSelector } from '@kit/accounts/account-selector'; +import { SidebarContext } from '@kit/ui/sidebar'; import featureFlagsConfig from '~/config/feature-flags.config'; import pathsConfig from '~/config/paths.config'; @@ -19,13 +22,13 @@ export function HomeAccountSelector(props: { }>; userId: string; - collapsed: boolean; }) { const router = useRouter(); + const { collapsed } = useContext(SidebarContext); return ( diff --git a/apps/web/app/home/(user)/_components/home-mobile-navigation.tsx b/apps/web/app/home/(user)/_components/home-mobile-navigation.tsx index 7a01058d5..1dca69706 100644 --- a/apps/web/app/home/(user)/_components/home-mobile-navigation.tsx +++ b/apps/web/app/home/(user)/_components/home-mobile-navigation.tsx @@ -71,7 +71,6 @@ export function HomeMobileNavigation(props: { workspace: UserWorkspace }) { diff --git a/apps/web/app/home/(user)/_components/home-sidebar.tsx b/apps/web/app/home/(user)/_components/home-sidebar.tsx index e07ab8f51..17711b7c9 100644 --- a/apps/web/app/home/(user)/_components/home-sidebar.tsx +++ b/apps/web/app/home/(user)/_components/home-sidebar.tsx @@ -11,25 +11,28 @@ import { UserNotifications } from '~/home/(user)/_components/user-notifications' import type { UserWorkspace } from '../_lib/server/load-user-workspace'; import { HomeAccountSelector } from './home-account-selector'; -export function HomeSidebar(props: { workspace: UserWorkspace }) { +interface HomeSidebarProps { + workspace: UserWorkspace; +} + +export function HomeSidebar(props: HomeSidebarProps) { const { workspace, user, accounts } = props.workspace; + const collapsed = personalAccountNavigationConfig.sidebarCollapsed; return ( - +
} > - + - +
+ +
@@ -40,7 +43,6 @@ export function HomeSidebar(props: { workspace: UserWorkspace }) {
diff --git a/apps/web/app/home/(user)/layout.tsx b/apps/web/app/home/(user)/layout.tsx index 0e6684940..118ae1258 100644 --- a/apps/web/app/home/(user)/layout.tsx +++ b/apps/web/app/home/(user)/layout.tsx @@ -39,6 +39,7 @@ function UserHomeLayout({ children }: React.PropsWithChildren) { + @@ -56,4 +57,4 @@ function getLayoutStyle() { (cookies().get('layout-style')?.value as PageLayoutStyle) ?? personalAccountNavigationConfig.style ); -} +} \ No newline at end of file diff --git a/apps/web/app/home/[account]/_components/team-account-accounts-selector.tsx b/apps/web/app/home/[account]/_components/team-account-accounts-selector.tsx index 72a64264e..23463586f 100644 --- a/apps/web/app/home/[account]/_components/team-account-accounts-selector.tsx +++ b/apps/web/app/home/[account]/_components/team-account-accounts-selector.tsx @@ -20,6 +20,8 @@ export function TeamAccountAccountsSelector(params: { value: string | null; image: string | null; }>; + + collapsed?: boolean; }) { const router = useRouter(); @@ -28,7 +30,7 @@ export function TeamAccountAccountsSelector(params: { selectedAccount={params.selectedAccount} accounts={params.accounts} userId={params.userId} - collapsed={false} + collapsed={params.collapsed} features={features} onAccountChange={(value) => { const path = value 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 b254b4300..49742abb8 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,8 +1,14 @@ -import { User } from '@supabase/supabase-js'; +'use client'; -import { Sidebar, SidebarContent } from '@kit/ui/sidebar'; +import { useContext } from 'react'; + +import type { User } from '@supabase/supabase-js'; + +import { Sidebar, SidebarContent, SidebarContext } from '@kit/ui/sidebar'; +import { cn } from '@kit/ui/utils'; import { ProfileAccountDropdownContainer } from '~/components//personal-account-dropdown-container'; +import { getTeamAccountSidebarConfig } from '~/config/team-account-navigation.config'; import { TeamAccountNotifications } from '~/home/[account]/_components/team-account-notifications'; import { TeamAccountAccountsSelector } from '../_components/team-account-accounts-selector'; @@ -18,11 +24,12 @@ export function TeamAccountLayoutSidebar(props: { account: string; accountId: string; accounts: AccountModel[]; - collapsed: boolean; user: User; }) { + const collapsed = getTeamAccountSidebarConfig(props.account).sidebarCollapsed; + return ( - + -
+
- +
+ +
@@ -70,7 +89,6 @@ function SidebarContainer(props: {
diff --git a/apps/web/app/home/[account]/_components/team-account-navigation-menu.tsx b/apps/web/app/home/[account]/_components/team-account-navigation-menu.tsx index a1926296e..764c33ef5 100644 --- a/apps/web/app/home/[account]/_components/team-account-navigation-menu.tsx +++ b/apps/web/app/home/[account]/_components/team-account-navigation-menu.tsx @@ -62,7 +62,6 @@ export function TeamAccountNavigationMenu(props: { diff --git a/apps/web/app/home/[account]/layout.tsx b/apps/web/app/home/[account]/layout.tsx index 8bf2ee4a5..c569f7704 100644 --- a/apps/web/app/home/[account]/layout.tsx +++ b/apps/web/app/home/[account]/layout.tsx @@ -21,7 +21,7 @@ import { TeamAccountLayoutSidebar } from './_components/team-account-layout-side import { TeamAccountNavigationMenu } from './_components/team-account-navigation-menu'; import { loadTeamWorkspace } from './_lib/server/team-account-workspace.loader'; -interface Params { +interface TeamWorkspaceLayoutParams { account: string; } @@ -29,7 +29,7 @@ function TeamWorkspaceLayout({ children, params, }: React.PropsWithChildren<{ - params: Params; + params: TeamWorkspaceLayoutParams; }>) { const data = use(loadTeamWorkspace(params.account)); const style = getLayoutStyle(params.account); @@ -45,7 +45,6 @@ function TeamWorkspaceLayout({ - signOut.mutateAsync()} - /> -
+ signOut.mutateAsync()} + /> ); } diff --git a/apps/web/config/personal-account-navigation.config.tsx b/apps/web/config/personal-account-navigation.config.tsx index 05da6f358..d98702ab6 100644 --- a/apps/web/config/personal-account-navigation.config.tsx +++ b/apps/web/config/personal-account-navigation.config.tsx @@ -32,4 +32,5 @@ if (featureFlagsConfig.enablePersonalAccountBilling) { export const personalAccountNavigationConfig = NavigationConfigSchema.parse({ routes, style: process.env.NEXT_PUBLIC_USER_NAVIGATION_STYLE, + sidebarCollapsed: process.env.NEXT_PUBLIC_HOME_SIDEBAR_COLLAPSED, }); diff --git a/apps/web/config/team-account-navigation.config.tsx b/apps/web/config/team-account-navigation.config.tsx index 214b0fff4..e95522fd9 100644 --- a/apps/web/config/team-account-navigation.config.tsx +++ b/apps/web/config/team-account-navigation.config.tsx @@ -43,6 +43,7 @@ export function getTeamAccountSidebarConfig(account: string) { return NavigationConfigSchema.parse({ routes: getRoutes(account), style: process.env.NEXT_PUBLIC_TEAM_NAVIGATION_STYLE, + sidebarCollapsed: process.env.NEXT_PUBLIC_TEAM_SIDEBAR_COLLAPSED, }); } diff --git a/packages/features/accounts/src/components/account-selector.tsx b/packages/features/accounts/src/components/account-selector.tsx index fc7eda3a5..9638a06d2 100644 --- a/packages/features/accounts/src/components/account-selector.tsx +++ b/packages/features/accounts/src/components/account-selector.tsx @@ -101,7 +101,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, - 'justify-center': collapsed, + 'm-auto justify-center px-4 lg:w-full': collapsed, }, className, )} @@ -143,7 +143,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 33b5120ab..f167b334c 100644 --- a/packages/features/accounts/src/components/personal-account-dropdown.tsx +++ b/packages/features/accounts/src/components/personal-account-dropdown.tsx @@ -33,7 +33,7 @@ export function PersonalAccountDropdown({ className, user, signOutRequested, - showProfileName, + showProfileName = true, paths, features, account, @@ -56,8 +56,9 @@ export function PersonalAccountDropdown({ enableThemeToggle: boolean; }; - className?: string; showProfileName?: boolean; + + className?: string; }) { const { data: personalAccountData } = usePersonalAccountData( user.id, diff --git a/packages/ui/src/makerkit/navigation-config.schema.ts b/packages/ui/src/makerkit/navigation-config.schema.ts index ba913ba9c..396a1c3f0 100644 --- a/packages/ui/src/makerkit/navigation-config.schema.ts +++ b/packages/ui/src/makerkit/navigation-config.schema.ts @@ -7,6 +7,11 @@ const RouteMatchingEnd = z export const NavigationConfigSchema = z.object({ style: z.enum(['custom', 'sidebar', 'header']).default('sidebar'), + sidebarCollapsed: z + .enum(['false', 'true']) + .default('false') + .optional() + .transform((value) => value === `true`), routes: z.array( z.union([ z.object({ diff --git a/packages/ui/src/makerkit/page.tsx b/packages/ui/src/makerkit/page.tsx index b07c55dbb..e9f7f3cc0 100644 --- a/packages/ui/src/makerkit/page.tsx +++ b/packages/ui/src/makerkit/page.tsx @@ -30,14 +30,14 @@ function PageWithSidebar(props: PageProps) { return (
{Navigation}
{MobileNavigation} @@ -115,7 +115,7 @@ export function PageBody( } export function PageNavigation(props: React.PropsWithChildren) { - return
{props.children}
; + return
{props.children}
; } export function PageDescription(props: React.PropsWithChildren) { diff --git a/packages/ui/src/makerkit/sidebar.tsx b/packages/ui/src/makerkit/sidebar.tsx index e8c76a71f..61ac5f1e7 100644 --- a/packages/ui/src/makerkit/sidebar.tsx +++ b/packages/ui/src/makerkit/sidebar.tsx @@ -1,6 +1,6 @@ 'use client'; -import { useContext, useId, useState } from 'react'; +import { useContext, useId, useRef, useState } from 'react'; import Link from 'next/link'; import { usePathname } from 'next/navigation'; @@ -24,8 +24,11 @@ import { Trans } from './trans'; export type SidebarConfig = z.infer; +export { SidebarContext }; + export function Sidebar(props: { collapsed?: boolean; + expandOnHover?: boolean; className?: string; children: | React.ReactNode @@ -35,19 +38,62 @@ export function Sidebar(props: { }) => React.ReactNode); }) { const [collapsed, setCollapsed] = useState(props.collapsed ?? false); + const isExpandedRef = useRef(false); - const className = getClassNameBuilder(props.className ?? '')({ + const expandOnHover = + props.expandOnHover ?? + process.env.NEXT_PUBLIC_EXPAND_SIDEBAR_ON_HOVER === 'true'; + + const sidebarSizeClassName = getSidebarSizeClassName( collapsed, + isExpandedRef.current, + ); + + const className = getClassNameBuilder( + cn(props.className ?? '', sidebarSizeClassName, {}), + )(); + + const containerClassName = cn(sidebarSizeClassName, 'bg-inherit', { + 'max-w-[4rem]': expandOnHover && isExpandedRef.current, }); const ctx = { collapsed, setCollapsed }; + const onMouseEnter = + props.collapsed && expandOnHover + ? () => { + setCollapsed(false); + isExpandedRef.current = true; + } + : undefined; + + const onMouseLeave = + props.collapsed && expandOnHover + ? () => { + if (!isRadixPopupOpen()) { + setCollapsed(true); + isExpandedRef.current = false; + } else { + onRadixPopupClose(() => { + setCollapsed(true); + isExpandedRef.current = false; + }); + } + } + : undefined; + return ( -
- {typeof props.children === 'function' - ? props.children(ctx) - : props.children} +
+
+ {typeof props.children === 'function' + ? props.children(ctx) + : props.children} +
); @@ -55,17 +101,22 @@ export function Sidebar(props: { export function SidebarContent({ children, - className, + className: customClassName, }: React.PropsWithChildren<{ className?: string; }>) { - return ( -
- {children} -
+ const { collapsed } = useContext(SidebarContext); + + const className = cn( + 'flex w-full flex-col space-y-1.5 py-1', + customClassName, + { + 'px-4': !collapsed, + 'px-2': collapsed, + }, ); + + return
{children}
; } export function SidebarGroup({ @@ -96,7 +147,7 @@ export function SidebarGroup({ const Wrapper = () => { const className = cn( - 'group flex items-center justify-between px-container space-x-2.5', + 'px-container group flex items-center justify-between space-x-2.5', { 'py-2.5': !sidebarCollapsed, }, @@ -131,7 +182,11 @@ export function SidebarGroup({ }; return ( -
+
@@ -164,48 +219,84 @@ export function SidebarItem({ const active = isRouteActive(path, currentPath, end ?? false); const variant = active ? 'secondary' : 'ghost'; - const size = collapsed ? 'icon' : 'sm'; return ( - + - - {children} - - - + + + {children} + - - {children} - - + + ); } function getClassNameBuilder(className: string) { - return cva([cn('flex box-content h-screen flex-col relative', className)], { - variants: { - collapsed: { - true: `w-[6rem]`, - false: `w-2/12 lg:w-[17rem]`, - }, - }, + return cva([ + cn( + 'group/sidebar transition-width fixed box-content flex h-screen w-2/12 flex-col bg-inherit backdrop-blur-sm duration-100', + className, + ), + ]); +} + +function getSidebarSizeClassName(collapsed: boolean, isExpanded: boolean) { + return cn(['z-10 flex w-full flex-col'], { + 'dark:shadow-primary/20 lg:w-[17rem]': !collapsed, + 'lg:w-[4rem]': collapsed, + shadow: isExpanded, }); } +function getRadixPopup() { + return document.querySelector('[data-radix-popper-content-wrapper]'); +} + +function isRadixPopupOpen() { + return getRadixPopup() !== null; +} + +function onRadixPopupClose(callback: () => void) { + const element = getRadixPopup(); + + if (element) { + const observer = new MutationObserver(() => { + if (!getRadixPopup()) { + callback(); + + observer.disconnect(); + } + }); + + observer.observe(element.parentElement!, { + childList: true, + subtree: true, + }); + } +} + export function SidebarNavigation({ config, }: React.PropsWithChildren<{