Просмотр исходного кода

feat: add config drawer and update overall layout (#186)

* feat!: implement layout context and configuration drawer

This update introduces a new layout context to manage sidebar collapsibility and variant settings, enhancing the application's layout flexibility. Additionally, a configuration drawer component is added, allowing users to adjust theme settings dynamically. Various components are updated to utilize the new context, improving overall state management and user experience.

* feat: update layout config style with custom icons

* feat: add collapsible config in theme config

* feat: add light/dark theme setting in theme config

* feat: add dir theme setting in theme config

* refactor: extract RadioGroupItem and improve UX

- Extract reusable RadioGroupItem component to reduce code duplication
- Add CircleCheck icon for better visual feedback on selected items
- Improve import structure by destructuring Radix UI components
- Update sheet description text for better clarity
- Add focus-visible ring styling for improved accessibility
- Reorder component functions for better code organization
- Remove redundant className from Settings icon

* feat: enhance theme management with resolvedTheme state

Update ThemeConfig to use resolvedTheme instead of theme. This ensures the active state even if the user chooses 'system'.

* feat: add theme config reset buttons

* feat: add config for system theme

* fix: make config drawer scrollable if overflow

* feat: add default layout config option

* refactor: update theme config section names and icons

* fix: update sidebar reset state

* fix: optimize theme resolution and improve a11y

- Replace getResolvedTheme function with useMemo to prevent unnecessary re-computations
- Remove redundant useState for resolvedTheme state management
- Add comprehensive ARIA labels and accessibility attributes to config drawer
- Improve screen reader support with proper semantic markup
- Add descriptive labels for all radio groups and interactive elements
- Mark decorative icons as aria-hidden for better accessibility
- Add hidden descriptions explaining each configuration option
- Update direction description text for better clarity
Sat Naing 11 месяцев назад
Родитель
Сommit
7e7c68bb9c

+ 1 - 0
README.md

@@ -32,6 +32,7 @@ If you want to update components using the Shadcn CLI (e.g., `npx shadcn@latest
 
 - scroll-area
 - sonner
+- separator
 
 ### RTL Updated Components
 

+ 1 - 1
index.html

@@ -73,7 +73,7 @@
     <meta name="theme-color" content="#fff" />
   </head>
 
-  <body class="group/body">
+  <body>
     <div id="root"></div>
     <script type="module" src="/src/main.tsx"></script>
   </body>

+ 110 - 0
src/assets/custom/icon-dir.tsx

@@ -0,0 +1,110 @@
+import { SVGProps } from 'react'
+import { cn } from '@/lib/utils'
+import { Direction } from '@/context/direction-context'
+
+interface Props extends SVGProps<SVGSVGElement> {
+  dir: Direction
+}
+
+export function IconDir({ dir, className, ...props }: Props) {
+  return (
+    <svg
+      data-name={`icon-dir-${dir}`}
+      xmlns='http://www.w3.org/2000/svg'
+      viewBox='0 0 79.86 51.14'
+      className={cn(dir === 'rtl' && 'rotate-y-180', className)}
+      {...props}
+    >
+      <path
+        d='M23.42.51h51.92c2.21 0 4 1.79 4 4v42.18c0 2.21-1.79 4-4 4H23.42s-.04-.02-.04-.04V.55s.02-.04.04-.04z'
+        opacity={0.15}
+      />
+      <path
+        fill='none'
+        opacity={0.72}
+        strokeLinecap='round'
+        strokeMiterlimit={10}
+        strokeWidth='2px'
+        d='M5.56 14.88L17.78 14.88'
+      />
+      <path
+        fill='none'
+        opacity={0.48}
+        strokeLinecap='round'
+        strokeMiterlimit={10}
+        strokeWidth='2px'
+        d='M5.56 22.09L16.08 22.09'
+      />
+      <path
+        fill='none'
+        opacity={0.55}
+        strokeLinecap='round'
+        strokeMiterlimit={10}
+        strokeWidth='2px'
+        d='M5.56 18.38L14.93 18.38'
+      />
+      <g strokeLinecap='round' strokeMiterlimit={10}>
+        <circle cx={7.51} cy={7.4} r={2.54} opacity={0.8} />
+        <path
+          fill='none'
+          opacity={0.8}
+          strokeWidth='2px'
+          d='M12.06 6.14L17.78 6.14'
+        />
+        <path fill='none' opacity={0.6} d='M11.85 8.79L16.91 8.79' />
+      </g>
+      <path
+        fill='none'
+        opacity={0.62}
+        strokeLinecap='round'
+        strokeMiterlimit={10}
+        strokeWidth='3px'
+        d='M29.41 7.4L34.67 7.4'
+      />
+      <rect
+        x={28.76}
+        y={11.21}
+        width={26.03}
+        height={2.73}
+        rx={0.64}
+        ry={0.64}
+        opacity={0.44}
+        strokeLinecap='round'
+        strokeMiterlimit={10}
+      />
+      <rect
+        x={28.76}
+        y={17.01}
+        width={44.25}
+        height={13.48}
+        rx={0.64}
+        ry={0.64}
+        opacity={0.3}
+        strokeLinecap='round'
+        strokeMiterlimit={10}
+      />
+      <rect
+        x={28.76}
+        y={33.57}
+        width={44.25}
+        height={4.67}
+        rx={0.64}
+        ry={0.64}
+        opacity={0.21}
+        strokeLinecap='round'
+        strokeMiterlimit={10}
+      />
+      <rect
+        x={28.76}
+        y={41.32}
+        width={36.21}
+        height={4.67}
+        rx={0.64}
+        ry={0.64}
+        opacity={0.3}
+        strokeLinecap='round'
+        strokeMiterlimit={10}
+      />
+    </svg>
+  )
+}

+ 131 - 0
src/assets/custom/icon-layout-compact.tsx

@@ -0,0 +1,131 @@
+import { SVGProps } from 'react'
+
+export function IconLayoutCompact(props: SVGProps<SVGSVGElement>) {
+  return (
+    <svg
+      data-name='icon-layout-compact'
+      xmlns='http://www.w3.org/2000/svg'
+      viewBox='0 0 79.86 51.14'
+      {...props}
+    >
+      <rect
+        x={5.84}
+        y={5.2}
+        width={4}
+        height={40}
+        rx={2}
+        ry={2}
+        strokeLinecap='round'
+        strokeMiterlimit={10}
+      />
+      <g stroke='#fff' strokeLinecap='round' strokeMiterlimit={10}>
+        <path
+          fill='none'
+          opacity={0.66}
+          strokeWidth='2px'
+          d='M7.26 11.56L8.37 11.56'
+        />
+        <path
+          fill='none'
+          opacity={0.51}
+          strokeWidth='2px'
+          d='M7.26 14.49L8.37 14.49'
+        />
+        <path
+          fill='none'
+          opacity={0.52}
+          strokeWidth='2px'
+          d='M7.26 17.39L8.37 17.39'
+        />
+        <circle cx={7.81} cy={7.25} r={1.16} fill='#fff' opacity={0.8} />
+      </g>
+      <path
+        fill='none'
+        opacity={0.75}
+        strokeLinecap='round'
+        strokeMiterlimit={10}
+        strokeWidth='3px'
+        d='M15.81 14.49L22.89 14.49'
+      />
+      <rect
+        x={14.93}
+        y={18.39}
+        width={22.19}
+        height={2.73}
+        rx={0.64}
+        ry={0.64}
+        opacity={0.5}
+        strokeLinecap='round'
+        strokeMiterlimit={10}
+      />
+      <rect
+        x={14.93}
+        y={5.89}
+        width={59.16}
+        height={2.73}
+        rx={0.64}
+        ry={0.64}
+        opacity={0.9}
+        strokeLinecap='round'
+        strokeMiterlimit={10}
+      />
+      <rect
+        x={14.93}
+        y={24.22}
+        width={32.68}
+        height={19.95}
+        rx={2.11}
+        ry={2.11}
+        opacity={0.4}
+        strokeLinecap='round'
+        strokeMiterlimit={10}
+      />
+      <g strokeLinecap='round' strokeMiterlimit={10}>
+        <rect
+          x={59.05}
+          y={38.15}
+          width={2.01}
+          height={3.42}
+          rx={0.33}
+          ry={0.33}
+          opacity={0.32}
+        />
+        <rect
+          x={54.78}
+          y={34.99}
+          width={2.01}
+          height={6.58}
+          rx={0.33}
+          ry={0.33}
+          opacity={0.44}
+        />
+        <rect
+          x={63.17}
+          y={32.86}
+          width={2.01}
+          height={8.7}
+          rx={0.33}
+          ry={0.33}
+          opacity={0.53}
+        />
+        <rect
+          x={67.54}
+          y={29.17}
+          width={2.01}
+          height={12.4}
+          rx={0.33}
+          ry={0.33}
+          opacity={0.66}
+        />
+      </g>
+      <g opacity={0.5}>
+        <circle cx={62.16} cy={18.63} r={7.5} />
+        <path d='M62.16 11.63c3.86 0 7 3.14 7 7s-3.14 7-7 7-7-3.14-7-7 3.14-7 7-7m0-1c-4.42 0-8 3.58-8 8s3.58 8 8 8 8-3.58 8-8-3.58-8-8-8z' />
+      </g>
+      <g opacity={0.74}>
+        <path d='M63.04 18.13l3.38-5.67c.93.64 1.7 1.48 2.26 2.47.56.98.89 2.08.96 3.21h-6.6z' />
+        <path d='M66.57 13.19a6.977 6.977 0 012.52 4.44h-5.17l2.65-4.44m-.31-1.43l-4.1 6.87h8c0-1.39-.36-2.75-1.04-3.95a8.007 8.007 0 00-2.86-2.92z' />
+      </g>
+    </svg>
+  )
+}

+ 124 - 0
src/assets/custom/icon-layout-default.tsx

@@ -0,0 +1,124 @@
+import { SVGProps } from 'react'
+
+export function IconLayoutDefault(props: SVGProps<SVGSVGElement>) {
+  return (
+    <svg
+      data-name='con-layout-default'
+      xmlns='http://www.w3.org/2000/svg'
+      viewBox='0 0 79.86 51.14'
+      {...props}
+    >
+      <path
+        d='M39.22 15.99h-8.16c-.79 0-1.43-.67-1.43-1.5s.64-1.5 1.43-1.5h8.16c.79 0 1.43.67 1.43 1.5s-.64 1.5-1.43 1.5z'
+        opacity={0.75}
+      />
+      <rect
+        x={29.63}
+        y={18.39}
+        width={16.72}
+        height={2.73}
+        rx={1.36}
+        ry={1.36}
+        opacity={0.5}
+      />
+      <path
+        d='M75.1 6.68v1.45c0 .63-.49 1.14-1.09 1.14H30.72c-.6 0-1.09-.51-1.09-1.14V6.68c0-.62.49-1.14 1.09-1.14h43.29c.6 0 1.09.52 1.09 1.14z'
+        opacity={0.9}
+      />
+      <rect
+        x={29.63}
+        y={24.22}
+        width={21.8}
+        height={19.95}
+        rx={2.11}
+        ry={2.11}
+        opacity={0.4}
+      />
+      <g strokeLinecap='round' strokeMiterlimit={10}>
+        <rect
+          x={61.06}
+          y={38.15}
+          width={2.01}
+          height={3.42}
+          rx={0.33}
+          ry={0.33}
+          opacity={0.32}
+        />
+        <rect
+          x={56.78}
+          y={34.99}
+          width={2.01}
+          height={6.58}
+          rx={0.33}
+          ry={0.33}
+          opacity={0.44}
+        />
+        <rect
+          x={65.17}
+          y={32.86}
+          width={2.01}
+          height={8.7}
+          rx={0.33}
+          ry={0.33}
+          opacity={0.53}
+        />
+        <rect
+          x={69.55}
+          y={29.17}
+          width={2.01}
+          height={12.4}
+          rx={0.33}
+          ry={0.33}
+          opacity={0.66}
+        />
+      </g>
+      <g opacity={0.5}>
+        <circle cx={63.17} cy={18.63} r={7.5} />
+        <path d='M63.17 11.63c3.86 0 7 3.14 7 7s-3.14 7-7 7-7-3.14-7-7 3.14-7 7-7m0-1c-4.42 0-8 3.58-8 8s3.58 8 8 8 8-3.58 8-8-3.58-8-8-8z' />
+      </g>
+      <g opacity={0.74}>
+        <path d='M64.05 18.13l3.38-5.67c.93.64 1.7 1.48 2.26 2.47.56.98.89 2.08.96 3.21h-6.6z' />
+        <path d='M67.57 13.19a6.977 6.977 0 012.52 4.44h-5.17l2.65-4.44m-.31-1.43l-4.1 6.87h8c0-1.39-.36-2.75-1.04-3.95a8.007 8.007 0 00-2.86-2.92z' />
+      </g>
+      <g strokeLinecap='round' strokeMiterlimit={10}>
+        <rect
+          x={5.84}
+          y={5.02}
+          width={19.14}
+          height={40}
+          rx={2}
+          ry={2}
+          opacity={0.8}
+        />
+        <g stroke='#fff'>
+          <path
+            fill='none'
+            opacity={0.72}
+            strokeWidth='2px'
+            d='M9.02 17.39L21.25 17.39'
+          />
+          <path
+            fill='none'
+            opacity={0.48}
+            strokeWidth='2px'
+            d='M9.02 24.6L19.54 24.6'
+          />
+          <path
+            fill='none'
+            opacity={0.55}
+            strokeWidth='2px'
+            d='M9.02 20.88L18.4 20.88'
+          />
+          <circle cx={10.98} cy={9.91} r={2.54} fill='#fff' opacity={0.8} />
+          <path
+            fill='none'
+            opacity={0.8}
+            strokeWidth='2px'
+            d='M15.53 8.65L21.25 8.65'
+          />
+          <path fill='none' opacity={0.6} d='M15.32 11.3L20.38 11.3' />
+        </g>
+      </g>
+    </svg>
+  )
+}

+ 100 - 0
src/assets/custom/icon-layout-full.tsx

@@ -0,0 +1,100 @@
+import { SVGProps } from 'react'
+
+export function IconLayoutFull(props: SVGProps<SVGSVGElement>) {
+  return (
+    <svg
+      data-name='icon-layout-full'
+      xmlns='http://www.w3.org/2000/svg'
+      viewBox='0 0 79.86 51.14'
+      {...props}
+    >
+      <path
+        fill='none'
+        opacity={0.75}
+        strokeLinecap='round'
+        strokeMiterlimit={10}
+        strokeWidth='3px'
+        d='M6.85 14.49L15.02 14.49'
+      />
+      <rect
+        x={5.84}
+        y={18.39}
+        width={25.6}
+        height={2.73}
+        rx={0.64}
+        ry={0.64}
+        opacity={0.5}
+        strokeLinecap='round'
+        strokeMiterlimit={10}
+      />
+      <rect
+        x={5.84}
+        y={5.89}
+        width={68.26}
+        height={2.73}
+        rx={0.64}
+        ry={0.64}
+        opacity={0.9}
+        strokeLinecap='round'
+        strokeMiterlimit={10}
+      />
+      <rect
+        x={5.84}
+        y={24.22}
+        width={37.71}
+        height={19.95}
+        rx={2.11}
+        ry={2.11}
+        opacity={0.4}
+        strokeLinecap='round'
+        strokeMiterlimit={10}
+      />
+      <g strokeLinecap='round' strokeMiterlimit={10}>
+        <rect
+          x={59.05}
+          y={38.15}
+          width={2.01}
+          height={3.42}
+          rx={0.33}
+          ry={0.33}
+          opacity={0.32}
+        />
+        <rect
+          x={54.78}
+          y={34.99}
+          width={2.01}
+          height={6.58}
+          rx={0.33}
+          ry={0.33}
+          opacity={0.44}
+        />
+        <rect
+          x={63.17}
+          y={32.86}
+          width={2.01}
+          height={8.7}
+          rx={0.33}
+          ry={0.33}
+          opacity={0.53}
+        />
+        <rect
+          x={67.54}
+          y={29.17}
+          width={2.01}
+          height={12.4}
+          rx={0.33}
+          ry={0.33}
+          opacity={0.66}
+        />
+      </g>
+      <g opacity={0.5}>
+        <circle cx={62.16} cy={18.63} r={7.5} />
+        <path d='M62.16 11.63c3.86 0 7 3.14 7 7s-3.14 7-7 7-7-3.14-7-7 3.14-7 7-7m0-1c-4.42 0-8 3.58-8 8s3.58 8 8 8 8-3.58 8-8-3.58-8-8-8z' />
+      </g>
+      <g opacity={0.74}>
+        <path d='M63.04 18.13l3.38-5.67c.93.64 1.7 1.48 2.26 2.47.56.98.89 2.08.96 3.21h-6.6z' />
+        <path d='M66.57 13.19a6.977 6.977 0 012.52 4.44h-5.17l2.65-4.44m-.31-1.43l-4.1 6.87h8c0-1.39-.36-2.75-1.04-3.95a8.007 8.007 0 00-2.86-2.92z' />
+      </g>
+    </svg>
+  )
+}

+ 82 - 0
src/assets/custom/icon-sidebar-floating.tsx

@@ -0,0 +1,82 @@
+import { SVGProps } from 'react'
+
+export function IconSidebarFloating(props: SVGProps<SVGSVGElement>) {
+  return (
+    <svg
+      data-name='icon-sidebar-floating'
+      xmlns='http://www.w3.org/2000/svg'
+      viewBox='0 0 79.86 51.14'
+      {...props}
+    >
+      <rect
+        x={5.89}
+        y={5.15}
+        width={19.74}
+        height={40}
+        rx={2}
+        ry={2}
+        opacity={0.8}
+        strokeLinecap='round'
+        strokeMiterlimit={10}
+      />
+      <g stroke='#fff' strokeLinecap='round' strokeMiterlimit={10}>
+        <path
+          fill='none'
+          opacity={0.72}
+          strokeWidth='2px'
+          d='M9.81 18.36L22.04 18.36'
+        />
+        <path
+          fill='none'
+          opacity={0.48}
+          strokeWidth='2px'
+          d='M9.81 25.57L20.33 25.57'
+        />
+        <path
+          fill='none'
+          opacity={0.55}
+          strokeWidth='2px'
+          d='M9.81 21.85L19.18 21.85'
+        />
+        <circle cx={11.76} cy={10.88} r={2.54} fill='#fff' opacity={0.8} />
+        <path
+          fill='none'
+          opacity={0.8}
+          strokeWidth='2px'
+          d='M16.31 9.62L22.04 9.62'
+        />
+        <path fill='none' opacity={0.6} d='M16.1 12.27L21.16 12.27' />
+      </g>
+      <path
+        fill='none'
+        opacity={0.62}
+        strokeLinecap='round'
+        strokeMiterlimit={10}
+        strokeWidth='3px'
+        d='M30.59 9.62L35.85 9.62'
+      />
+      <rect
+        x={29.94}
+        y={13.42}
+        width={26.03}
+        height={2.73}
+        rx={0.64}
+        ry={0.64}
+        opacity={0.44}
+        strokeLinecap='round'
+        strokeMiterlimit={10}
+      />
+      <rect
+        x={29.94}
+        y={19.28}
+        width={43.11}
+        height={25.87}
+        rx={2}
+        ry={2}
+        opacity={0.3}
+        strokeLinecap='round'
+        strokeMiterlimit={10}
+      />
+    </svg>
+  )
+}

+ 58 - 0
src/assets/custom/icon-sidebar-inset.tsx

@@ -0,0 +1,58 @@
+import { SVGProps } from 'react'
+
+export function IconSidebarInset(props: SVGProps<SVGSVGElement>) {
+  return (
+    <svg
+      data-name='icon-sidebar-inset'
+      xmlns='http://www.w3.org/2000/svg'
+      viewBox='0 0 79.86 51.14'
+      {...props}
+    >
+      <rect
+        x={23.39}
+        y={5.57}
+        width={50.22}
+        height={40}
+        rx={2}
+        ry={2}
+        opacity={0.2}
+        strokeLinecap='round'
+        strokeMiterlimit={10}
+      />
+      <path
+        fill='none'
+        opacity={0.72}
+        strokeLinecap='round'
+        strokeMiterlimit={10}
+        strokeWidth='2px'
+        d='M5.08 17.05L17.31 17.05'
+      />
+      <path
+        fill='none'
+        opacity={0.48}
+        strokeLinecap='round'
+        strokeMiterlimit={10}
+        strokeWidth='2px'
+        d='M5.08 24.25L15.6 24.25'
+      />
+      <path
+        fill='none'
+        opacity={0.55}
+        strokeLinecap='round'
+        strokeMiterlimit={10}
+        strokeWidth='2px'
+        d='M5.08 20.54L14.46 20.54'
+      />
+      <g strokeLinecap='round' strokeMiterlimit={10}>
+        <circle cx={7.04} cy={9.57} r={2.54} opacity={0.8} />
+        <path
+          fill='none'
+          opacity={0.8}
+          strokeWidth='2px'
+          d='M11.59 8.3L17.31 8.3'
+        />
+        <path fill='none' opacity={0.6} d='M11.38 10.95L16.44 10.95' />
+      </g>
+    </svg>
+  )
+}

+ 53 - 0
src/assets/custom/icon-sidebar-sidebar.tsx

@@ -0,0 +1,53 @@
+import { SVGProps } from 'react'
+
+export function IconSidebarSidebar(props: SVGProps<SVGSVGElement>) {
+  return (
+    <svg
+      data-name='icon-sidebar-sidebar'
+      xmlns='http://www.w3.org/2000/svg'
+      viewBox='0 0 79.86 51.14'
+      {...props}
+    >
+      <path
+        d='M23.42.51h51.99c2.21 0 4 1.79 4 4v42.18c0 2.21-1.79 4-4 4H23.42s-.04-.02-.04-.04V.55s.02-.04.04-.04z'
+        opacity={0.2}
+        strokeLinecap='round'
+        strokeMiterlimit={10}
+      />
+      <path
+        fill='none'
+        opacity={0.72}
+        strokeLinecap='round'
+        strokeMiterlimit={10}
+        strokeWidth='2px'
+        d='M5.56 14.88L17.78 14.88'
+      />
+      <path
+        fill='none'
+        opacity={0.48}
+        strokeLinecap='round'
+        strokeMiterlimit={10}
+        strokeWidth='2px'
+        d='M5.56 22.09L16.08 22.09'
+      />
+      <path
+        fill='none'
+        opacity={0.55}
+        strokeLinecap='round'
+        strokeMiterlimit={10}
+        strokeWidth='2px'
+        d='M5.56 18.38L14.93 18.38'
+      />
+      <g strokeLinecap='round' strokeMiterlimit={10}>
+        <circle cx={7.51} cy={7.4} r={2.54} opacity={0.8} />
+        <path
+          fill='none'
+          opacity={0.8}
+          strokeWidth='2px'
+          d='M12.06 6.14L17.78 6.14'
+        />
+        <path fill='none' opacity={0.6} d='M11.85 8.79L16.91 8.79' />
+      </g>
+    </svg>
+  )
+}

+ 79 - 0
src/assets/custom/icon-theme-dark.tsx

@@ -0,0 +1,79 @@
+import { SVGProps } from 'react'
+
+export function IconThemeDark(props: SVGProps<SVGSVGElement>) {
+  return (
+    <svg
+      data-name='icon-theme-dark'
+      xmlns='http://www.w3.org/2000/svg'
+      viewBox='0 0 79.86 51.14'
+      {...props}
+    >
+      <g fill='#1d2b3f'>
+        <rect x={0.53} y={0.5} width={78.83} height={50.14} rx={3.5} ry={3.5} />
+        <path d='M75.86 1c1.65 0 3 1.35 3 3v43.14c0 1.65-1.35 3-3 3H4.03c-1.65 0-3-1.35-3-3V4c0-1.65 1.35-3 3-3h71.83m0-1H4.03c-2.21 0-4 1.79-4 4v43.14c0 2.21 1.79 4 4 4h71.83c2.21 0 4-1.79 4-4V4c0-2.21-1.79-4-4-4z' />
+      </g>
+      <path
+        d='M22.88 0h52.97c2.21 0 4 1.79 4 4v43.14c0 2.21-1.79 4-4 4H22.88V0z'
+        fill='#0d1628'
+      />
+      <circle cx={6.7} cy={7.04} r={3.54} fill='#426187' />
+      <path
+        d='M18.12 6.39h-5.87c-.6 0-1.09-.45-1.09-1s.49-1 1.09-1h5.87c.6 0 1.09.45 1.09 1s-.49 1-1.09 1zM16.55 9.77h-4.24c-.55 0-1-.45-1-1s.45-1 1-1h4.24c.55 0 1 .45 1 1s-.45 1-1 1zM18.32 17.37H4.59c-.69 0-1.25-.47-1.25-1.05s.56-1.05 1.25-1.05h13.73c.69 0 1.25.47 1.25 1.05s-.56 1.05-1.25 1.05zM15.34 21.26h-11c-.55 0-1-.41-1-.91s.45-.91 1-.91h11c.55 0 1 .41 1 .91s-.45.91-1 .91zM16.46 25.57H4.43c-.6 0-1.09-.44-1.09-.98s.49-.98 1.09-.98h12.03c.6 0 1.09.44 1.09.98s-.49.98-1.09.98z'
+        fill='#426187'
+      />
+      <g fill='#2a62bc'>
+        <rect
+          x={33.36}
+          y={19.73}
+          width={2.75}
+          height={3.42}
+          rx={0.33}
+          ry={0.33}
+          opacity={0.32}
+        />
+        <rect
+          x={29.64}
+          y={16.57}
+          width={2.75}
+          height={6.58}
+          rx={0.33}
+          ry={0.33}
+          opacity={0.44}
+        />
+        <rect
+          x={37.16}
+          y={14.44}
+          width={2.75}
+          height={8.7}
+          rx={0.33}
+          ry={0.33}
+          opacity={0.53}
+        />
+        <rect
+          x={41.19}
+          y={10.75}
+          width={2.75}
+          height={12.4}
+          rx={0.33}
+          ry={0.33}
+          opacity={0.53}
+        />
+      </g>
+      <circle cx={62.74} cy={16.32} r={8} fill='#2f5491' opacity={0.5} />
+      <path
+        d='M62.74 16.32l4.1-6.87c1.19.71 2.18 1.72 2.86 2.92s1.04 2.57 1.04 3.95h-8z'
+        fill='#2f5491'
+        opacity={0.74}
+      />
+      <rect
+        x={29.64}
+        y={27.75}
+        width={41.62}
+        height={18.62}
+        rx={1.69}
+        ry={1.69}
+        fill='#17273f'
+      />
+    </svg>
+  )
+}

+ 78 - 0
src/assets/custom/icon-theme-light.tsx

@@ -0,0 +1,78 @@
+import { SVGProps } from 'react'
+
+export function IconThemeLight(props: SVGProps<SVGSVGElement>) {
+  return (
+    <svg
+      data-name='icon-theme-light'
+      xmlns='http://www.w3.org/2000/svg'
+      viewBox='0 0 79.86 51.14'
+      {...props}
+    >
+      <g fill='#d9d9d9'>
+        <rect x={0.53} y={0.5} width={78.83} height={50.14} rx={3.5} ry={3.5} />
+        <path d='M75.86 1c1.65 0 3 1.35 3 3v43.14c0 1.65-1.35 3-3 3H4.03c-1.65 0-3-1.35-3-3V4c0-1.65 1.35-3 3-3h71.83m0-1H4.03c-2.21 0-4 1.79-4 4v43.14c0 2.21 1.79 4 4 4h71.83c2.21 0 4-1.79 4-4V4c0-2.21-1.79-4-4-4z' />
+      </g>
+      <path
+        d='M22.88 0h52.97c2.21 0 4 1.79 4 4v43.14c0 2.21-1.79 4-4 4H22.88V0z'
+        fill='#ecedef'
+      />
+      <circle cx={6.7} cy={7.04} r={3.54} fill='#fff' />
+      <path
+        d='M18.12 6.39h-5.87c-.6 0-1.09-.45-1.09-1s.49-1 1.09-1h5.87c.6 0 1.09.45 1.09 1s-.49 1-1.09 1zM16.55 9.77h-4.24c-.55 0-1-.45-1-1s.45-1 1-1h4.24c.55 0 1 .45 1 1s-.45 1-1 1zM18.32 17.37H4.59c-.69 0-1.25-.47-1.25-1.05s.56-1.05 1.25-1.05h13.73c.69 0 1.25.47 1.25 1.05s-.56 1.05-1.25 1.05zM15.34 21.26h-11c-.55 0-1-.41-1-.91s.45-.91 1-.91h11c.55 0 1 .41 1 .91s-.45.91-1 .91zM16.46 25.57H4.43c-.6 0-1.09-.44-1.09-.98s.49-.98 1.09-.98h12.03c.6 0 1.09.44 1.09.98s-.49.98-1.09.98z'
+        fill='#fff'
+      />
+      <g fill='#c0c4c4'>
+        <rect
+          x={33.36}
+          y={19.73}
+          width={2.75}
+          height={3.42}
+          rx={0.33}
+          ry={0.33}
+          opacity={0.32}
+        />
+        <rect
+          x={29.64}
+          y={16.57}
+          width={2.75}
+          height={6.58}
+          rx={0.33}
+          ry={0.33}
+          opacity={0.44}
+        />
+        <rect
+          x={37.16}
+          y={14.44}
+          width={2.75}
+          height={8.7}
+          rx={0.33}
+          ry={0.33}
+          opacity={0.53}
+        />
+        <rect
+          x={41.19}
+          y={10.75}
+          width={2.75}
+          height={12.4}
+          rx={0.33}
+          ry={0.33}
+          opacity={0.53}
+        />
+      </g>
+      <circle cx={62.74} cy={16.32} r={8} fill='#fff' />
+      <g fill='#d9d9d9'>
+        <path d='M63.62 15.82L67 10.15c.93.64 1.7 1.48 2.26 2.47.56.98.89 2.08.96 3.21h-6.6z' />
+        <path d='M67.14 10.88a6.977 6.977 0 012.52 4.44h-5.17l2.65-4.44m-.31-1.43l-4.1 6.87h8c0-1.39-.36-2.75-1.04-3.95s-1.67-2.21-2.86-2.92z' />
+      </g>
+      <rect
+        x={29.64}
+        y={27.75}
+        width={41.62}
+        height={18.62}
+        rx={1.69}
+        ry={1.69}
+        fill='#fff'
+      />
+    </svg>
+  )
+}

+ 116 - 0
src/assets/custom/icon-theme-system.tsx

@@ -0,0 +1,116 @@
+import { SVGProps } from 'react'
+import { cn } from '@/lib/utils'
+
+export function IconThemeSystem({
+  className,
+  ...props
+}: SVGProps<SVGSVGElement>) {
+  return (
+    <svg
+      data-name='icon-theme-system'
+      xmlns='http://www.w3.org/2000/svg'
+      viewBox='0 0 79.86 51.14'
+      className={cn(
+        'overflow-hidden rounded-[6px]',
+        'stroke-primary fill-primary group-data-[state=unchecked]:stroke-muted-foreground group-data-[state=unchecked]:fill-muted-foreground',
+        className
+      )}
+      {...props}
+    >
+      <path opacity={0.2} d='M0 0.03H22.88V51.17H0z' />
+      <circle
+        cx={6.7}
+        cy={7.04}
+        r={3.54}
+        fill='#fff'
+        opacity={0.8}
+        stroke='#fff'
+        strokeLinecap='round'
+        strokeMiterlimit={10}
+      />
+      <path
+        d='M18.12 6.39h-5.87c-.6 0-1.09-.45-1.09-1s.49-1 1.09-1h5.87c.6 0 1.09.45 1.09 1s-.49 1-1.09 1zM16.55 9.77h-4.24c-.55 0-1-.45-1-1s.45-1 1-1h4.24c.55 0 1 .45 1 1s-.45 1-1 1z'
+        fill='#fff'
+        stroke='none'
+        opacity={0.75}
+      />
+      <path
+        d='M18.32 17.37H4.59c-.69 0-1.25-.47-1.25-1.05s.56-1.05 1.25-1.05h13.73c.69 0 1.25.47 1.25 1.05s-.56 1.05-1.25 1.05z'
+        fill='#fff'
+        stroke='none'
+        opacity={0.72}
+      />
+      <path
+        d='M15.34 21.26h-11c-.55 0-1-.41-1-.91s.45-.91 1-.91h11c.55 0 1 .41 1 .91s-.45.91-1 .91z'
+        fill='#fff'
+        stroke='none'
+        opacity={0.55}
+      />
+      <path
+        d='M16.46 25.57H4.43c-.6 0-1.09-.44-1.09-.98s.49-.98 1.09-.98h12.03c.6 0 1.09.44 1.09.98s-.49.98-1.09.98z'
+        fill='#fff'
+        stroke='none'
+        opacity={0.67}
+      />
+      <rect
+        x={33.36}
+        y={19.73}
+        width={2.75}
+        height={3.42}
+        rx={0.33}
+        ry={0.33}
+        opacity={0.31}
+        stroke='none'
+      />
+      <rect
+        x={29.64}
+        y={16.57}
+        width={2.75}
+        height={6.58}
+        rx={0.33}
+        ry={0.33}
+        opacity={0.4}
+        stroke='none'
+      />
+      <rect
+        x={37.16}
+        y={14.44}
+        width={2.75}
+        height={8.7}
+        rx={0.33}
+        ry={0.33}
+        opacity={0.26}
+        stroke='none'
+      />
+      <rect
+        x={41.19}
+        y={10.75}
+        width={2.75}
+        height={12.4}
+        rx={0.33}
+        ry={0.33}
+        opacity={0.37}
+        stroke='none'
+      />
+      <g>
+        <circle cx={62.74} cy={16.32} r={8} opacity={0.25} />
+        <path
+          d='M62.74 16.32l4.1-6.87c1.19.71 2.18 1.72 2.86 2.92s1.04 2.57 1.04 3.95h-8z'
+          opacity={0.45}
+        />
+      </g>
+      <rect
+        x={29.64}
+        y={27.75}
+        width={41.62}
+        height={18.62}
+        rx={1.69}
+        ry={1.69}
+        opacity={0.3}
+        stroke='none'
+        strokeLinecap='round'
+        strokeMiterlimit={10}
+      />
+    </svg>
+  )
+}

+ 353 - 0
src/components/config-drawer.tsx

@@ -0,0 +1,353 @@
+import { SVGProps } from 'react'
+import { Root as Radio, Item } from '@radix-ui/react-radio-group'
+import { CircleCheck, RotateCcw, Settings } from 'lucide-react'
+import { IconDir } from '@/assets/custom/icon-dir'
+import { IconLayoutCompact } from '@/assets/custom/icon-layout-compact'
+import { IconLayoutDefault } from '@/assets/custom/icon-layout-default'
+import { IconLayoutFull } from '@/assets/custom/icon-layout-full'
+import { IconSidebarFloating } from '@/assets/custom/icon-sidebar-floating'
+import { IconSidebarInset } from '@/assets/custom/icon-sidebar-inset'
+import { IconSidebarSidebar } from '@/assets/custom/icon-sidebar-sidebar'
+import { IconThemeDark } from '@/assets/custom/icon-theme-dark'
+import { IconThemeLight } from '@/assets/custom/icon-theme-light'
+import { IconThemeSystem } from '@/assets/custom/icon-theme-system'
+import { cn } from '@/lib/utils'
+import { useDirection } from '@/context/direction-context'
+import { Collapsible, useLayout } from '@/context/layout-context'
+import { useTheme } from '@/context/theme-context'
+import { Button } from '@/components/ui/button'
+import {
+  Sheet,
+  SheetContent,
+  SheetDescription,
+  SheetFooter,
+  SheetHeader,
+  SheetTitle,
+  SheetTrigger,
+} from '@/components/ui/sheet'
+import { useSidebar } from './ui/sidebar'
+
+export function ConfigDrawer() {
+  const { setOpen } = useSidebar()
+  const { resetDir } = useDirection()
+  const { resetTheme } = useTheme()
+  const { resetLayout } = useLayout()
+
+  const handleReset = () => {
+    setOpen(true)
+    resetDir()
+    resetTheme()
+    resetLayout()
+  }
+
+  return (
+    <Sheet>
+      <SheetTrigger asChild>
+        <Button
+          size='icon'
+          variant='ghost'
+          aria-label='Open theme settings'
+          aria-describedby='config-drawer-description'
+        >
+          <Settings aria-hidden='true' />
+        </Button>
+      </SheetTrigger>
+      <SheetContent className='flex flex-col'>
+        <SheetHeader className='pb-0 text-start'>
+          <SheetTitle>Theme Settings</SheetTitle>
+          <SheetDescription id='config-drawer-description'>
+            Adjust the appearance and layout to suit your preferences.
+          </SheetDescription>
+        </SheetHeader>
+        <div className='space-y-6 overflow-y-auto px-4'>
+          <ThemeConfig />
+          <SidebarConfig />
+          <LayoutConfig />
+          <DirConfig />
+        </div>
+        <SheetFooter className='gap-2'>
+          <Button
+            variant='destructive'
+            onClick={handleReset}
+            aria-label='Reset all settings to default values'
+          >
+            Reset
+          </Button>
+        </SheetFooter>
+      </SheetContent>
+    </Sheet>
+  )
+}
+
+function SectionTitle({
+  title,
+  showReset = false,
+  onReset,
+  className,
+}: {
+  title: string
+  showReset?: boolean
+  onReset?: () => void
+  className?: string
+}) {
+  return (
+    <div
+      className={cn(
+        'text-muted-foreground mb-2 flex items-center gap-2 text-sm font-semibold',
+        className
+      )}
+    >
+      {title}
+      {showReset && onReset && (
+        <Button
+          size='icon'
+          variant='secondary'
+          className='size-4 rounded-full'
+          onClick={onReset}
+        >
+          <RotateCcw className='size-3' />
+        </Button>
+      )}
+    </div>
+  )
+}
+
+function RadioGroupItem({
+  item,
+  isTheme = false,
+}: {
+  item: {
+    value: string
+    label: string
+    icon: (props: SVGProps<SVGSVGElement>) => React.ReactElement
+  }
+  isTheme?: boolean
+}) {
+  return (
+    <Item
+      value={item.value}
+      className={cn('group outline-none', 'transition duration-200 ease-in')}
+      aria-label={`Select ${item.label.toLowerCase()}`}
+      aria-describedby={`${item.value}-description`}
+    >
+      <div
+        className={cn(
+          'ring-border relative rounded-[6px] ring-[1px]',
+          'group-data-[state=checked]:ring-primary group-data-[state=checked]:shadow-2xl',
+          'group-focus-visible:ring-2'
+        )}
+        role='img'
+        aria-hidden='false'
+        aria-label={`${item.label} option preview`}
+      >
+        <CircleCheck
+          className={cn(
+            'fill-primary size-6 stroke-white',
+            'group-data-[state=unchecked]:hidden',
+            'absolute top-0 right-0 translate-x-1/2 -translate-y-1/2'
+          )}
+          aria-hidden='true'
+        />
+        <item.icon
+          className={cn(
+            !isTheme &&
+              'stroke-primary fill-primary group-data-[state=unchecked]:stroke-muted-foreground group-data-[state=unchecked]:fill-muted-foreground'
+          )}
+          aria-hidden='true'
+        />
+      </div>
+      <div
+        className='mt-1 text-xs'
+        id={`${item.value}-description`}
+        aria-live='polite'
+      >
+        {item.label}
+      </div>
+    </Item>
+  )
+}
+
+function ThemeConfig() {
+  const { defaultTheme, theme, setTheme } = useTheme()
+  return (
+    <div>
+      <SectionTitle
+        title='Theme'
+        showReset={theme !== defaultTheme}
+        onReset={() => setTheme(defaultTheme)}
+      />
+      <Radio
+        value={theme}
+        onValueChange={setTheme}
+        className='grid w-full max-w-md grid-cols-3 gap-4'
+        aria-label='Select theme preference'
+        aria-describedby='theme-description'
+      >
+        {[
+          {
+            value: 'system',
+            label: 'System',
+            icon: IconThemeSystem,
+          },
+          {
+            value: 'light',
+            label: 'Light',
+            icon: IconThemeLight,
+          },
+          {
+            value: 'dark',
+            label: 'Dark',
+            icon: IconThemeDark,
+          },
+        ].map((item) => (
+          <RadioGroupItem key={item.value} item={item} isTheme />
+        ))}
+      </Radio>
+      <div id='theme-description' className='sr-only'>
+        Choose between system preference, light mode, or dark mode
+      </div>
+    </div>
+  )
+}
+
+function SidebarConfig() {
+  const { defaultVariant, variant, setVariant } = useLayout()
+  return (
+    <div>
+      <SectionTitle
+        title='Sidebar'
+        showReset={defaultVariant !== variant}
+        onReset={() => setVariant(defaultVariant)}
+      />
+      <Radio
+        value={variant}
+        onValueChange={setVariant}
+        className='grid w-full max-w-md grid-cols-3 gap-4'
+        aria-label='Select sidebar style'
+        aria-describedby='sidebar-description'
+      >
+        {[
+          {
+            value: 'inset',
+            label: 'Inset',
+            icon: IconSidebarInset,
+          },
+          {
+            value: 'floating',
+            label: 'Floating',
+            icon: IconSidebarFloating,
+          },
+          {
+            value: 'sidebar',
+            label: 'Sidebar',
+            icon: IconSidebarSidebar,
+          },
+        ].map((item) => (
+          <RadioGroupItem key={item.value} item={item} />
+        ))}
+      </Radio>
+      <div id='sidebar-description' className='sr-only'>
+        Choose between inset, floating, or standard sidebar layout
+      </div>
+    </div>
+  )
+}
+
+function LayoutConfig() {
+  const { open, setOpen } = useSidebar()
+  const { defaultCollapsible, collapsible, setCollapsible } = useLayout()
+
+  const radioState = open ? 'default' : collapsible
+
+  return (
+    <div>
+      <SectionTitle
+        title='Layout'
+        showReset={radioState !== 'default'}
+        onReset={() => {
+          setOpen(true)
+          setCollapsible(defaultCollapsible)
+        }}
+      />
+      <Radio
+        value={radioState}
+        onValueChange={(v) => {
+          if (v === 'default') {
+            setOpen(true)
+            return
+          }
+          setOpen(false)
+          setCollapsible(v as Collapsible)
+        }}
+        className='grid w-full max-w-md grid-cols-3 gap-4'
+        aria-label='Select layout style'
+        aria-describedby='layout-description'
+      >
+        {[
+          {
+            value: 'default',
+            label: 'Default',
+            icon: IconLayoutDefault,
+          },
+          {
+            value: 'icon',
+            label: 'Compact',
+            icon: IconLayoutCompact,
+          },
+          {
+            value: 'offcanvas',
+            label: 'Full layout',
+            icon: IconLayoutFull,
+          },
+        ].map((item) => (
+          <RadioGroupItem key={item.value} item={item} />
+        ))}
+      </Radio>
+      <div id='layout-description' className='sr-only'>
+        Choose between default expanded, compact icon-only, or full layout mode
+      </div>
+    </div>
+  )
+}
+
+function DirConfig() {
+  const { defaultDir, dir, setDir } = useDirection()
+  return (
+    <div>
+      <SectionTitle
+        title='Direction'
+        showReset={defaultDir !== dir}
+        onReset={() => setDir(defaultDir)}
+      />
+      <Radio
+        value={dir}
+        onValueChange={setDir}
+        className='grid w-full max-w-md grid-cols-3 gap-4'
+        aria-label='Select site direction'
+        aria-describedby='direction-description'
+      >
+        {[
+          {
+            value: 'ltr',
+            label: 'Left to Right',
+            icon: (props: SVGProps<SVGSVGElement>) => (
+              <IconDir dir='ltr' {...props} />
+            ),
+          },
+          {
+            value: 'rtl',
+            label: 'Right to Left',
+            icon: (props: SVGProps<SVGSVGElement>) => (
+              <IconDir dir='rtl' {...props} />
+            ),
+          },
+        ].map((item) => (
+          <RadioGroupItem key={item.value} item={item} />
+        ))}
+      </Radio>
+      <div id='direction-description' className='sr-only'>
+        Choose between left-to-right or right-to-left site direction
+      </div>
+    </div>
+  )
+}

+ 4 - 27
src/components/layout/app-sidebar.tsx

@@ -1,30 +1,7 @@
-import {
-  Sidebar,
-  SidebarContent,
-  SidebarFooter,
-  SidebarHeader,
-  SidebarRail,
-} from '@/components/ui/sidebar'
-import { NavGroup } from '@/components/layout/nav-group'
-import { NavUser } from '@/components/layout/nav-user'
-import { TeamSwitcher } from '@/components/layout/team-switcher'
-import { sidebarData } from './data/sidebar-data'
+import { useLayout } from '@/context/layout-context'
+import { Sidebar } from '@/components/ui/sidebar'
 
 export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
-  return (
-    <Sidebar collapsible='icon' variant='floating' {...props}>
-      <SidebarHeader>
-        <TeamSwitcher teams={sidebarData.teams} />
-      </SidebarHeader>
-      <SidebarContent>
-        {sidebarData.navGroups.map((props) => (
-          <NavGroup key={props.title} {...props} />
-        ))}
-      </SidebarContent>
-      <SidebarFooter>
-        <NavUser user={sidebarData.user} />
-      </SidebarFooter>
-      <SidebarRail />
-    </Sidebar>
-  )
+  const { collapsible, variant } = useLayout()
+  return <Sidebar {...props} collapsible={collapsible} variant={variant} />
 }

+ 43 - 17
src/components/layout/authenticated-layout.tsx

@@ -1,10 +1,22 @@
 import Cookies from 'js-cookie'
 import { Outlet } from '@tanstack/react-router'
 import { cn } from '@/lib/utils'
+import { LayoutProvider } from '@/context/layout-context'
 import { SearchProvider } from '@/context/search-context'
-import { SidebarProvider } from '@/components/ui/sidebar'
+import {
+  SidebarContent,
+  SidebarFooter,
+  SidebarHeader,
+  SidebarInset,
+  SidebarProvider,
+  SidebarRail,
+} from '@/components/ui/sidebar'
 import { AppSidebar } from '@/components/layout/app-sidebar'
 import SkipToMain from '@/components/skip-to-main'
+import { sidebarData } from './data/sidebar-data'
+import { NavGroup } from './nav-group'
+import { NavUser } from './nav-user'
+import { TeamSwitcher } from './team-switcher'
 
 interface Props {
   children?: React.ReactNode
@@ -15,22 +27,36 @@ export function AuthenticatedLayout({ children }: Props) {
   return (
     <SearchProvider>
       <SidebarProvider defaultOpen={defaultOpen}>
-        <SkipToMain />
-        <AppSidebar />
-        <div
-          id='content'
-          className={cn(
-            'ms-auto w-full max-w-full',
-            'peer-data-[state=collapsed]:w-[calc(100%-var(--sidebar-width-icon)-1rem)]',
-            'peer-data-[state=expanded]:w-[calc(100%-var(--sidebar-width))]',
-            'sm:transition-[width] sm:duration-200 sm:ease-linear',
-            'flex h-svh flex-col',
-            'group-data-[scroll-locked=1]/body:h-full',
-            'has-[main.fixed-main]:group-data-[scroll-locked=1]/body:h-svh'
-          )}
-        >
-          {children ? children : <Outlet />}
-        </div>
+        <LayoutProvider>
+          <SkipToMain />
+          <AppSidebar>
+            <SidebarHeader>
+              <TeamSwitcher teams={sidebarData.teams} />
+            </SidebarHeader>
+            <SidebarContent>
+              {sidebarData.navGroups.map((props) => (
+                <NavGroup key={props.title} {...props} />
+              ))}
+            </SidebarContent>
+            <SidebarFooter>
+              <NavUser user={sidebarData.user} />
+            </SidebarFooter>
+            <SidebarRail />
+          </AppSidebar>
+          <SidebarInset
+            className={cn(
+              // If layout is fixed, set the height
+              // to 100svh to prevent overflow
+              'has-[[data-layout=fixed]]:h-svh',
+
+              // If layout is fixed and sidebar is inset,
+              // set the height to 100svh - 1rem (total margins) to prevent overflow
+              'peer-data-[variant=inset]:has-[[data-layout=fixed]]:h-[calc(100svh-1rem)]'
+            )}
+          >
+            {children ?? <Outlet />}
+          </SidebarInset>
+        </LayoutProvider>
       </SidebarProvider>
     </SearchProvider>
   )

+ 15 - 6
src/components/layout/header.tsx

@@ -31,16 +31,25 @@ export const Header = ({
   return (
     <header
       className={cn(
-        'bg-background flex h-16 items-center gap-3 p-4 sm:gap-4',
-        fixed && 'header-fixed peer/header fixed z-50 w-[inherit] rounded-md',
-        offset > 10 && fixed ? 'shadow-sm' : 'shadow-none',
+        'z-50 h-16',
+        fixed && 'header-fixed peer/header sticky top-0 w-[inherit]',
+        offset > 10 && fixed ? 'shadow' : 'shadow-none',
         className
       )}
       {...props}
     >
-      <SidebarTrigger variant='outline' className='scale-125 sm:scale-100' />
-      <Separator orientation='vertical' className='h-6' />
-      {children}
+      <div
+        className={cn(
+          'relative flex h-full items-center gap-3 p-4 sm:gap-4',
+          offset > 10 &&
+            fixed &&
+            'after:bg-background/20 after:absolute after:inset-0 after:-z-10 after:backdrop-blur-lg'
+        )}
+      >
+        <SidebarTrigger variant='outline' className='scale-125 sm:scale-100' />
+        <Separator orientation='vertical' className='h-6' />
+        {children}
+      </div>
     </header>
   )
 }

+ 2 - 2
src/components/layout/main.tsx

@@ -9,10 +9,10 @@ interface MainProps extends React.HTMLAttributes<HTMLElement> {
 export const Main = ({ fixed, className, ...props }: MainProps) => {
   return (
     <main
+      data-layout={fixed ? 'fixed' : 'auto'}
       className={cn(
-        'peer-[.header-fixed]/header:mt-16',
         'px-4 py-6',
-        fixed && 'fixed-main flex grow flex-col overflow-hidden',
+        fixed && 'flex grow flex-col overflow-hidden',
         className
       )}
       {...props}

+ 1 - 1
src/components/ui/separator.tsx

@@ -14,7 +14,7 @@ function Separator({
       decorative={decorative}
       orientation={orientation}
       className={cn(
-        'bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px',
+        'bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:w-px',
         className
       )}
       {...props}

+ 21 - 6
src/context/direction-context.tsx

@@ -1,19 +1,27 @@
 import { createContext, useContext, useEffect, useState } from 'react'
 import { DirectionProvider as RdxDirProvider } from '@radix-ui/react-direction'
 
-type Direction = 'ltr' | 'rtl'
+export type Direction = 'ltr' | 'rtl'
 
-const DirectionContext = createContext<{
+const DEFAULT_DIRECTION = 'ltr'
+
+interface DirectionContextType {
+  defaultDir: Direction
   dir: Direction
   setDir: (dir: Direction) => void
-}>({
-  dir: 'ltr',
+  resetDir: () => void
+}
+
+const DirectionContext = createContext<DirectionContextType>({
+  defaultDir: DEFAULT_DIRECTION,
+  dir: DEFAULT_DIRECTION,
   setDir: () => {},
+  resetDir: () => {},
 })
 
 export function DirectionProvider({ children }: { children: React.ReactNode }) {
   const [dir, _setDir] = useState<Direction>(
-    () => (localStorage.getItem('dir') as Direction) || 'ltr'
+    () => (localStorage.getItem('dir') as Direction) || DEFAULT_DIRECTION
   )
 
   useEffect(() => {
@@ -26,8 +34,15 @@ export function DirectionProvider({ children }: { children: React.ReactNode }) {
     localStorage.setItem('dir', dir)
   }
 
+  const resetDir = () => {
+    _setDir(DEFAULT_DIRECTION)
+    localStorage.removeItem('dir')
+  }
+
   return (
-    <DirectionContext value={{ dir, setDir }}>
+    <DirectionContext
+      value={{ defaultDir: DEFAULT_DIRECTION, dir, setDir, resetDir }}
+    >
       <RdxDirProvider dir={dir}>{children}</RdxDirProvider>
     </DirectionContext>
   )

+ 97 - 0
src/context/layout-context.tsx

@@ -0,0 +1,97 @@
+import { createContext, useContext, useEffect, useState } from 'react'
+import Cookies from 'js-cookie'
+
+export type Collapsible = 'offcanvas' | 'icon' | 'none'
+export type Variant = 'inset' | 'sidebar' | 'floating'
+
+// Cookie constants following the pattern from sidebar.tsx
+const LAYOUT_COLLAPSIBLE_COOKIE_NAME = 'layout_collapsible'
+const LAYOUT_VARIANT_COOKIE_NAME = 'layout_variant'
+const LAYOUT_COOKIE_MAX_AGE = 60 * 60 * 24 * 7 // 7 days
+
+// Default values
+const DEFAULT_VARIANT = 'inset'
+const DEFAULT_COLLAPSIBLE = 'icon'
+
+interface LayoutContextType {
+  resetLayout: () => void
+
+  defaultCollapsible: Collapsible
+  collapsible: Collapsible
+  setCollapsible: (collapsible: Collapsible) => void
+
+  defaultVariant: Variant
+  variant: Variant
+  setVariant: (variant: Variant) => void
+}
+
+const LayoutContext = createContext<LayoutContextType>({
+  resetLayout: () => {},
+  defaultCollapsible: DEFAULT_COLLAPSIBLE,
+  collapsible: DEFAULT_COLLAPSIBLE,
+  setCollapsible: () => {},
+
+  defaultVariant: DEFAULT_VARIANT,
+  variant: DEFAULT_VARIANT,
+  setVariant: () => {},
+})
+
+interface LayoutProviderProps {
+  children: React.ReactNode
+}
+
+// Helper function to set cookie
+function setCookie(name: string, value: string): void {
+  document.cookie = `${name}=${value}; path=/; max-age=${LAYOUT_COOKIE_MAX_AGE}`
+}
+
+export function LayoutProvider({ children }: LayoutProviderProps) {
+  const [collapsible, setCollapsible] = useState<Collapsible>(
+    () =>
+      (Cookies.get(LAYOUT_COLLAPSIBLE_COOKIE_NAME) as Collapsible) ||
+      DEFAULT_COLLAPSIBLE
+  )
+  const [variant, setVariant] = useState<Variant>(
+    () =>
+      (Cookies.get(LAYOUT_VARIANT_COOKIE_NAME) as Variant) || DEFAULT_VARIANT
+  )
+
+  useEffect(() => {
+    setCookie(LAYOUT_COLLAPSIBLE_COOKIE_NAME, collapsible)
+  }, [collapsible])
+
+  useEffect(() => {
+    setCookie(LAYOUT_VARIANT_COOKIE_NAME, variant)
+  }, [variant])
+
+  const resetLayout = () => {
+    setCollapsible(DEFAULT_COLLAPSIBLE)
+    setVariant(DEFAULT_VARIANT)
+  }
+
+  return (
+    <LayoutContext.Provider
+      value={{
+        defaultCollapsible: DEFAULT_COLLAPSIBLE,
+        defaultVariant: DEFAULT_VARIANT,
+        resetLayout,
+        collapsible,
+        setCollapsible,
+        variant,
+        setVariant,
+      }}
+    >
+      {children}
+    </LayoutContext.Provider>
+  )
+}
+
+// Define the hook for the provider
+// eslint-disable-next-line react-refresh/only-export-components
+export function useLayout() {
+  const context = useContext(LayoutContext)
+  if (!context) {
+    throw new Error('useLayout must be used within a LayoutProvider')
+  }
+  return context
+}

+ 36 - 10
src/context/theme-context.tsx

@@ -1,6 +1,9 @@
-import { createContext, useContext, useEffect, useState } from 'react'
+import { createContext, useContext, useEffect, useState, useMemo } from 'react'
 
 type Theme = 'dark' | 'light' | 'system'
+type ResolvedTheme = Exclude<Theme, 'system'>
+
+const DEFAULT_THEME = 'system'
 
 type ThemeProviderProps = {
   children: React.ReactNode
@@ -9,20 +12,26 @@ type ThemeProviderProps = {
 }
 
 type ThemeProviderState = {
+  defaultTheme: Theme
+  resolvedTheme: ResolvedTheme
   theme: Theme
   setTheme: (theme: Theme) => void
+  resetTheme: () => void
 }
 
 const initialState: ThemeProviderState = {
-  theme: 'system',
+  defaultTheme: DEFAULT_THEME,
+  resolvedTheme: 'light',
+  theme: DEFAULT_THEME,
   setTheme: () => null,
+  resetTheme: () => {},
 }
 
 const ThemeProviderContext = createContext<ThemeProviderState>(initialState)
 
 export function ThemeProvider({
   children,
-  defaultTheme = 'system',
+  defaultTheme = DEFAULT_THEME,
   storageKey = 'vite-ui-theme',
   ...props
 }: ThemeProviderProps) {
@@ -30,36 +39,53 @@ export function ThemeProvider({
     () => (localStorage.getItem(storageKey) as Theme) || defaultTheme
   )
 
+  // Optimized: Memoize the resolved theme calculation to prevent unnecessary re-computations
+  const resolvedTheme = useMemo((): ResolvedTheme => {
+    if (theme === 'system') {
+      return window.matchMedia('(prefers-color-scheme: dark)').matches
+        ? 'dark'
+        : 'light'
+    }
+    return theme as ResolvedTheme
+  }, [theme])
+
   useEffect(() => {
     const root = window.document.documentElement
     const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
 
-    const applyTheme = (theme: Theme) => {
+    const applyTheme = (currentResolvedTheme: ResolvedTheme) => {
       root.classList.remove('light', 'dark') // Remove existing theme classes
-      const systemTheme = mediaQuery.matches ? 'dark' : 'light'
-      const effectiveTheme = theme === 'system' ? systemTheme : theme
-      root.classList.add(effectiveTheme) // Add the new theme class
+      root.classList.add(currentResolvedTheme) // Add the new theme class
     }
 
     const handleChange = () => {
       if (theme === 'system') {
-        applyTheme('system')
+        const systemTheme = mediaQuery.matches ? 'dark' : 'light'
+        applyTheme(systemTheme)
       }
     }
 
-    applyTheme(theme)
+    applyTheme(resolvedTheme)
 
     mediaQuery.addEventListener('change', handleChange)
 
     return () => mediaQuery.removeEventListener('change', handleChange)
-  }, [theme])
+  }, [theme, resolvedTheme])
 
   const setTheme = (theme: Theme) => {
     localStorage.setItem(storageKey, theme)
     _setTheme(theme)
   }
 
+  const resetTheme = () => {
+    localStorage.removeItem(storageKey)
+    _setTheme(DEFAULT_THEME)
+  }
+
   const value = {
+    defaultTheme,
+    resolvedTheme,
+    resetTheme,
     theme,
     setTheme,
   }

+ 2 - 0
src/features/tasks/index.tsx

@@ -1,3 +1,4 @@
+import { ConfigDrawer } from '@/components/config-drawer'
 import { DirSwitch } from '@/components/dir-switch'
 import { Header } from '@/components/layout/header'
 import { Main } from '@/components/layout/main'
@@ -19,6 +20,7 @@ export default function Tasks() {
         <div className='ms-auto flex items-center space-x-4'>
           <ThemeSwitch />
           <DirSwitch />
+          <ConfigDrawer />
           <ProfileDropdown />
         </div>
       </Header>

+ 1 - 1
src/main.tsx

@@ -91,7 +91,7 @@ if (!rootElement.innerHTML) {
   root.render(
     <StrictMode>
       <QueryClientProvider client={queryClient}>
-        <ThemeProvider defaultTheme='light' storageKey='vite-ui-theme'>
+        <ThemeProvider>
           <FontProvider>
             <DirectionProvider>
               <RouterProvider router={router} />

+ 7 - 1
src/styles/index.css

@@ -14,9 +14,15 @@
     @apply overflow-x-hidden;
   }
   body {
-    @apply bg-background text-foreground min-h-svh w-full;
+    @apply bg-background text-foreground has-[div[data-variant='inset']]:bg-sidebar min-h-svh w-full;
   }
 
+  /* Override Radix scroll locking for sticky headers */
+  body[data-scroll-locked] {
+    overflow: unset !important;
+  }
+
+  /* Cursor pointer for buttons */
   button:not(:disabled),
   [role='button']:not(:disabled) {
     cursor: pointer;