fix(layout): unblock tablet topbar trigger + un-crush 1024 dashboard title

Two Bucket 1 quick-fixes from the 2026-05-22 visual audit, both
1-2-line CSS changes with outsized visual impact.

PageHeader stack point: lg → xl
The earlier sm → lg revision (commit 6d665d0) fixed the 768 tablet
crush but introduced a SECOND crush at exactly 1024: that's where
the desktop shell mounts (sidebar = 256px) AND lg:flex-row kicks
in, leaving the title cell to compete with a 4-button action row
in only ~720px of content. Title degraded to "(" and "Last 30
days" wrapped three-deep ("Last / 30 / days"). Moving to xl
(1280) keeps the strip stacked through tablet AND the narrowest
desktop width. Verified via Playwright at 1024 — title now reads
cleanly with the action row stacked below.

Topbar tablet logo trigger:
AppShell mounts a logo button in Topbar's leadingSlot prop on
tablet (the design intent: click logo → sidebar Sheet slides in).
Live screenshot at 768 showed zero affordance — search bar started
at the very left edge of the visible viewport. Two root causes,
both fixed:
- center grid column was minmax(420px, 800px) which starved the
  left column to ~100px at 768 width (no sidebar present).
  Changed to minmax(280px, 800px) at base, minmax(420px, 800px)
  only at lg+.
- search container had unconditional sm:-translate-x-...
  shifting it 128px LEFT to compensate for a sidebar that isn't
  present at tablet, pulling the search input over the leading-
  slot. Gated the translate to lg: so it only kicks in when the
  sidebar is actually inline.

Verified via Playwright at 768 — hamburger icon now appears in
the top-left corner; search bar sits to its right without overlap.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-22 14:02:57 +02:00
parent d0639421bd
commit 2f1e1b5f3f
2 changed files with 24 additions and 11 deletions

View File

@@ -59,7 +59,13 @@ export function Topbar({ ports, user, leadingSlot }: TopbarProps) {
// Three-column grid: breadcrumbs left, search center, actions right. // Three-column grid: breadcrumbs left, search center, actions right.
// The brand logo lives in the sidebar header (per design feedback) so the // The brand logo lives in the sidebar header (per design feedback) so the
// topbar center is dedicated to the global search bar. // topbar center is dedicated to the global search bar.
<header className="grid h-14 grid-cols-[minmax(0,1fr)_minmax(420px,800px)_minmax(0,1fr)] items-center border-b border-border bg-background gap-3 px-4 shrink-0"> //
// Center column min-width: 280 at tablet, 420 at lg+. The earlier
// single 420 min starved the left column to ~100px at 768 viewport
// width (when the sidebar is hidden under a Sheet on tablet) — that
// was hiding the new logo-trigger leadingSlot AppShell mounts. Two
// breakpoints split the difference cleanly.
<header className="grid h-14 grid-cols-[minmax(0,1fr)_minmax(280px,800px)_minmax(0,1fr)] items-center border-b border-border bg-background gap-3 px-4 shrink-0 lg:grid-cols-[minmax(0,1fr)_minmax(420px,800px)_minmax(0,1fr)]">
{/* LEFT: optional sidebar trigger (tablet) + optional back button + breadcrumbs */} {/* LEFT: optional sidebar trigger (tablet) + optional back button + breadcrumbs */}
<div className="min-w-0 flex items-center gap-1.5"> <div className="min-w-0 flex items-center gap-1.5">
{leadingSlot} {leadingSlot}
@@ -85,9 +91,13 @@ export function Topbar({ ports, user, leadingSlot }: TopbarProps) {
translate-X that shifts left by half the sidebar width. translate-X that shifts left by half the sidebar width.
Without the translate the topbar's grid centers inside the Without the translate the topbar's grid centers inside the
area-after-the-sidebar, so the search visually drifts right area-after-the-sidebar, so the search visually drifts right
by half the sidebar width. */} by half the sidebar width.
The translate is gated to `lg:` because at tablet (768-1023)
the sidebar is HIDDEN behind a Sheet — translating left there
shifts the search into the leading-slot column, hiding the
AppShell-mounted logo trigger. */}
<div className="flex items-center justify-center min-w-0"> <div className="flex items-center justify-center min-w-0">
<div className="w-full max-w-2xl mx-auto min-w-0 sm:-translate-x-[calc(var(--width-sidebar)/2)]"> <div className="w-full max-w-2xl mx-auto min-w-0 lg:-translate-x-[calc(var(--width-sidebar)/2)]">
<CommandSearch /> <CommandSearch />
</div> </div>
</div> </div>

View File

@@ -46,18 +46,21 @@ export function PageHeader({
) : null} ) : null}
{/* Tablet + desktop: full strip with title, eyebrow, description, kpi line, actions. {/* Tablet + desktop: full strip with title, eyebrow, description, kpi line, actions.
Stacks vertically at sm (640px) up to lg (1024px) so the title Stacks vertically at sm (640px) up to xl (1280px). The earlier
doesn't get truncated next to a wide actions row on tablet — the revision moved the stack point from sm to lg, which fixed the
previous `sm:flex-row sm:flex-nowrap` forced four-button toolbars tablet (768) crush but introduced a SECOND crush at exactly
(e.g. the dashboard's DateRange + ExportPdf + Rearrange + Customize) 1024: that's where the desktop shell mounts (sidebar = 256px)
onto one row at 768px, crushing the title. At lg+ the row layout AND `lg:flex-row` kicks in, leaving the title cell to compete
returns. */} with a four-button action row in only ~720px of content. Moving
to xl (1280) means the strip stays stacked through tablet AND
the narrowest desktop width; horizontal layout returns once
there's actual room. */}
<div <div
className={cn( className={cn(
// Removed `sm:mb-6` - the parent shell already provides // Removed `sm:mb-6` - the parent shell already provides
// appropriate gap-y between header and the next section, and the // appropriate gap-y between header and the next section, and the
// double-spacing produced an oversized top margin on dashboards. // double-spacing produced an oversized top margin on dashboards.
'hidden sm:flex flex-col gap-3 lg:flex-row lg:items-start lg:justify-between lg:gap-4', 'hidden sm:flex flex-col gap-3 xl:flex-row xl:items-start xl:justify-between xl:gap-4',
isGradient && isGradient &&
'rounded-xl border border-slate-200 bg-gradient-brand-soft px-5 py-4 shadow-xs', 'rounded-xl border border-slate-200 bg-gradient-brand-soft px-5 py-4 shadow-xs',
className, className,
@@ -85,7 +88,7 @@ export function PageHeader({
) : null} ) : null}
</div> </div>
{actions ? ( {actions ? (
<div className="flex shrink-0 flex-wrap items-center gap-2 lg:flex-nowrap">{actions}</div> <div className="flex shrink-0 flex-wrap items-center gap-2 xl:flex-nowrap">{actions}</div>
) : null} ) : null}
</div> </div>
</> </>