From ed5de5d144152a28e4ffcbfc1fea9f99245fb8e3 Mon Sep 17 00:00:00 2001 From: Pulse Test Date: Sat, 29 Aug 2026 16:37:05 +0100 Subject: [PATCH] fix(frontend): align desktop drawer detail cards --- .../subsystems/frontend-primitives.md | 17 +++++--- frontend-modern/browser-verification.json | 33 ++++++--------- .../components/shared/DetailSectionTable.tsx | 42 ++++++++++++++++--- .../SharedPrimitives.guardrails.test.ts | 9 +++- .../__tests__/DetailSectionTable.test.tsx | 32 ++++++++++++-- 5 files changed, 95 insertions(+), 38 deletions(-) diff --git a/docs/release-control/v6/internal/subsystems/frontend-primitives.md b/docs/release-control/v6/internal/subsystems/frontend-primitives.md index 096f23fe0..72ae89c1f 100644 --- a/docs/release-control/v6/internal/subsystems/frontend-primitives.md +++ b/docs/release-control/v6/internal/subsystems/frontend-primitives.md @@ -3868,12 +3868,17 @@ interactive provider-support content belongs behind boundary. `DetailSectionTable` keeps the single bordered table at narrow widths where density matters, then presents those same canonical rows as bounded section cards on desktop. Desktop cards share the available row width, stretch -to the same row height, balance five- and six-section drawers across three-card -rows, and use a bounded local label column with left-aligned values so the -layout has no ragged fixed-width island, stranded full-width final card, or -full-drawer scan distance. Unified-resource technical summaries are part of -this boundary and must not retain a full-width local table on desktop. The -responsive presentation stays owned by the shared primitive; +to the same row height, and use stable three- or four-column tracks. An +incomplete final row must fill those same tracks with explicit integral spans; +it must not independently flex-grow every remaining card into unrelated column +edges. Five- and six-section drawers remain balanced across three-column rows, +while seven-section drawers use a two-column span for the first card in the +final row so the remaining cards keep the four-column alignment. Cards use a +bounded local label column with left-aligned values so the layout has no ragged +fixed-width island, stranded full-width final card, or full-drawer scan +distance. Unified-resource technical summaries are part of this boundary and +must not retain a full-width local table on desktop. The responsive +presentation stays owned by the shared primitive; provider drawers must not fork their own desktop card renderers. Monitoring Optional detail-row progress is also owned by that shared presentation: the value text remains visible, `DetailSectionTable` composes `ProgressBar` for the diff --git a/frontend-modern/browser-verification.json b/frontend-modern/browser-verification.json index 983839721..894e2cc1f 100644 --- a/frontend-modern/browser-verification.json +++ b/frontend-modern/browser-verification.json @@ -1,22 +1,19 @@ { "version": 1, - "base_sha": "0df9747bd7ef97d768d5320e1952116ad4e675ea", - "verified_at": "2026-08-29T13:46:17Z", + "base_sha": "974f102333d5e394ce6ddf966ad2759d1888386f", + "verified_at": "2026-08-29T15:34:14Z", "result": "passed", "changed_paths": [ - "frontend-modern/src/api/settings.ts" + "frontend-modern/src/components/shared/DetailSectionTable.tsx" ], "content_sha256": { - "frontend-modern/src/api/settings.ts": "6b3b67e14054d6b93a0eeb92107426384b4d2badeec7c52798b7c00cf6f5e920" + "frontend-modern/src/components/shared/DetailSectionTable.tsx": "9164c52fe67c6e6504a961a48e562479613d498d8d12ae10411d5b193dba36b5" }, - "routes": [ - "/settings/system-general", - "/docs/PRIVACY" - ], + "routes": ["/proxmox/overview"], "viewports": [ { - "width": 1280, - "height": 800 + "width": 1920, + "height": 900 }, { "width": 390, @@ -24,17 +21,13 @@ } ], "states": [ - "authenticated General settings with outbound telemetry disabled by PULSE_TELEMETRY=false", - "schema-14 heartbeat payload preview expanded at desktop width", - "schema-14 heartbeat payload preview expanded without document overflow at narrow width", - "rotating pseudonymous install ID refreshed after operator confirmation", - "shipped privacy document opened from the telemetry disclosure" + "mock-backed Proxmox Overview with a seven-section node drawer expanded", + "desktop node details aligned to four shared column tracks across both rows", + "compact mobile node details retained as one full-width bordered table" ], "interactions": [ - "opened General settings at 1280x800 and selected Preview payload", - "confirmed schema_version 14 and representative alert severity, resolution-time, and persistence-health aggregates in the rendered payload", - "resized the current interaction to 390x844, confirmed the payload stayed visible, and verified no horizontal document overflow", - "selected Reset ID at narrow width, accepted the confirmation dialog, and confirmed the rendered install ID rotated", - "opened Full details and confirmed the shipped /docs/PRIVACY document rendered in a browser popup" + "opened Proxmox Overview at 1920x900, expanded Analytics A, and confirmed the four first-row cards share equal quarter-width tracks", + "confirmed Network spans two desktop tracks while Storage and Telemetry each span one, filling the final row with aligned card edges", + "repeated the expanded drawer at 390x844 and confirmed every section remains a full-width table row group with adjacent labels and values" ] } diff --git a/frontend-modern/src/components/shared/DetailSectionTable.tsx b/frontend-modern/src/components/shared/DetailSectionTable.tsx index 92b37529d..ea9b9ce58 100644 --- a/frontend-modern/src/components/shared/DetailSectionTable.tsx +++ b/frontend-modern/src/components/shared/DetailSectionTable.tsx @@ -26,10 +26,40 @@ const detailValueToneClass = (tone: DetailValueTone | undefined): string => { return 'text-base-content'; }; -const detailSectionDesktopBasisClass = (sectionCount: number): string => - sectionCount === 5 || sectionCount === 6 - ? 'lg:basis-[calc(33.333%-0.5rem)]' - : 'lg:basis-[calc(25%-0.5rem)]'; +const detailSectionDesktopColumnCount = (sectionCount: number): 1 | 2 | 3 | 4 => { + if (sectionCount <= 1) return 1; + if (sectionCount === 2) return 2; + if (sectionCount === 3 || sectionCount === 5 || sectionCount === 6) return 3; + return 4; +}; + +const detailSectionDesktopSpan = (sectionCount: number, sectionIndex: number): number => { + const columnCount = detailSectionDesktopColumnCount(sectionCount); + const remainder = sectionCount % columnCount; + if (remainder === 0 || sectionIndex < sectionCount - remainder) return 1; + + const remainderIndex = sectionIndex - (sectionCount - remainder); + const baseSpan = Math.floor(columnCount / remainder); + const widerSectionCount = columnCount % remainder; + return baseSpan + (remainderIndex < widerSectionCount ? 1 : 0); +}; + +const detailSectionDesktopBasisClass = (sectionCount: number, sectionIndex: number): string => { + const columnCount = detailSectionDesktopColumnCount(sectionCount); + const span = detailSectionDesktopSpan(sectionCount, sectionIndex); + if (span >= columnCount) return 'lg:basis-full'; + + if (columnCount === 4) { + if (span === 3) return 'lg:basis-[calc(75%-0.125rem)]'; + if (span === 2) return 'lg:basis-[calc(50%-0.25rem)]'; + return 'lg:basis-[calc(25%-0.375rem)]'; + } + if (columnCount === 3) { + if (span === 2) return 'lg:basis-[calc(66.667%-0.167rem)]'; + return 'lg:basis-[calc(33.333%-0.333rem)]'; + } + return 'lg:basis-[calc(50%-0.25rem)]'; +}; export const DetailSectionTable: Component<{ sections: DetailSection[]; @@ -45,10 +75,10 @@ export const DetailSectionTable: Component<{ wrapperClass="lg:overflow-visible" > - {(section) => ( + {(section, sectionIndex) => ( { expect(detailSectionTableSource).toContain('DetailSectionTable'); expect(detailSectionTableSource).toContain('InlineDetailPanel'); expect(detailSectionTableSource).toContain('ObjectDrawerHeader'); - expect(detailSectionTableSource).toContain('lg:basis-[calc(25%-0.5rem)]'); - expect(detailSectionTableSource).toContain('lg:basis-[calc(33.333%-0.5rem)]'); + expect(detailSectionTableSource).toContain('detailSectionDesktopSpan'); + expect(detailSectionTableSource).toContain('lg:basis-[calc(25%-0.375rem)]'); + expect(detailSectionTableSource).toContain('lg:basis-[calc(50%-0.25rem)]'); + expect(detailSectionTableSource).toContain('lg:basis-[calc(33.333%-0.333rem)]'); + expect(detailSectionTableSource).toContain('lg:basis-[calc(66.667%-0.167rem)]'); + expect(detailSectionTableSource).toContain('lg:flex-none'); + expect(detailSectionTableSource).not.toContain('lg:flex-1'); expect(detailSectionTableSource).toContain('lg:grid-cols-[7rem_minmax(0,1fr)]'); expect(detailSectionTableSource).toContain('lg:text-left'); expect(detailSectionTableSource).toContain('lg:divide-y-0'); diff --git a/frontend-modern/src/components/shared/__tests__/DetailSectionTable.test.tsx b/frontend-modern/src/components/shared/__tests__/DetailSectionTable.test.tsx index bfb21d2fb..deb817f32 100644 --- a/frontend-modern/src/components/shared/__tests__/DetailSectionTable.test.tsx +++ b/frontend-modern/src/components/shared/__tests__/DetailSectionTable.test.tsx @@ -87,8 +87,8 @@ describe('DetailSectionTable', () => { expect(sections).toHaveLength(2); expect(sections[0]).toHaveClass( 'lg:flex', - 'lg:flex-1', - 'lg:basis-[calc(25%-0.5rem)]', + 'lg:flex-none', + 'lg:basis-[calc(50%-0.25rem)]', 'lg:rounded', 'lg:border', 'lg:p-3', @@ -185,7 +185,7 @@ describe('DetailSectionTable', () => { expect(fill?.firstElementChild).toHaveClass('bg-emerald-500'); }); - it('balances five desktop sections across three- and two-card rows', () => { + it('fills five desktop sections on the same three-column tracks', () => { const { container } = render(() => ( ({ @@ -197,7 +197,31 @@ describe('DetailSectionTable', () => { const sections = container.querySelectorAll('tbody'); expect(sections).toHaveLength(5); - sections.forEach((section) => expect(section).toHaveClass('lg:basis-[calc(33.333%-0.5rem)]')); + Array.from(sections) + .slice(0, 3) + .forEach((section) => expect(section).toHaveClass('lg:basis-[calc(33.333%-0.333rem)]')); + expect(sections[3]).toHaveClass('lg:basis-[calc(66.667%-0.167rem)]'); + expect(sections[4]).toHaveClass('lg:basis-[calc(33.333%-0.333rem)]'); + }); + + it('fills seven desktop sections on the same four-column tracks', () => { + const { container } = render(() => ( + ({ + label: `Section ${index + 1}`, + rows: [{ label: 'Value', value: String(index + 1) }], + }))} + /> + )); + + const sections = container.querySelectorAll('tbody'); + expect(sections).toHaveLength(7); + Array.from(sections) + .slice(0, 4) + .forEach((section) => expect(section).toHaveClass('lg:basis-[calc(25%-0.375rem)]')); + expect(sections[4]).toHaveClass('lg:basis-[calc(50%-0.25rem)]'); + expect(sections[5]).toHaveClass('lg:basis-[calc(25%-0.375rem)]'); + expect(sections[6]).toHaveClass('lg:basis-[calc(25%-0.375rem)]'); }); it('lazily renders technical details with the same compact section rows', () => {