From a75d4f5d697240de78e72e2b507bb2dc8aa1b168 Mon Sep 17 00:00:00 2001 From: Matt Ciaccio Date: Fri, 1 May 2026 16:34:28 +0200 Subject: [PATCH] feat(mobile): redesign topbar + collapse cumbersome page-header on mobile MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Topbar (mobile-topbar.tsx): - Bumped to 56px to match standard mobile-app proportions. - Deep-navy gradient surface (#1e2844 -> #171f35) with white type — matches the desktop sidebar identity, gives the app a premium finish instead of generic white-with-text. - Brand "PN" wordmark mark on the left when no back affordance is needed (rounded brand-blue square, inset highlight + drop shadow). - Soft glow shadow underneath for elevation depth instead of a hard bottom border. - White-on-navy back arrow with active-state translucent fill. PageHeader (page-header.tsx): - On mobile, the gradient hero strip + duplicate title + description block now collapses entirely — the topbar already shows the title, so duplicating it in the body wasted a third of the viewport. - The actions slot remains rendered as a flush right-aligned row so primary buttons (date-range pickers, "+ New X") stay accessible. - Desktop rendering is untouched. Mobile shell (mobile-layout.tsx): - Top buffer 16px below the topbar so content doesn't ride flush. - Bottom buffer 32px above the tab bar so the last card breathes. CSS (globals.css): - Hide the react-query-devtools floating button below lg: — it was overlapping the bottom-tab bar's "More" affordance. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/app/globals.css | 12 +++ .../layout/mobile/mobile-layout.tsx | 11 ++- .../layout/mobile/mobile-topbar.tsx | 46 ++++++++--- src/components/shared/page-header.tsx | 79 +++++++++++-------- 4 files changed, 103 insertions(+), 45 deletions(-) diff --git a/src/app/globals.css b/src/app/globals.css index e89da2c..b9e5b5e 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -157,3 +157,15 @@ body[data-form-factor='mobile'] [data-shell='desktop'] { body[data-form-factor='mobile'] [data-shell='mobile'] { display: block; } + +/* + * React Query Devtools floating button collides with the bottom tab bar's + * "More" tab on mobile. The devtools panel itself remains accessible from + * desktop where the toggle is positioned out of the way of any UI. + */ +@media (max-width: 1023.98px) { + .tsqd-open-btn-container, + .tsqd-parent-container { + display: none !important; + } +} diff --git a/src/components/layout/mobile/mobile-layout.tsx b/src/components/layout/mobile/mobile-layout.tsx index e3b3d2f..2735d7c 100644 --- a/src/components/layout/mobile/mobile-layout.tsx +++ b/src/components/layout/mobile/mobile-layout.tsx @@ -2,6 +2,7 @@ import { useState, type ReactNode } from 'react'; +import { cn } from '@/lib/utils'; import { MobileLayoutProvider } from './mobile-layout-provider'; import { MobileTopbar } from './mobile-topbar'; import { MobileBottomTabs } from './mobile-bottom-tabs'; @@ -21,7 +22,15 @@ export function MobileLayout({ children }: { children: ReactNode }) {
-
+
{children}
setMoreOpen(true)} /> diff --git a/src/components/layout/mobile/mobile-topbar.tsx b/src/components/layout/mobile/mobile-topbar.tsx index f6e5a40..d2514ea 100644 --- a/src/components/layout/mobile/mobile-topbar.tsx +++ b/src/components/layout/mobile/mobile-topbar.tsx @@ -7,16 +7,20 @@ import { cn } from '@/lib/utils'; import { useMobileChrome } from './mobile-layout-provider'; /** - * Fixed compact topbar (52px + safe-area top inset). Renders the page title - * (auto-truncating), an optional back button, and an optional primary action - * — all driven by `useMobileChrome()` from the active page. + * Fixed mobile topbar (56px + safe-area top inset). Marina-editorial premium: + * deep-navy gradient surface with white type, the brand "PN" mark on the + * left when there's no back affordance, and a soft glow shadow underneath + * for depth instead of a hard divider line. + * + * Slots: title (auto-truncating), back arrow, primary action — all driven by + * `useMobileChrome()` from the active page. When no page has set a title the + * URL's last segment is title-cased as a fallback. */ export function MobileTopbar() { const { title, primaryAction, showBackButton } = useMobileChrome(); const router = useRouter(); const pathname = usePathname(); - // Fall back to the last path segment (Title Case) if no page-supplied title. const fallbackTitle = pathname .split('/') @@ -28,8 +32,10 @@ export function MobileTopbar() { return (
@@ -38,19 +44,37 @@ export function MobileTopbar() { type="button" onClick={() => router.back()} aria-label="Go back" - className="-ml-1 size-11 inline-flex items-center justify-center text-foreground" + className={cn( + 'size-11 inline-flex items-center justify-center rounded-full -ml-1', + 'text-white/95 active:bg-white/10 transition-colors', + )} > - + ) : ( -
+
+ PN +
)} -

+

{title ?? fallbackTitle}

-
{primaryAction}
+
+ {primaryAction} +
); } diff --git a/src/components/shared/page-header.tsx b/src/components/shared/page-header.tsx index d3d55a5..b35033f 100644 --- a/src/components/shared/page-header.tsx +++ b/src/components/shared/page-header.tsx @@ -20,9 +20,11 @@ interface PageHeaderProps { * landing pages and detail headers; the plain variant remains the default so * existing call-sites stay unchanged. * - * Mobile-aware: below sm (640px) the header stacks vertically (title above - * actions) instead of side-by-side, and the description hides when actions - * are present (to preserve scroll real estate). Title font scales down too. + * Mobile-aware: below sm (640px) the title/eyebrow/description/gradient + * frame all collapse — the page title is already shown by the mobile topbar, + * so duplicating it in the body wastes scroll real estate. What remains is a + * flush right-aligned action row (or nothing if there are no actions). On sm+ + * the full strip with title+description renders as before. */ export function PageHeader({ title, @@ -35,37 +37,48 @@ export function PageHeader({ }: PageHeaderProps) { const isGradient = variant === 'gradient'; return ( -
-
- {eyebrow ? ( -
- {eyebrow} -
- ) : null} -

- {title} -

- {description ? ( -

- {description} -

- ) : null} - {kpiLine ? ( -
- {kpiLine} -
+ <> + {/* Mobile: actions only. Title/description are duplicated by the topbar. */} + {actions ? ( +
+ {actions} +
+ ) : null} + + {/* Desktop: full strip with title, eyebrow, description, kpi line, actions. */} +
for accessibility / SEO so screen + // readers still get a heading on mobile from the topbar's

. + aria-hidden={undefined} + > +
+ {eyebrow ? ( +
+ {eyebrow} +
+ ) : null} +

+ {title} +

+ {description ? ( +

{description}

+ ) : null} + {kpiLine ? ( +
+ {kpiLine} +
+ ) : null} +
+ {actions ? ( +
{actions}
) : null}

- {actions ? ( -
{actions}
- ) : null} -
+ ); }