Răsfoiți Sursa

fix: update layouts and solve overflow issues (#11)

Changes made
- update main and sidebar layouts
- solve overflow issues in mobile sidebar menu
- extract repetitive codes into a reusable component
- hide unnecessary overflow
Sat Naing 2 ani în urmă
părinte
comite
6bee397721

+ 85 - 39
src/components/custom/layout.tsx

@@ -1,60 +1,106 @@
 import * as React from 'react'
 import { cn } from '@/lib/utils'
 
+const LayoutContext = React.createContext<{
+  offset: number
+  fixed: boolean
+} | null>(null)
+
 interface LayoutProps extends React.HTMLAttributes<HTMLDivElement> {
-  fadedBelow?: boolean
-  fixedHeight?: boolean
+  fixed?: boolean
 }
 
-const Layout = React.forwardRef<HTMLDivElement, LayoutProps>(
-  ({ className, fadedBelow = false, fixedHeight = false, ...props }, ref) => (
-    <div
-      ref={ref}
-      className={cn(
-        'relative flex h-full w-full flex-col',
-        fadedBelow &&
-          'after:pointer-events-none after:absolute after:bottom-0 after:left-0 after:hidden after:h-32 after:w-full after:bg-[linear-gradient(180deg,_transparent_10%,_hsl(var(--background))_70%)] after:md:block',
-        fixedHeight && 'md:h-svh',
-        className
-      )}
-      {...props}
-    />
+const Layout = ({ className, fixed = false, ...props }: LayoutProps) => {
+  const divRef = React.useRef<HTMLDivElement>(null)
+  const [offset, setOffset] = React.useState(0)
+
+  React.useEffect(() => {
+    const div = divRef.current
+
+    if (!div) return
+    const onScroll = () => setOffset(div.scrollTop)
+
+    // clean up code
+    div.removeEventListener('scroll', onScroll)
+    div.addEventListener('scroll', onScroll, { passive: true })
+    return () => div.removeEventListener('scroll', onScroll)
+  }, [])
+
+  return (
+    <LayoutContext.Provider value={{ offset, fixed }}>
+      <div
+        ref={divRef}
+        data-layout='layout'
+        className={cn(
+          'h-full overflow-auto',
+          fixed && 'flex flex-col',
+          className
+        )}
+        {...props}
+      />
+    </LayoutContext.Provider>
   )
-)
+}
 Layout.displayName = 'Layout'
 
-const LayoutHeader = React.forwardRef<
+interface HeaderProps extends React.HTMLAttributes<HTMLDivElement> {
+  sticky?: boolean
+}
+
+const Header = React.forwardRef<HTMLDivElement, HeaderProps>(
+  ({ className, sticky, ...props }, ref) => {
+    // Check if Layout.Header is used within Layout
+    const contextVal = React.useContext(LayoutContext)
+    if (contextVal === null) {
+      throw new Error(
+        `Layout.Header must be used within ${Layout.displayName}.`
+      )
+    }
+
+    return (
+      <div
+        ref={ref}
+        data-layout='header'
+        className={cn(
+          `z-10 flex h-[var(--header-height)] items-center gap-4 bg-background p-4 md:px-8`,
+          contextVal.offset > 10 && sticky ? 'shadow' : 'shadow-none',
+          contextVal.fixed && 'flex-none',
+          sticky && 'sticky top-0',
+          className
+        )}
+        {...props}
+      />
+    )
+  }
+)
+Header.displayName = 'Header'
+
+const Body = React.forwardRef<
   HTMLDivElement,
   React.HTMLAttributes<HTMLDivElement>
->(({ className, ...props }, ref) => (
-  <div
-    ref={ref}
-    className={cn(
-      'flex h-[var(--header-height)] flex-none items-center gap-4 bg-background p-4 md:px-8',
-      className
-    )}
-    {...props}
-  />
-))
-LayoutHeader.displayName = 'LayoutHeader'
-
-interface LayoutBodyProps extends React.HTMLAttributes<HTMLDivElement> {
-  fixedHeight?: boolean
-}
+>(({ className, ...props }, ref) => {
+  // Check if Layout.Body is used within Layout
+  const contextVal = React.useContext(LayoutContext)
+  if (contextVal === null) {
+    throw new Error(`Layout.Body must be used within ${Layout.displayName}.`)
+  }
 
-const LayoutBody = React.forwardRef<HTMLDivElement, LayoutBodyProps>(
-  ({ className, fixedHeight, ...props }, ref) => (
+  return (
     <div
       ref={ref}
+      data-layout='body'
       className={cn(
-        'flex-1 overflow-hidden px-4 py-6 md:px-8',
-        fixedHeight && 'h-[calc(100%-var(--header-height))]',
+        'px-4 py-6 md:overflow-hidden md:px-8',
+        contextVal && contextVal.fixed && 'flex-1',
         className
       )}
       {...props}
     />
   )
-)
-LayoutBody.displayName = 'LayoutBody'
+})
+Body.displayName = 'Body'
+
+Layout.Header = Header
+Layout.Body = Body
 
-export { Layout, LayoutHeader, LayoutBody }
+export { Layout }

+ 10 - 7
src/components/sidebar.tsx

@@ -1,6 +1,6 @@
 import { useEffect, useState } from 'react'
 import { IconChevronsLeft, IconMenu2, IconX } from '@tabler/icons-react'
-import { Layout, LayoutHeader } from './custom/layout'
+import { Layout } from './custom/layout'
 import { Button } from './custom/button'
 import Nav from './nav'
 import { cn } from '@/lib/utils'
@@ -11,7 +11,7 @@ interface SidebarProps extends React.HTMLAttributes<HTMLElement> {
   setIsCollapsed: React.Dispatch<React.SetStateAction<boolean>>
 }
 
-export default function Sidebar2({
+export default function Sidebar({
   className,
   isCollapsed,
   setIsCollapsed,
@@ -40,9 +40,12 @@ export default function Sidebar2({
         className={`absolute inset-0 transition-[opacity] delay-100 duration-700 ${navOpened ? 'h-svh opacity-50' : 'h-0 opacity-0'} w-full bg-black md:hidden`}
       />
 
-      <Layout>
+      <Layout fixed className={navOpened ? 'h-svh' : ''}>
         {/* Header */}
-        <LayoutHeader className='sticky top-0 justify-between px-4 py-3 shadow md:px-4'>
+        <Layout.Header
+          sticky
+          className='z-50 flex justify-between px-4 py-3 shadow-sm md:px-4'
+        >
           <div className={`flex items-center ${!isCollapsed ? 'gap-2' : ''}`}>
             <svg
               xmlns='http://www.w3.org/2000/svg'
@@ -94,12 +97,12 @@ export default function Sidebar2({
           >
             {navOpened ? <IconX /> : <IconMenu2 />}
           </Button>
-        </LayoutHeader>
+        </Layout.Header>
 
         {/* Navigation links */}
         <Nav
           id='sidebar-menu'
-          className={`h-full flex-1 overflow-auto ${navOpened ? 'max-h-screen' : 'max-h-0 py-0 md:max-h-screen md:py-2'}`}
+          className={`z-40 h-full flex-1 overflow-auto ${navOpened ? 'max-h-screen' : 'max-h-0 py-0 md:max-h-screen md:py-2'}`}
           closeNav={() => setNavOpened(false)}
           isCollapsed={isCollapsed}
           links={sidelinks}
@@ -110,7 +113,7 @@ export default function Sidebar2({
           onClick={() => setIsCollapsed((prev) => !prev)}
           size='icon'
           variant='outline'
-          className='absolute -right-5 top-1/2 hidden rounded-full md:inline-flex'
+          className='absolute -right-5 top-1/2 z-50 hidden rounded-full md:inline-flex'
         >
           <IconChevronsLeft
             stroke={1.5}

+ 7 - 0
src/index.css

@@ -104,6 +104,9 @@
   body {
     @apply min-h-svh w-full bg-background text-foreground;
   }
+  body > #root {
+    @apply h-svh;
+  }
 }
 
 @layer utilities {
@@ -116,4 +119,8 @@
     -ms-overflow-style: none; /* IE and Edge */
     scrollbar-width: none; /* Firefox */
   }
+
+  .faded-bottom {
+    @apply after:pointer-events-none after:absolute after:bottom-0 after:left-0 after:hidden after:h-32 after:w-full after:bg-[linear-gradient(180deg,_transparent_10%,_hsl(var(--background))_70%)] after:md:block;
+  }
 }

+ 7 - 7
src/pages/apps/index.tsx

@@ -4,7 +4,7 @@ import {
   IconSortAscendingLetters,
   IconSortDescendingLetters,
 } from '@tabler/icons-react'
-import { Layout, LayoutBody, LayoutHeader } from '@/components/custom/layout'
+import { Layout } from '@/components/custom/layout'
 import { Input } from '@/components/ui/input'
 import {
   Select,
@@ -47,9 +47,9 @@ export default function Apps() {
     .filter((app) => app.name.toLowerCase().includes(searchTerm.toLowerCase()))
 
   return (
-    <Layout fadedBelow fixedHeight>
+    <Layout fixed>
       {/* ===== Top Heading ===== */}
-      <LayoutHeader>
+      <Layout.Header>
         <div className='flex w-full items-center justify-between'>
           <Search />
           <div className='flex items-center space-x-4'>
@@ -57,10 +57,10 @@ export default function Apps() {
             <UserNav />
           </div>
         </div>
-      </LayoutHeader>
+      </Layout.Header>
 
       {/* ===== Content ===== */}
-      <LayoutBody className='flex flex-col' fixedHeight>
+      <Layout.Body className='flex flex-col'>
         <div>
           <h1 className='text-2xl font-bold tracking-tight'>
             App Integrations
@@ -112,7 +112,7 @@ export default function Apps() {
           </Select>
         </div>
         <Separator className='shadow' />
-        <ul className='no-scrollbar grid gap-4 overflow-y-scroll pb-16 pt-4 md:grid-cols-2 lg:grid-cols-3'>
+        <ul className='faded-bottom no-scrollbar grid gap-4 overflow-auto pb-16 pt-4 md:grid-cols-2 lg:grid-cols-3'>
           {filteredApps.map((app) => (
             <li
               key={app.name}
@@ -139,7 +139,7 @@ export default function Apps() {
             </li>
           ))}
         </ul>
-      </LayoutBody>
+      </Layout.Body>
     </Layout>
   )
 }

+ 8 - 10
src/pages/dashboard/index.tsx

@@ -1,3 +1,4 @@
+import { Layout } from '@/components/custom/layout'
 import { Button } from '@/components/custom/button'
 import {
   Card,
@@ -11,7 +12,6 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
 import ThemeSwitch from '@/components/theme-switch'
 import { TopNav } from '@/components/top-nav'
 import { UserNav } from '@/components/user-nav'
-import { Layout, LayoutBody, LayoutHeader } from '@/components/custom/layout'
 import { RecentSales } from './components/recent-sales'
 import { Overview } from './components/overview'
 
@@ -19,21 +19,19 @@ export default function Dashboard() {
   return (
     <Layout>
       {/* ===== Top Heading ===== */}
-      <LayoutHeader>
+      <Layout.Header>
         <TopNav links={topNav} />
         <div className='ml-auto flex items-center space-x-4'>
           <Search />
           <ThemeSwitch />
           <UserNav />
         </div>
-      </LayoutHeader>
+      </Layout.Header>
 
       {/* ===== Main ===== */}
-      <LayoutBody className='space-y-4'>
-        <div className='flex items-center justify-between space-y-2'>
-          <h1 className='text-2xl font-bold tracking-tight md:text-3xl'>
-            Dashboard
-          </h1>
+      <Layout.Body>
+        <div className='mb-2 flex items-center justify-between space-y-2'>
+          <h1 className='text-2xl font-bold tracking-tight'>Dashboard</h1>
           <div className='flex items-center space-x-2'>
             <Button>Download</Button>
           </div>
@@ -43,7 +41,7 @@ export default function Dashboard() {
           defaultValue='overview'
           className='space-y-4'
         >
-          <div className='w-full overflow-x-scroll pb-2'>
+          <div className='w-full overflow-x-auto pb-2'>
             <TabsList>
               <TabsTrigger value='overview'>Overview</TabsTrigger>
               <TabsTrigger value='analytics'>Analytics</TabsTrigger>
@@ -178,7 +176,7 @@ export default function Dashboard() {
             </div>
           </TabsContent>
         </Tabs>
-      </LayoutBody>
+      </Layout.Body>
     </Layout>
   )
 }

+ 6 - 5
src/pages/extra-components/index.tsx

@@ -3,7 +3,7 @@ import { Link } from 'react-router-dom'
 import { IconChevronRight } from '@tabler/icons-react'
 import { nord } from 'react-syntax-highlighter/dist/esm/styles/prism'
 import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'
-import { Layout, LayoutBody, LayoutHeader } from '@/components/custom/layout'
+import { Layout } from '@/components/custom/layout'
 import { Breadcrumb, BreadcrumbItem } from '@/components/custom/breadcrumb'
 import { PinInput, PinInputField } from '@/components/custom/pin-input'
 import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
@@ -37,15 +37,16 @@ export default function ExtraComponents() {
   return (
     <Layout>
       {/* ===== Top Heading ===== */}
-      <LayoutHeader>
+      <Layout.Header>
         <div className='ml-auto flex items-center space-x-4'>
           <Search />
           <ThemeSwitch />
           <UserNav />
         </div>
-      </LayoutHeader>
+      </Layout.Header>
 
-      <LayoutBody className='space-y-4'>
+      {/* className='space-y-4' */}
+      <Layout.Body className='space-y-4'>
         <div className='flex items-center justify-between space-y-2'>
           <h1 className='text-2xl font-bold tracking-tight md:text-3xl'>
             Extra Components
@@ -160,7 +161,7 @@ export default function ExtraComponents() {
             </Tabs>
           </div>
         </div>
-      </LayoutBody>
+      </Layout.Body>
     </Layout>
   )
 }

+ 7 - 11
src/pages/settings/account/index.tsx

@@ -1,18 +1,14 @@
-import { Separator } from '@/components/ui/separator'
 import { AccountForm } from './account-form'
+import ContentSection from '../components/content-section'
 
 export default function SettingsAccount() {
   return (
-    <div className='space-y-6'>
-      <div>
-        <h3 className='text-lg font-medium'>Account</h3>
-        <p className='text-sm text-muted-foreground'>
-          Update your account settings. Set your preferred language and
-          timezone.
-        </p>
-      </div>
-      <Separator />
+    <ContentSection
+      title='Account'
+      desc='Update your account settings. Set your preferred language and
+          timezone.'
+    >
       <AccountForm />
-    </div>
+    </ContentSection>
   )
 }

+ 7 - 11
src/pages/settings/appearance/index.tsx

@@ -1,18 +1,14 @@
 import { AppearanceForm } from './appearance-form'
-import { Separator } from '@/components/ui/separator'
+import ContentSection from '../components/content-section'
 
 export default function SettingsAppearance() {
   return (
-    <div className='space-y-6'>
-      <div>
-        <h3 className='text-lg font-medium'>Appearance</h3>
-        <p className='text-sm text-muted-foreground'>
-          Customize the appearance of the app. Automatically switch between day
-          and night themes.
-        </p>
-      </div>
-      <Separator />
+    <ContentSection
+      title='Appearance'
+      desc='Customize the appearance of the app. Automatically switch between day
+          and night themes.'
+    >
       <AppearanceForm />
-    </div>
+    </ContentSection>
   )
 }

+ 26 - 0
src/pages/settings/components/content-section.tsx

@@ -0,0 +1,26 @@
+import { Separator } from '@/components/ui/separator'
+
+interface ContentSectionProps {
+  title: string
+  desc: string
+  children: JSX.Element
+}
+
+export default function ContentSection({
+  title,
+  desc,
+  children,
+}: ContentSectionProps) {
+  return (
+    <div className='flex flex-1 flex-col'>
+      <div className='flex-none'>
+        <h3 className='text-lg font-medium'>{title}</h3>
+        <p className='text-sm text-muted-foreground'>{desc}</p>
+      </div>
+      <Separator className='my-4 flex-none' />
+      <div className='faded-bottom -mx-4 flex-1 overflow-auto scroll-smooth px-4 md:pb-16'>
+        <div className='lg:max-w-xl'>{children}</div>
+      </div>
+    </div>
+  )
+}

+ 6 - 10
src/pages/settings/display/index.tsx

@@ -1,17 +1,13 @@
-import { Separator } from '@/components/ui/separator'
 import { DisplayForm } from './display-form'
+import ContentSection from '../components/content-section'
 
 export default function SettingsDisplay() {
   return (
-    <div className='space-y-6'>
-      <div>
-        <h3 className='text-lg font-medium'>Display</h3>
-        <p className='text-sm text-muted-foreground'>
-          Turn items on or off to control what&apos;s displayed in the app.
-        </p>
-      </div>
-      <Separator />
+    <ContentSection
+      title='Display'
+      desc="Turn items on or off to control what's displayed in the app."
+    >
       <DisplayForm />
-    </div>
+    </ContentSection>
   )
 }

+ 11 - 13
src/pages/settings/index.tsx

@@ -7,26 +7,26 @@ import {
   IconTool,
   IconUser,
 } from '@tabler/icons-react'
+import { Layout } from '@/components/custom/layout'
 import { Search } from '@/components/search'
 import { Separator } from '@/components/ui/separator'
 import ThemeSwitch from '@/components/theme-switch'
 import { UserNav } from '@/components/user-nav'
-import { Layout, LayoutBody, LayoutHeader } from '@/components/custom/layout'
 import SidebarNav from './components/sidebar-nav'
 
 export default function Settings() {
   return (
-    <Layout fadedBelow fixedHeight>
+    <Layout fixed>
       {/* ===== Top Heading ===== */}
-      <LayoutHeader>
+      <Layout.Header>
         <Search />
         <div className='ml-auto flex items-center space-x-4'>
           <ThemeSwitch />
           <UserNav />
         </div>
-      </LayoutHeader>
+      </Layout.Header>
 
-      <LayoutBody className='flex flex-col' fixedHeight>
+      <Layout.Body className='flex flex-col'>
         <div className='space-y-0.5'>
           <h1 className='text-2xl font-bold tracking-tight md:text-3xl'>
             Settings
@@ -35,18 +35,16 @@ export default function Settings() {
             Manage your account settings and set e-mail preferences.
           </p>
         </div>
-        <Separator className='my-6' />
-        <div className='flex flex-1 flex-col space-y-8 overflow-auto lg:flex-row lg:space-x-12 lg:space-y-0'>
-          <aside className='sticky top-0 lg:w-1/5'>
+        <Separator className='my-4 lg:my-6' />
+        <div className='flex flex-1 flex-col space-y-8 md:space-y-2 md:overflow-hidden lg:flex-row lg:space-x-12 lg:space-y-0'>
+          <aside className='top-0 lg:sticky lg:w-1/5'>
             <SidebarNav items={sidebarNavItems} />
           </aside>
-          <div className='w-full p-1 pr-4 lg:max-w-xl'>
-            <div className='pb-16'>
-              <Outlet />
-            </div>
+          <div className='flex w-full p-1 pr-4 md:overflow-y-hidden'>
+            <Outlet />
           </div>
         </div>
-      </LayoutBody>
+      </Layout.Body>
     </Layout>
   )
 }

+ 6 - 10
src/pages/settings/notifications/index.tsx

@@ -1,17 +1,13 @@
-import { Separator } from '@/components/ui/separator'
 import { NotificationsForm } from './notifications-form'
+import ContentSection from '../components/content-section'
 
 export default function SettingsNotifications() {
   return (
-    <div className='space-y-6'>
-      <div>
-        <h3 className='text-lg font-medium'>Notifications</h3>
-        <p className='text-sm text-muted-foreground'>
-          Configure how you receive notifications.
-        </p>
-      </div>
-      <Separator />
+    <ContentSection
+      title='Notifications'
+      desc='Configure how you receive notifications.'
+    >
       <NotificationsForm />
-    </div>
+    </ContentSection>
   )
 }

+ 6 - 10
src/pages/settings/profile/index.tsx

@@ -1,17 +1,13 @@
-import { Separator } from '@/components/ui/separator'
 import ProfileForm from './profile-form'
+import ContentSection from '../components/content-section'
 
 export default function SettingsProfile() {
   return (
-    <div className='space-y-6'>
-      <div>
-        <h3 className='text-lg font-medium'>Profile</h3>
-        <p className='text-sm text-muted-foreground'>
-          This is how others will see you on the site.
-        </p>
-      </div>
-      <Separator className='my-4' />
+    <ContentSection
+      title='Profile'
+      desc='This is how others will see you on the site.'
+    >
       <ProfileForm />
-    </div>
+    </ContentSection>
   )
 }

+ 5 - 5
src/pages/tasks/index.tsx

@@ -1,7 +1,7 @@
+import { Layout } from '@/components/custom/layout'
 import { Search } from '@/components/search'
 import ThemeSwitch from '@/components/theme-switch'
 import { UserNav } from '@/components/user-nav'
-import { Layout, LayoutBody, LayoutHeader } from '@/components/custom/layout'
 import { DataTable } from './components/data-table'
 import { columns } from './components/columns'
 import { tasks } from './data/tasks'
@@ -10,15 +10,15 @@ export default function Tasks() {
   return (
     <Layout>
       {/* ===== Top Heading ===== */}
-      <LayoutHeader>
+      <Layout.Header sticky>
         <Search />
         <div className='ml-auto flex items-center space-x-4'>
           <ThemeSwitch />
           <UserNav />
         </div>
-      </LayoutHeader>
+      </Layout.Header>
 
-      <LayoutBody className='flex flex-col' fixedHeight>
+      <Layout.Body>
         <div className='mb-2 flex items-center justify-between space-y-2'>
           <div>
             <h2 className='text-2xl font-bold tracking-tight'>Welcome back!</h2>
@@ -30,7 +30,7 @@ export default function Tasks() {
         <div className='-mx-4 flex-1 overflow-auto px-4 py-1 lg:flex-row lg:space-x-12 lg:space-y-0'>
           <DataTable data={tasks} columns={columns} />
         </div>
-      </LayoutBody>
+      </Layout.Body>
     </Layout>
   )
 }