2 Commits

Author SHA1 Message Date
c24f9e5508 docs(uat): annotate the two Bucket 1 layout fixes as SHIPPED in 2f1e1b5
Some checks failed
Build & Push Docker Images / lint (push) Has been cancelled
Build & Push Docker Images / build-and-push (push) Has been cancelled
PageHeader stack point + tablet topbar trigger fixes verified via
Playwright re-screenshot at 768 + 1024.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 14:04:05 +02:00
2f1e1b5f3f 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>
2026-05-22 14:02:57 +02:00
3 changed files with 26 additions and 13 deletions

View File

@@ -23,8 +23,8 @@ _Copy tweaks, alignment, single-prop edits, obvious typos._
> **[Captured 2026-05-22 — visual breakpoint audit, 5 viewports × 20 surfaces via Playwright MCP. Screenshots local at `tmp/visual-audit-2026-05-22/<surface>/<viewport>.png`. Tablet tier (768-1023) infrastructure + dashboard PageHeader stacking shipped in `6d665d0`; findings below are the residue surfaced after that ship lit up the tier.]** > **[Captured 2026-05-22 — visual breakpoint audit, 5 viewports × 20 surfaces via Playwright MCP. Screenshots local at `tmp/visual-audit-2026-05-22/<surface>/<viewport>.png`. Tablet tier (768-1023) infrastructure + dashboard PageHeader stacking shipped in `6d665d0`; findings below are the residue surfaced after that ship lit up the tier.]**
> >
> - **Tablet topbar logo trigger doesn't render visibly**_src/components/layout/app-shell.tsx_ + _src/components/layout/topbar.tsx_ — AppShell mounts a logo button in Topbar's `leadingSlot` prop on tablet (the design intent: click logo → sidebar Sheet slides in). Live render at 768 shows zero affordance in the topbar's left column — only the truncated search-input placeholder is visible. Two likely causes (do both): (a) the topbar's center column is `minmax(420px, 800px)` which starves the left column to ~100px at 768 viewport width with no sidebar; (b) the search container has an unconditional `sm:-translate-x-[calc(var(--width-sidebar)/2)]` that shifts it 128px LEFT to visually-center against a sidebar that doesn't exist on tablet, pulling the search input over the leading-slot. Fix: (a) change center column to `minmax(280px, 600px)` AND (b) gate the translate to `lg:` so it only kicks in when the sidebar is actually present. ~30 min. Captured 2026-05-22 from visual audit. > - **SHIPPED in 2f1e1b5:** Tablet topbar logo trigger doesn't render visibly — center grid column changed to `minmax(280px, 800px)` at base with `lg:` override back to 420px min; search-container `sm:-translate-x-...` gated to `lg:` so it only kicks in when sidebar is inline. Verified at 768 — hamburger affordance visible top-left.
> - **Dashboard title strip crushed at 1024 viewport**_src/components/shared/page-header.tsx_ — at exactly 1024 the desktop shell mounts (sidebar takes 256px) AND `PageHeader`'s `lg:flex-row` kicks in, forcing the title cell to compete with the four-button action row (Today/7d/30d/90d/Custom + Export PDF + Rearrange + Customize). Title degrades to `(` and the "Last 30 days" subtitle wraps three-deep ("Last / 30 / days"). Move the horizontal-stack breakpoint from `lg:` to `xl:` so the strip stacks until 1280px, giving the actions row room to wrap without crushing the title at the tightest sidebar-present width. ~15 min. Captured 2026-05-22. > - **SHIPPED in 2f1e1b5:** Dashboard title strip crushed at 1024 viewport — PageHeader horizontal-stack breakpoint moved from `lg:` (1024) to `xl:` (1280) so the strip stays stacked through tablet AND narrowest desktop width. Verified at 1024 — title reads cleanly with action row stacked below.
> - **`useIsMobile()` returns `true` on tablet — call-site audit needed** — _src/hooks/use-is-mobile.ts_ + every component currently importing `useIsMobile` — the new `useViewportTier()` is the precise hook; `useIsMobile` was kept as `tier !== 'desktop'` for back-compat, which is what most call sites want ("show short stage labels", "stack vertically"). But a handful intend strict mobile-only behaviour (e.g. drawer-instead-of-popover, bottom-sheet picker) and should migrate to `useViewportTier() === 'mobile'`. Grep `useIsMobile` and triage each per actual intent. ~30 min for the audit + targeted migrations. Captured 2026-05-22. > - **`useIsMobile()` returns `true` on tablet — call-site audit needed** — _src/hooks/use-is-mobile.ts_ + every component currently importing `useIsMobile` — the new `useViewportTier()` is the precise hook; `useIsMobile` was kept as `tier !== 'desktop'` for back-compat, which is what most call sites want ("show short stage labels", "stack vertically"). But a handful intend strict mobile-only behaviour (e.g. drawer-instead-of-popover, bottom-sheet picker) and should migrate to `useViewportTier() === 'mobile'`. Grep `useIsMobile` and triage each per actual intent. ~30 min for the audit + targeted migrations. Captured 2026-05-22.
> **Outstanding quick-fixes (rapid UAT capture — not yet shipped):** > **Outstanding quick-fixes (rapid UAT capture — not yet shipped):**

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>
</> </>