diff --git a/docs/release-control/v6/internal/subsystems/frontend-primitives.md b/docs/release-control/v6/internal/subsystems/frontend-primitives.md index fd73a5370c..96f1789562 100644 --- a/docs/release-control/v6/internal/subsystems/frontend-primitives.md +++ b/docs/release-control/v6/internal/subsystems/frontend-primitives.md @@ -5770,7 +5770,15 @@ the surface-alt detail row shell locally. The content shell must clip horizontal paint below the large breakpoint without becoming a scroll container, reset the parent table's `whitespace-nowrap` inheritance, and allow its descendants to shrink, then restore visible overflow for the static -desktop layout. Long operator-state copy must wrap inside the shared row border +desktop layout. Below that breakpoint the default content shell is capped at +the table scroll shell's content-box inline size (`max-w-[100cqi]`; the shell +is a size container), never a viewport estimate of the page chrome, so a phone +drawer does not leave an empty strip beside its content (the retired +`100vw-3.5rem` cap left 27px at 390px) and is never wider than the table's +visible area. Callers that pass their own `contentClass` own their width. +Fixed-layout cells clip overflow, so the sticky offset is inert and horizontal +scrolling still moves the drawer with its cell. +Long operator-state copy must wrap inside the shared row border instead of painting beneath adjacent controls or disappearing at the clip edge. When focused detail content is removed, `InlineDetailTableRow` restores focus to its current `aria-controls` disclosure with `preventScroll`; live refresh, diff --git a/frontend-modern/browser-verification/52bccba8cf17119d.json b/frontend-modern/browser-verification/52bccba8cf17119d.json new file mode 100644 index 0000000000..d8eeeef9fe --- /dev/null +++ b/frontend-modern/browser-verification/52bccba8cf17119d.json @@ -0,0 +1,52 @@ +{ + "version": 1, + "base_sha": "7eb32f318100fef045d63414fd63304c62acfdef", + "verified_at": "2026-10-07T16:55:00Z", + "result": "passed", + "changed_paths": [ + "frontend-modern/src/components/shared/InlineDetailTableRow.tsx" + ], + "content_sha256": { + "frontend-modern/src/components/shared/InlineDetailTableRow.tsx": "e5b27178cb3268132595943058849b3f619da22fcbd7916dfc301d4e852ef244" + }, + "routes": [ + "/standalone/availability", + "/standalone/machines", + "/proxmox", + "/docker", + "/kubernetes" + ], + "viewports": [ + { + "width": 1440, + "height": 900 + }, + { + "width": 1000, + "height": 800 + }, + { + "width": 768, + "height": 1024 + }, + { + "width": 390, + "height": 844 + }, + { + "width": 320, + "height": 700 + } + ], + "states": [ + "Private stacks served by Vite from this tree (Playwright 1.56.1 headless Chromium): a non-mock stack with one HTTP availability check, then a mock estate (3 Proxmox nodes, 2 Docker hosts, 1 Kubernetes cluster, 2 generic hosts).", + "Before (origin/main class max-w-[calc(100vw-3.5rem)]): at 390px every opened drawer (availability check, Proxmox node, Proxmox guest, Docker host, Docker container, Kubernetes node, Machines agent) was 334px inside a 361px visible table, leaving a 27px empty strip; at 768px a table forced wider than its shell got a 712px drawer in a 695px visible area.", + "After (max-w-[100cqi]): drawer content equals the scroll shell's visible width at 320 (291px), 390 (361px), 768 (695px) and 1000 (916px) with no right gap, including the forced-wider table; 1440px keeps the static lg layout (max-width none, 1320px). The availability drawer's hostname now shows in full instead of truncating.", + "Long unwrapped identity chips (e.g. proxmox-lxc-docker:Production West:pve1:107) were already clipped at the content edge before (Kubernetes 20px, Docker 8px and 83px) and are clipped less after (7px, 70px); chip wrapping is a separate follow-up.", + "Sticky positioning is inert in both versions because .table-fixed td sets overflow hidden; not changed here. This is not an installed-build, physical-phone or release acceptance." + ], + "interactions": [ + "Opened each drawer by its disclosure button (row tap at phone width where the button is hidden), measured the scroll shell, detail cell, content shell and every descendant's right edge, and captured screenshots at 390px before and after.", + "Forced a 900px table inside its shell at 390px and 768px and scrolled it 300px sideways to compare drawer width against the visible area for both classes." + ] +} diff --git a/frontend-modern/src/components/shared/InlineDetailTableRow.tsx b/frontend-modern/src/components/shared/InlineDetailTableRow.tsx index 1b7f479484..e8e46af75a 100644 --- a/frontend-modern/src/components/shared/InlineDetailTableRow.tsx +++ b/frontend-modern/src/components/shared/InlineDetailTableRow.tsx @@ -5,9 +5,12 @@ import { TableCell, TableRow, type TableRowProps } from './Table'; export const INLINE_DETAIL_TABLE_CELL_CLASS = 'p-0 border-b border-border bg-surface-alt'; // overflow-x-clip keeps over-wide content from painting outside the row's // border below the lg breakpoint (issue #1622) without creating a scroll -// container the way overflow-hidden would. +// container the way overflow-hidden would. Below lg the sticky content is as +// wide as the table's visible area: 100cqi resolves against the table scroll +// shell (a size container), so a guess at the page chrome cannot leave an +// empty strip beside the drawer or push it past the visible edge. export const INLINE_DETAIL_TABLE_CONTENT_CLASS = - 'sticky left-0 min-w-0 max-w-[calc(100vw-3.5rem)] whitespace-normal overflow-x-clip px-2 py-3 sm:px-4 sm:py-4 lg:static lg:max-w-none lg:overflow-x-visible'; + 'sticky left-0 min-w-0 max-w-[100cqi] whitespace-normal overflow-x-clip px-2 py-3 sm:px-4 sm:py-4 lg:static lg:max-w-none lg:overflow-x-visible'; export interface InlineDetailTableRowProps extends TableRowProps { cellId?: string; diff --git a/frontend-modern/src/components/shared/SharedPrimitives.guardrails.test.ts b/frontend-modern/src/components/shared/SharedPrimitives.guardrails.test.ts index d0f28a031d..4c9b60ba8d 100644 --- a/frontend-modern/src/components/shared/SharedPrimitives.guardrails.test.ts +++ b/frontend-modern/src/components/shared/SharedPrimitives.guardrails.test.ts @@ -8757,7 +8757,10 @@ describe('shared primitive guardrails', () => { expect(summaryRowActionButtonSource).toContain('h-9 w-9'); expect(searchFieldSource).toContain('min-h-11'); expect(filterToolbarSource).toContain('min-h-11'); - expect(inlineDetailTableRowSource).toContain('max-w-[calc(100vw-3.5rem)]'); + // Phone and tablet drawers fill the table's visible width (the scroll + // shell is a size container), not a viewport guess at the page chrome. + expect(inlineDetailTableRowSource).toContain('max-w-[100cqi]'); + expect(inlineDetailTableRowSource).not.toContain('100vw-3.5rem'); }); it('keeps shared settings panels dense without forking their desktop framing', () => { diff --git a/frontend-modern/src/components/shared/__tests__/InlineDetailTableRow.test.tsx b/frontend-modern/src/components/shared/__tests__/InlineDetailTableRow.test.tsx index 989cb28536..30212d1e7e 100644 --- a/frontend-modern/src/components/shared/__tests__/InlineDetailTableRow.test.tsx +++ b/frontend-modern/src/components/shared/__tests__/InlineDetailTableRow.test.tsx @@ -39,7 +39,7 @@ describe('InlineDetailTableRow', () => { expect(detail.parentElement).toHaveClass('left-0'); expect(detail.parentElement).toHaveClass('min-w-0'); expect(detail.parentElement).toHaveClass('whitespace-normal'); - expect(detail.parentElement).toHaveClass('max-w-[calc(100vw-3.5rem)]'); + expect(detail.parentElement).toHaveClass('max-w-[100cqi]'); expect(detail.parentElement).toHaveClass('lg:static'); expect(detail.parentElement).toHaveClass('lg:max-w-none'); });