Bläddra i källkod

refactor: update layouts and extract common layout

Update the entire layout structure and css. Extract
common components like Header and Main.
satnaing 1 år sedan
förälder
incheckning
8936dbede6

+ 1 - 0
package.json

@@ -30,6 +30,7 @@
     "@radix-ui/react-tabs": "^1.1.1",
     "@radix-ui/react-toast": "^1.2.2",
     "@radix-ui/react-tooltip": "^1.1.4",
+    "@radix-ui/react-visually-hidden": "^1.1.0",
     "@tabler/icons-react": "^3.22.0",
     "@tanstack/react-query": "^5.59.20",
     "@tanstack/react-router": "^1.81.4",

+ 3 - 0
pnpm-lock.yaml

@@ -62,6 +62,9 @@ importers:
       '@radix-ui/react-tooltip':
         specifier: ^1.1.4
         version: 1.1.4(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+      '@radix-ui/react-visually-hidden':
+        specifier: ^1.1.0
+        version: 1.1.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
       '@tabler/icons-react':
         specifier: ^3.22.0
         version: 3.22.0(react@18.3.1)

+ 0 - 25
src/components/app-sidebar-trigger.tsx

@@ -1,25 +0,0 @@
-import React from 'react'
-import { cn } from '@/lib/utils'
-import { Button } from '@/components/ui/button'
-import { useSidebar } from '@/components/ui/sidebar'
-
-export const AppSidebarTrigger = React.forwardRef<
-  React.ElementRef<typeof Button>,
-  React.ComponentProps<typeof Button>
->(({ className, size = 'icon', children, ...props }, ref) => {
-  const { toggleSidebar } = useSidebar()
-
-  return (
-    <Button
-      ref={ref}
-      size={size}
-      onClick={toggleSidebar}
-      className={cn('h-7 w-7', className)}
-      {...props}
-    >
-      {children}
-      <span className='sr-only'>Toggle Sidebar</span>
-    </Button>
-  )
-})
-AppSidebarTrigger.displayName = 'AppSidebarTrigger'

+ 14 - 21
src/components/layout/app-sidebar.tsx

@@ -14,7 +14,6 @@ import {
   IconPalette,
   IconServerOff,
   IconSettings,
-  IconSwitchHorizontal,
   IconTool,
   IconUserCog,
   IconUserOff,
@@ -31,7 +30,6 @@ import {
 import { NavGroup } from '@/components/layout/nav-group'
 import { NavUser } from '@/components/layout/nav-user'
 import { TeamSwitcher } from '@/components/layout/team-switcher'
-import { AppSidebarTrigger } from '../app-sidebar-trigger'
 import { type SidebarData } from './types'
 
 // SideNav data
@@ -198,24 +196,19 @@ const data: SidebarData = {
 
 export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
   return (
-    <div className='relative'>
-      <Sidebar collapsible='icon' {...props}>
-        <SidebarHeader>
-          <TeamSwitcher teams={data.teams} />
-        </SidebarHeader>
-        <SidebarContent>
-          {data.navGroups.map((props) => (
-            <NavGroup key={props.title} {...props} />
-          ))}
-        </SidebarContent>
-        <SidebarFooter>
-          <NavUser user={data.user} />
-        </SidebarFooter>
-        <SidebarRail />
-      </Sidebar>
-      <AppSidebarTrigger className='absolute top-1/2 -right-4 z-50 rounded-full hidden md:inline-flex'>
-        <IconSwitchHorizontal />
-      </AppSidebarTrigger>
-    </div>
+    <Sidebar collapsible='icon' {...props}>
+      <SidebarHeader>
+        <TeamSwitcher teams={data.teams} />
+      </SidebarHeader>
+      <SidebarContent>
+        {data.navGroups.map((props) => (
+          <NavGroup key={props.title} {...props} />
+        ))}
+      </SidebarContent>
+      <SidebarFooter>
+        <NavUser user={data.user} />
+      </SidebarFooter>
+      <SidebarRail />
+    </Sidebar>
   )
 }

+ 44 - 0
src/components/layout/header.tsx

@@ -0,0 +1,44 @@
+import React from 'react'
+import { cn } from '@/lib/utils'
+import { Separator } from '@/components/ui/separator'
+import { SidebarTrigger } from '@/components/ui/sidebar'
+
+interface HeaderProps extends React.HTMLAttributes<React.ElementRef<'header'>> {
+  sticky?: boolean
+}
+
+export const Header = React.forwardRef<React.ElementRef<'header'>, HeaderProps>(
+  ({ className, sticky, children, ...props }, ref) => {
+    const [offset, setOffset] = React.useState(0)
+
+    React.useEffect(() => {
+      const onScroll = () => {
+        setOffset(document.body.scrollTop || document.documentElement.scrollTop)
+      }
+
+      // Add scroll listener to the body
+      document.addEventListener('scroll', onScroll, { passive: true })
+
+      // Clean up the event listener on unmount
+      return () => document.removeEventListener('scroll', onScroll)
+    }, [])
+
+    return (
+      <header
+        ref={ref}
+        className={cn(
+          'flex items-center gap-3 sm:gap-4 bg-background p-4 md:px-8 h-16',
+          sticky && 'sticky top-0 z-10',
+          offset > 10 && sticky ? 'shadow' : 'shadow-none',
+          className
+        )}
+        {...props}
+      >
+        <SidebarTrigger variant='outline' className='scale-125 sm:scale-100' />
+        <Separator orientation='vertical' className='h-6' />
+        {children}
+      </header>
+    )
+  }
+)
+Header.displayName = 'Header'

+ 0 - 0
src/components/layout/index.tsx → src/components/layout/layout-backup.tsx


+ 22 - 0
src/components/layout/main.tsx

@@ -0,0 +1,22 @@
+import React from 'react'
+import { cn } from '@/lib/utils'
+
+interface MainProps extends React.HTMLAttributes<React.ElementRef<'main'>> {
+  fixed?: boolean
+}
+
+export const Main = React.forwardRef<React.ElementRef<'main'>, MainProps>(
+  ({ fixed, ...props }, ref) => {
+    return (
+      <main
+        ref={ref}
+        className={cn(
+          'px-4 py-6 md:px-8',
+          fixed && 'flex flex-col flex-grow overflow-hidden'
+        )}
+        {...props}
+      />
+    )
+  }
+)
+Main.displayName = 'Main'

+ 13 - 1
src/components/ui/sidebar.tsx

@@ -1,5 +1,6 @@
 import * as React from 'react'
 import { Slot } from '@radix-ui/react-slot'
+import { VisuallyHidden } from '@radix-ui/react-visually-hidden'
 import { VariantProps, cva } from 'class-variance-authority'
 import { PanelLeft } from 'lucide-react'
 import { cn } from '@/lib/utils'
@@ -7,7 +8,12 @@ import { useIsMobile } from '@/hooks/use-mobile'
 import { Button } from '@/components/ui/button'
 import { Input } from '@/components/ui/input'
 import { Separator } from '@/components/ui/separator'
-import { Sheet, SheetContent } from '@/components/ui/sheet'
+import {
+  Sheet,
+  SheetContent,
+  SheetDescription,
+  SheetTitle,
+} from '@/components/ui/sheet'
 import { Skeleton } from '@/components/ui/skeleton'
 import {
   Tooltip,
@@ -192,6 +198,9 @@ const Sidebar = React.forwardRef<
     if (isMobile) {
       return (
         <Sheet open={openMobile} onOpenChange={setOpenMobile} {...props}>
+          <VisuallyHidden asChild>
+            <SheetTitle />
+          </VisuallyHidden>
           <SheetContent
             data-sidebar='sidebar'
             data-mobile='true'
@@ -203,6 +212,9 @@ const Sidebar = React.forwardRef<
             }
             side={side}
           >
+            <VisuallyHidden asChild>
+              <SheetDescription />
+            </VisuallyHidden>
             <div className='flex h-full w-full flex-col'>{children}</div>
           </SheetContent>
         </Sheet>

+ 13 - 14
src/features/apps/index.tsx

@@ -14,7 +14,8 @@ import {
 } from '@/components/ui/select'
 import { Separator } from '@/components/ui/separator'
 import { Button } from '@/components/button'
-import { Layout } from '@/components/layout'
+import { Header } from '@/components/layout/header'
+import { Main } from '@/components/layout/main'
 import { ProfileDropdown } from '@/components/profile-dropdown'
 import { Search } from '@/components/search'
 import { ThemeSwitch } from '@/components/theme-switch'
@@ -47,20 +48,18 @@ export default function Apps() {
     .filter((app) => app.name.toLowerCase().includes(searchTerm.toLowerCase()))
 
   return (
-    <Layout fixed>
+    <>
       {/* ===== Top Heading ===== */}
-      <Layout.Header>
-        <div className='flex w-full items-center justify-between'>
-          <Search />
-          <div className='flex items-center space-x-4'>
-            <ThemeSwitch />
-            <ProfileDropdown />
-          </div>
+      <Header>
+        <Search />
+        <div className='ml-auto flex items-center gap-4'>
+          <ThemeSwitch />
+          <ProfileDropdown />
         </div>
-      </Layout.Header>
+      </Header>
 
       {/* ===== Content ===== */}
-      <Layout.Body className='flex flex-col'>
+      <Main fixed>
         <div>
           <h1 className='text-2xl font-bold tracking-tight'>
             App Integrations
@@ -112,7 +111,7 @@ export default function Apps() {
           </Select>
         </div>
         <Separator className='shadow' />
-        <ul className='faded-bottom no-scrollbar grid gap-4 overflow-auto pb-16 pt-4 md:grid-cols-2 lg:grid-cols-3'>
+        <ul className='faded-bottom no-scrollbar h-full 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 +138,7 @@ export default function Apps() {
             </li>
           ))}
         </ul>
-      </Layout.Body>
-    </Layout>
+      </Main>
+    </>
   )
 }

+ 10 - 9
src/features/chats/index.tsx

@@ -19,7 +19,8 @@ import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
 import { ScrollArea } from '@/components/ui/scroll-area'
 import { Separator } from '@/components/ui/separator'
 import { Button } from '@/components/button'
-import { Layout } from '@/components/layout'
+import { Header } from '@/components/layout/header'
+import { Main } from '@/components/layout/main'
 import { ProfileDropdown } from '@/components/profile-dropdown'
 import { Search } from '@/components/search'
 import { ThemeSwitch } from '@/components/theme-switch'
@@ -59,17 +60,17 @@ export default function Chats() {
   )
 
   return (
-    <Layout fixed>
+    <>
       {/* ===== Top Heading ===== */}
-      <Layout.Header>
+      <Header>
         <Search />
         <div className='ml-auto flex items-center space-x-4'>
           <ThemeSwitch />
           <ProfileDropdown />
         </div>
-      </Layout.Header>
+      </Header>
 
-      <Layout.Body className='sm:overflow-hidden'>
+      <Main fixed>
         <section className='flex h-full gap-6'>
           {/* Left Side */}
           <div className='flex w-full flex-col gap-2 sm:w-56 lg:w-72 2xl:w-80'>
@@ -144,8 +145,8 @@ export default function Chats() {
           {/* Right Side */}
           <div
             className={cn(
-              'absolute inset-0 left-full z-50 flex w-full flex-1 flex-col rounded-md border bg-primary-foreground shadow-sm transition-all duration-200 sm:static sm:z-auto sm:flex',
-              mobileSelectedUser && 'left-0'
+              'absolute inset-0 hidden left-full z-50 w-full flex-1 flex-col rounded-md border bg-primary-foreground shadow-sm transition-all duration-200 sm:static sm:z-auto sm:flex',
+              mobileSelectedUser && 'left-0 flex'
             )}
           >
             {/* Top Part */}
@@ -300,7 +301,7 @@ export default function Chats() {
             </div>
           </div>
         </section>
-      </Layout.Body>
-    </Layout>
+      </Main>
+    </>
   )
 }

+ 8 - 7
src/features/dashboard/index.tsx

@@ -7,7 +7,8 @@ import {
 } from '@/components/ui/card'
 import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
 import { Button } from '@/components/button'
-import { Layout } from '@/components/layout'
+import { Header } from '@/components/layout/header'
+import { Main } from '@/components/layout/main'
 import { TopNav } from '@/components/layout/top-nav'
 import { ProfileDropdown } from '@/components/profile-dropdown'
 import { Search } from '@/components/search'
@@ -17,19 +18,19 @@ import { RecentSales } from './components/recent-sales'
 
 export default function Dashboard() {
   return (
-    <Layout>
+    <>
       {/* ===== Top Heading ===== */}
-      <Layout.Header>
+      <Header>
         <TopNav links={topNav} />
         <div className='ml-auto flex items-center space-x-4'>
           <Search />
           <ThemeSwitch />
           <ProfileDropdown />
         </div>
-      </Layout.Header>
+      </Header>
 
       {/* ===== Main ===== */}
-      <Layout.Body>
+      <Main>
         <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'>
@@ -182,8 +183,8 @@ export default function Dashboard() {
             </div>
           </TabsContent>
         </Tabs>
-      </Layout.Body>
-    </Layout>
+      </Main>
+    </>
   )
 }
 

+ 10 - 9
src/features/settings/index.tsx

@@ -7,7 +7,8 @@ import {
   IconUser,
 } from '@tabler/icons-react'
 import { Separator } from '@/components/ui/separator'
-import { Layout } from '@/components/layout'
+import { Header } from '@/components/layout/header'
+import { Main } from '@/components/layout/main'
 import { ProfileDropdown } from '@/components/profile-dropdown'
 import { Search } from '@/components/search'
 import { ThemeSwitch } from '@/components/theme-switch'
@@ -15,17 +16,17 @@ import SidebarNav from './components/sidebar-nav'
 
 export default function Settings() {
   return (
-    <Layout fixed>
+    <>
       {/* ===== Top Heading ===== */}
-      <Layout.Header>
+      <Header>
         <Search />
         <div className='ml-auto flex items-center space-x-4'>
           <ThemeSwitch />
           <ProfileDropdown />
         </div>
-      </Layout.Header>
+      </Header>
 
-      <Layout.Body className='flex flex-col'>
+      <Main fixed>
         <div className='space-y-0.5'>
           <h1 className='text-2xl font-bold tracking-tight md:text-3xl'>
             Settings
@@ -35,16 +36,16 @@ export default function Settings() {
           </p>
         </div>
         <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'>
+        <div className='flex flex-1 flex-col space-y-2 md:space-y-2 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='flex w-full p-1 pr-4 md:overflow-y-hidden'>
+          <div className='flex w-full p-1 pr-4 overflow-y-hidden'>
             <Outlet />
           </div>
         </div>
-      </Layout.Body>
-    </Layout>
+      </Main>
+    </>
   )
 }
 

+ 8 - 7
src/features/tasks/index.tsx

@@ -1,4 +1,5 @@
-import { Layout } from '@/components/layout'
+import { Header } from '@/components/layout/header'
+import { Main } from '@/components/layout/main'
 import { ProfileDropdown } from '@/components/profile-dropdown'
 import { Search } from '@/components/search'
 import { ThemeSwitch } from '@/components/theme-switch'
@@ -8,17 +9,17 @@ import { tasks } from './data/tasks'
 
 export default function Tasks() {
   return (
-    <Layout>
+    <>
       {/* ===== Top Heading ===== */}
-      <Layout.Header sticky>
+      <Header sticky>
         <Search />
         <div className='ml-auto flex items-center space-x-4'>
           <ThemeSwitch />
           <ProfileDropdown />
         </div>
-      </Layout.Header>
+      </Header>
 
-      <Layout.Body>
+      <Main>
         <div className='mb-2 flex items-center justify-between space-y-2'>
           <div>
             <h2 className='text-2xl font-bold tracking-tight'>Tasks</h2>
@@ -30,7 +31,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>
-      </Layout.Body>
-    </Layout>
+      </Main>
+    </>
   )
 }

+ 12 - 13
src/index.css

@@ -102,19 +102,6 @@
     }
   }
 
-  * {
-    @apply border-border;
-  }
-  body {
-    @apply min-h-svh w-full bg-background text-foreground;
-  }
-  body > #root {
-    @apply h-svh;
-  }
-  body > #root > div:first-child {
-    @apply h-full;
-  }
-
   /* Prevent focus zoom on mobile devices */
   @media screen and (max-width: 767px) {
     input,
@@ -140,3 +127,15 @@
     @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;
   }
 }
+
+@layer base {
+  * {
+    @apply border-border;
+  }
+  html {
+    @apply overflow-x-hidden;
+  }
+  body {
+    @apply min-h-svh w-full bg-background text-foreground;
+  }
+}

+ 12 - 2
src/routes/_authenticated/route.tsx

@@ -1,5 +1,6 @@
 import { createFileRoute, Outlet } from '@tanstack/react-router'
 import Cookies from 'js-cookie'
+import { cn } from '@/lib/utils'
 import { SidebarProvider } from '@/components/ui/sidebar'
 import { AppSidebar } from '@/components/layout/app-sidebar'
 import SkipToMain from '@/components/skip-to-main'
@@ -14,9 +15,18 @@ function RouteComponent() {
     <SidebarProvider defaultOpen={defaultOpen}>
       <SkipToMain />
       <AppSidebar />
-      <main id='content' className='overflow-x-hidden h-full w-full'>
+      <div
+        id='content'
+        className={cn(
+          'max-w-full w-full ml-auto',
+          'peer-data-[state=collapsed]:w-[calc(100%-var(--sidebar-width-icon))]',
+          'peer-data-[state=expanded]:w-[calc(100%-var(--sidebar-width))]',
+          'transition-[width] ease-linear duration-200',
+          'h-svh flex flex-col'
+        )}
+      >
         <Outlet />
-      </main>
+      </div>
     </SidebarProvider>
   )
 }