refactor: Move page header outside content container using context

Problem:
- Page header inside scrollable content container
- Complex sticky positioning logic
- Different behavior in different modes

Better Architecture:
Move page header to same level as submenu, outside scroll container

Structure:
<main flex flex-col>
  <SubmenuBar sticky>           ← Sticky outside scroll
  <PageHeader sticky>            ← Sticky outside scroll 
  <div overflow-auto>            ← Only content scrolls
    <AppRoutes />

Implementation:
1. PageHeaderContext - Global state for page header
   - title: string
   - action: ReactNode (e.g., Save button)
   - setPageHeader() / clearPageHeader()

2. PageHeader Component - Renders at app level
   - Positioned after submenu
   - Sticky top-[49px] (below submenu)
   - Boxed layout (max-w-5xl, centered)
   - Consumes context

3. SettingsLayout - Sets header via context
   - useEffect to set/clear header
   - No inline sticky header
   - Cleaner component

Benefits:
 Page header outside scroll container
 Sticky works consistently (no mode detection)
 Submenu layout preserved (justify-start)
 Page header uses page layout (boxed, centered)
 Separation of concerns
 Reusable for any page that needs sticky header

Layout Hierarchy:
┌─────────────────────────────────────┐
│ <main flex flex-col>                │
│   ┌─────────────────────────────┐   │
│   │ SubmenuBar (sticky)         │   │ ← justify-start
│   ├─────────────────────────────┤   │
│   │ PageHeader (sticky)         │   │ ← max-w-5xl centered
│   ├─────────────────────────────┤   │
│   │ <div overflow-auto>         │   │
│   │   Content (scrolls)         │   │
│   └─────────────────────────────┘   │
└─────────────────────────────────────┘

Files Created:
- PageHeaderContext.tsx: Context provider
- PageHeader.tsx: Header component

Files Modified:
- App.tsx: Added PageHeader after submenu in all layouts
- SettingsLayout.tsx: Use context instead of inline header

Result:
 Clean architecture
 Consistent sticky behavior
 No mode-specific logic
 Reusable pattern
This commit is contained in:
dwindown
2025-11-06 15:34:00 +07:00
parent 99748ca202
commit 2ec76c7dec
4 changed files with 98 additions and 28 deletions

View File

@@ -1,6 +1,7 @@
import React, { useState } from 'react';
import React, { useState, useEffect } from 'react';
import { Button } from '@/components/ui/button';
import { Loader2 } from 'lucide-react';
import { usePageHeader } from '@/contexts/PageHeaderContext';
interface SettingsLayoutProps {
title: string;
@@ -22,6 +23,7 @@ export function SettingsLayout({
action,
}: SettingsLayoutProps) {
const [isSaving, setIsSaving] = useState(false);
const { setPageHeader, clearPageHeader } = usePageHeader();
const handleSave = async () => {
if (!onSave) return;
@@ -33,32 +35,35 @@ export function SettingsLayout({
}
};
// Set page header when component mounts
useEffect(() => {
if (onSave) {
setPageHeader(
title,
<Button
onClick={handleSave}
disabled={isSaving || isLoading}
size="sm"
>
{isSaving ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Saving...
</>
) : (
saveLabel
)}
</Button>
);
} else {
clearPageHeader();
}
return () => clearPageHeader();
}, [title, onSave, isSaving, isLoading, saveLabel]);
return (
<div className="space-y-6">
{/* Sticky Header with Save Button - Edge to edge */}
{onSave && (
<div className="sticky top-0 z-10 border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60 -mx-4 px-4 mb-6">
<div className="container px-0 max-w-5xl mx-auto py-3 flex items-center justify-between">
<div>
<h1 className="text-lg font-semibold">{title}</h1>
</div>
<Button
onClick={handleSave}
disabled={isSaving || isLoading}
size="sm"
>
{isSaving ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Saving...
</>
) : (
saveLabel
)}
</Button>
</div>
</div>
)}
{/* Content */}
<div className="container px-0 max-w-5xl mx-auto">