From 2ec76c7dec079bd8267eafeaaac7377d6ba94a74 Mon Sep 17 00:00:00 2001 From: dwindown Date: Thu, 6 Nov 2025 15:34:00 +0700 Subject: [PATCH] refactor: Move page header outside content container using context MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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:
← Sticky outside scroll ← Sticky outside scroll ✅
← Only content scrolls 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: ┌─────────────────────────────────────┐ │
│ │ ┌─────────────────────────────┐ │ │ │ SubmenuBar (sticky) │ │ ← justify-start │ ├─────────────────────────────┤ │ │ │ PageHeader (sticky) │ │ ← max-w-5xl centered │ ├─────────────────────────────┤ │ │ │
│ │ │ │ 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 --- admin-spa/src/App.tsx | 13 ++++- admin-spa/src/components/PageHeader.tsx | 19 +++++++ admin-spa/src/contexts/PageHeaderContext.tsx | 39 +++++++++++++ .../Settings/components/SettingsLayout.tsx | 55 ++++++++++--------- 4 files changed, 98 insertions(+), 28 deletions(-) create mode 100644 admin-spa/src/components/PageHeader.tsx create mode 100644 admin-spa/src/contexts/PageHeaderContext.tsx diff --git a/admin-spa/src/App.tsx b/admin-spa/src/App.tsx index 40ca763..b3506ae 100644 --- a/admin-spa/src/App.tsx +++ b/admin-spa/src/App.tsx @@ -29,6 +29,8 @@ import { useCommandStore } from "@/lib/useCommandStore"; import SubmenuBar from './components/nav/SubmenuBar'; import DashboardSubmenuBar from './components/nav/DashboardSubmenuBar'; import { DashboardProvider } from '@/contexts/DashboardContext'; +import { PageHeaderProvider } from '@/contexts/PageHeaderContext'; +import { PageHeader } from '@/components/PageHeader'; import { useActiveSection } from '@/hooks/useActiveSection'; import { NAV_TREE_VERSION } from '@/nav/tree'; import { __ } from '@/lib/i18n'; @@ -420,6 +422,7 @@ function Shell() { ) : ( )} +
@@ -434,6 +437,7 @@ function Shell() { )}
+
@@ -448,6 +452,7 @@ function Shell() { ) : ( )} +
@@ -505,9 +510,11 @@ function AuthWrapper() { } return ( - - - + + + + + ); } diff --git a/admin-spa/src/components/PageHeader.tsx b/admin-spa/src/components/PageHeader.tsx new file mode 100644 index 0000000..6db3e99 --- /dev/null +++ b/admin-spa/src/components/PageHeader.tsx @@ -0,0 +1,19 @@ +import React from 'react'; +import { usePageHeader } from '@/contexts/PageHeaderContext'; + +export function PageHeader() { + const { title, action } = usePageHeader(); + + if (!title) return null; + + return ( +
+
+
+

{title}

+
+ {action &&
{action}
} +
+
+ ); +} diff --git a/admin-spa/src/contexts/PageHeaderContext.tsx b/admin-spa/src/contexts/PageHeaderContext.tsx new file mode 100644 index 0000000..041e5ef --- /dev/null +++ b/admin-spa/src/contexts/PageHeaderContext.tsx @@ -0,0 +1,39 @@ +import React, { createContext, useContext, useState, ReactNode } from 'react'; + +interface PageHeaderContextType { + title: string | null; + action: ReactNode | null; + setPageHeader: (title: string | null, action?: ReactNode) => void; + clearPageHeader: () => void; +} + +const PageHeaderContext = createContext(undefined); + +export function PageHeaderProvider({ children }: { children: ReactNode }) { + const [title, setTitle] = useState(null); + const [action, setAction] = useState(null); + + const setPageHeader = (newTitle: string | null, newAction?: ReactNode) => { + setTitle(newTitle); + setAction(newAction || null); + }; + + const clearPageHeader = () => { + setTitle(null); + setAction(null); + }; + + return ( + + {children} + + ); +} + +export function usePageHeader() { + const context = useContext(PageHeaderContext); + if (!context) { + throw new Error('usePageHeader must be used within PageHeaderProvider'); + } + return context; +} diff --git a/admin-spa/src/routes/Settings/components/SettingsLayout.tsx b/admin-spa/src/routes/Settings/components/SettingsLayout.tsx index a771052..1cf53bb 100644 --- a/admin-spa/src/routes/Settings/components/SettingsLayout.tsx +++ b/admin-spa/src/routes/Settings/components/SettingsLayout.tsx @@ -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, + + ); + } else { + clearPageHeader(); + } + + return () => clearPageHeader(); + }, [title, onSave, isSaving, isLoading, saveLabel]); + return (
- {/* Sticky Header with Save Button - Edge to edge */} - {onSave && ( -
-
-
-

{title}

-
- -
-
- )} {/* Content */}