From 352b2420b714be991199e9d321bba9499c5eab6b Mon Sep 17 00:00:00 2001 From: Matt Date: Mon, 22 Jun 2026 16:23:56 +0200 Subject: [PATCH] =?UTF-8?q?fix(ui):=20mobile=20cutoff=20polish=20=E2=80=94?= =?UTF-8?q?=20onboarding=20banner=20+=20yacht=20owner=20truncate=20(R1/R2)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Responsive-overflow sweep findings (tests/e2e/matrix/responsive-overflow.spec.ts): - R1: the onboarding banner's verbose "N of M steps done. Next: " was clipped on mobile (extended ~160px past a 390px viewport) and duplicated the always-visible "View checklist" button. Now hidden below sm:; mobile shows just "Setup X% complete" + the checklist button. - R2: yacht card owner subtitle used inline-flex + truncate, so a long owner name overflowed ~11px on the narrowest widths. Switched to flex min-w-0 so it truncates within the card. - Detector: skip SVG internals (icons / the react-grab dev overlay) and elements inside overflow-x scroll containers (data tables scroll on purpose) to drop false positives. Sweep now confirms mobile/tablet clean + no real desktop overflow (berths wide table is the DataTable's intended horizontal scroll). Co-Authored-By: Claude Opus 4.8 (1M context) --- src/components/admin/onboarding-banner.tsx | 33 +++++++++++--------- src/components/yachts/yacht-card.tsx | 6 ++-- tests/e2e/matrix/responsive-overflow.spec.ts | 30 ++++++++++++++++-- 3 files changed, 51 insertions(+), 18 deletions(-) diff --git a/src/components/admin/onboarding-banner.tsx b/src/components/admin/onboarding-banner.tsx index 1fd15205..f987b20b 100644 --- a/src/components/admin/onboarding-banner.tsx +++ b/src/components/admin/onboarding-banner.tsx @@ -50,20 +50,25 @@ export function OnboardingBanner() {
- Setup is {data.percent}% complete. {data.completed} of {data.total} steps - done.{' '} - {next ? ( - <> - Next:{' '} - - {next.label} - - - ) : null} + Setup is {data.percent}% complete + {/* Verbose progress + the "Next:" deep-link are hidden on mobile, + where they get clipped (R1) and duplicate the always-visible + "View checklist" button. Shown from sm: up. */} + + . {data.completed} of {data.total} steps done.{' '} + {next ? ( + <> + Next:{' '} + + {next.label} + + + ) : null} +
diff --git a/src/components/yachts/yacht-card.tsx b/src/components/yachts/yacht-card.tsx index ea81e19f..3d2a61b4 100644 --- a/src/components/yachts/yacht-card.tsx +++ b/src/components/yachts/yacht-card.tsx @@ -102,9 +102,11 @@ export function YachtCard({ yacht, portSlug, onEdit, onArchive }: YachtCardProps
- {/* Owner subtitle */} + {/* Owner subtitle. `flex min-w-0` (not inline-flex) so a long owner + name truncates within the card instead of overflowing ~11px on + the narrowest mobile widths (R2). */} {yacht.currentOwnerName ? ( -

+

{yacht.currentOwnerName}

diff --git a/tests/e2e/matrix/responsive-overflow.spec.ts b/tests/e2e/matrix/responsive-overflow.spec.ts index c8333dc8..8981fb2e 100644 --- a/tests/e2e/matrix/responsive-overflow.spec.ts +++ b/tests/e2e/matrix/responsive-overflow.spec.ts @@ -82,13 +82,39 @@ test.describe('Responsive overflow sweep', () => { // Elements whose right edge runs past the viewport by > 2px and are // actually visible (have size, not display:none). const offscreen: { tag: string; cls: string; right: number; text: string }[] = []; + const SVG_INTERNAL = new Set([ + 'svg', + 'g', + 'ellipse', + 'path', + 'circle', + 'rect', + 'line', + 'polyline', + 'polygon', + ]); const els = document.querySelectorAll('body *'); for (const el of els) { + const tag = el.tagName.toLowerCase(); + // Skip SVG internals (icons, the react-grab dev overlay, chart guts) + // — not layout-cutoff signal. + if (SVG_INTERNAL.has(tag)) continue; const r = (el as HTMLElement).getBoundingClientRect(); if (r.width === 0 || r.height === 0) continue; if (r.right > vpWidth + 2 && r.left < vpWidth) { - // overflowing the right edge (partially clipped) - const tag = el.tagName.toLowerCase(); + // Skip elements inside a horizontal-scroll container (data tables + // etc. scroll on purpose) — that's intended, not a clip. + let p: HTMLElement | null = el.parentElement; + let inScroll = false; + while (p) { + const ox = getComputedStyle(p).overflowX; + if (ox === 'auto' || ox === 'scroll') { + inScroll = true; + break; + } + p = p.parentElement; + } + if (inScroll) continue; const cls = ((el as HTMLElement).className || '').toString().slice(0, 40); const text = (el.textContent || '').trim().slice(0, 30); offscreen.push({ tag, cls, right: Math.round(r.right), text });