From 32b721503cc1bf75592baed55599c98e1c2aa896 Mon Sep 17 00:00:00 2001 From: rcourtman Date: Mon, 24 Aug 2026 16:36:29 +0100 Subject: [PATCH] Canonicalize mobile row disclosure affordances --- .../subsystems/frontend-primitives.md | 9 ++-- .../subsystems/performance-and-scalability.md | 5 ++ .../internal/subsystems/storage-recovery.md | 9 ++-- .../internal/subsystems/unified-resources.md | 6 ++- frontend-modern/browser-verification.json | 52 ++++++++++++------- .../UnifiedResourceHostTableCard.tsx | 1 + .../UnifiedResourcePBSTableSection.tsx | 1 + .../UnifiedResourcePMGTableSection.tsx | 1 + ...esourceTable.performance.contract.test.tsx | 1 + .../src/components/Storage/DiskList.tsx | 1 + .../components/Storage/StorageGroupRow.tsx | 1 + .../src/components/Storage/StoragePoolRow.tsx | 1 + .../SharedPrimitives.guardrails.test.ts | 8 ++- .../shared/SummaryRowActionButton.tsx | 2 +- .../__tests__/SummaryRowActionButton.test.tsx | 2 + 15 files changed, 72 insertions(+), 28 deletions(-) diff --git a/docs/release-control/v6/internal/subsystems/frontend-primitives.md b/docs/release-control/v6/internal/subsystems/frontend-primitives.md index 0d503938e..65a305d68 100644 --- a/docs/release-control/v6/internal/subsystems/frontend-primitives.md +++ b/docs/release-control/v6/internal/subsystems/frontend-primitives.md @@ -1187,9 +1187,12 @@ not a replacement status card, CTA band, or page-local nested card. below the shared mobile breakpoint while preserving the button for screen readers and revealing it on keyboard focus. `PlatformResourceDetailToggleButton` applies that policy to provider-native platform tables, and workload rows - opt in only while their compact row remains the disclosure target. Rows - whose click action does something other than expand details must keep a - visible disclosure control. + opt in only while their compact row remains the disclosure target. Every + disclosure-button consumer must declare that row-target relationship + explicitly through the required `hideWhenRowTappableOnMobile` boolean, so a + new table cannot silently restore a redundant mobile chevron. Rows whose + click action does something other than expand details must pass `false` and + keep a visible disclosure control. They must not rely on a global rule that reveals every hidden column: phone priority remains source-specific because state, capacity, freshness, and recovery posture do not carry equal value for every resource diff --git a/docs/release-control/v6/internal/subsystems/performance-and-scalability.md b/docs/release-control/v6/internal/subsystems/performance-and-scalability.md index f1c81abd5..ea7868917 100644 --- a/docs/release-control/v6/internal/subsystems/performance-and-scalability.md +++ b/docs/release-control/v6/internal/subsystems/performance-and-scalability.md @@ -2191,6 +2191,11 @@ forking separate table-only presentation logic. That component now also consumes the shared `frontend-modern/src/utils/resourceChangePresentation.ts` label helper for canonical change kinds, source types, and adapter provenance so the chip wording stays consistent without adding extra hot-path branching. +Host, PBS, and PMG summary rows also declare themselves as the compact touch +disclosure target through `SummaryRowActionButton`; the shared primitive hides +the redundant visible mobile chevron while preserving accessible disclosure +semantics, so provider sections do not pay a separate cell-width or tap-target +cost for an action already owned by the whole row. The same infrastructure hot path now also depends on the shared `frontend-modern/src/components/shared/ProgressBar.tsx` primitive for metric fill rendering. Performance-sensitive metric bars may vary by value and color, diff --git a/docs/release-control/v6/internal/subsystems/storage-recovery.md b/docs/release-control/v6/internal/subsystems/storage-recovery.md index 3899d854d..00e214334 100644 --- a/docs/release-control/v6/internal/subsystems/storage-recovery.md +++ b/docs/release-control/v6/internal/subsystems/storage-recovery.md @@ -2187,9 +2187,12 @@ files. `frontend-modern/src/components/shared/SummaryRowActionButton.tsx`, but storage group headers should pin through the row itself and must not add a separate scope/pinned pill button beside the disclosure chevron. Touch - users still must not inherit synthetic hover branches, and storage must - not keep a special trailing expand column once the shared leading action - contract exists. + pool and physical-disk rows own expansion through the whole row, so they + must explicitly suppress the redundant visible mobile chevron; group + headers whose row click owns summary focus rather than expansion must keep + their disclosure chevron visible. Touch users still must not inherit + synthetic hover branches, and storage must not keep a special trailing + expand column once the shared leading action contract exists. Static subgroup header emphasis for storage group rows and recovery history day headers must also route through `frontend-modern/src/components/shared/groupedTableRowPresentation.ts` and diff --git a/docs/release-control/v6/internal/subsystems/unified-resources.md b/docs/release-control/v6/internal/subsystems/unified-resources.md index 7637b41f4..fb506a4d0 100644 --- a/docs/release-control/v6/internal/subsystems/unified-resources.md +++ b/docs/release-control/v6/internal/subsystems/unified-resources.md @@ -1585,7 +1585,11 @@ served clones. Proof: `TestClonedResourcesPreservePlatformAdmission` and `frontend-modern/src/components/shared/SummaryRowActionButton.tsx`, so the unified-resource table does not fork mouse-only hover logic, focusable-row button shims, touch-hostile synthetic hover, or provider-specific control - handling across host, PBS, and PMG sections. + handling across host, PBS, and PMG sections. Those three resource rows are + themselves the compact touch disclosure target and must explicitly enable + the shared mobile-chevron suppression; their accessible row/button state + remains available without spending a visible summary-cell slot on a + duplicate control. 15. Keep infrastructure search aligned with the governed display label. Shared infrastructure filtering through `frontend-modern/src/components/Infrastructure/infrastructureSelectors.ts` diff --git a/frontend-modern/browser-verification.json b/frontend-modern/browser-verification.json index b610959d8..9a3d2ffa3 100644 --- a/frontend-modern/browser-verification.json +++ b/frontend-modern/browser-verification.json @@ -1,25 +1,39 @@ { "version": 1, - "base_sha": "714b26b69f8a39d906b041dbf1b64d5d623632c1", - "verified_at": "2026-08-24T15:30:00Z", + "base_sha": "b67ac28f25434cb9e6e393a0787e72d915ba479a", + "verified_at": "2026-08-24T15:34:13Z", "result": "passed", "changed_paths": [ - "frontend-modern/src/components/shared/TooltipPortal.tsx", - "frontend-modern/src/components/shared/hoverCapability.ts", - "frontend-modern/src/components/shared/useTooltipState.ts", - "frontend-modern/src/hooks/useTooltip.ts" + "frontend-modern/src/components/Infrastructure/UnifiedResourceHostTableCard.tsx", + "frontend-modern/src/components/Infrastructure/UnifiedResourcePBSTableSection.tsx", + "frontend-modern/src/components/Infrastructure/UnifiedResourcePMGTableSection.tsx", + "frontend-modern/src/components/Storage/DiskList.tsx", + "frontend-modern/src/components/Storage/StorageGroupRow.tsx", + "frontend-modern/src/components/Storage/StoragePoolRow.tsx", + "frontend-modern/src/components/shared/SummaryRowActionButton.tsx" ], "content_sha256": { - "frontend-modern/src/components/shared/TooltipPortal.tsx": "dafea8136a75491438a9ab45de348a4307ada67799796f38e3e26087339ef445", - "frontend-modern/src/components/shared/hoverCapability.ts": "bae00548234580fe72e4fe93833c57ff237d7666d2ada00a0f673c676f9c39dc", - "frontend-modern/src/components/shared/useTooltipState.ts": "4959c6840e3c062fa31c2a0a7391f5c0314725ac8ef48deeed37551f9d48194c", - "frontend-modern/src/hooks/useTooltip.ts": "fa13e21a922954466a9cb09abaff9cd04b3caf0c43dbb025829e8bf251f8065f" + "frontend-modern/src/components/Infrastructure/UnifiedResourceHostTableCard.tsx": "78588725f625e58325c2ddca6133d3e0b0d46a18f9d19e8946f7c0181c094442", + "frontend-modern/src/components/Infrastructure/UnifiedResourcePBSTableSection.tsx": "15031095e5910c02bd47038213974de4b3d2aef53b1ff6cd030fee381a98fe91", + "frontend-modern/src/components/Infrastructure/UnifiedResourcePMGTableSection.tsx": "d9a5a6f209b97a02dfa40100f783d91e8202b0efc33af73415e05689251f6ec6", + "frontend-modern/src/components/Storage/DiskList.tsx": "5d600b9bfa8e34ee9ed13037cdbe87b931c10e6378440d41bf1d1ba68cab9526", + "frontend-modern/src/components/Storage/StorageGroupRow.tsx": "5ce92da0c45b4146c51eb52786adb245978b5f6b4adc95b40d2294d949720e99", + "frontend-modern/src/components/Storage/StoragePoolRow.tsx": "f389fef94c53a74cf69d6f165ae01f71f02a1e0143dc68c1318033aebe0001a4", + "frontend-modern/src/components/shared/SummaryRowActionButton.tsx": "dbd6a62b6dbf11cabfaf01bc7240ab3ba3a8230cef1ee20bc2f0bd8c19e4bb14" }, - "routes": ["/proxmox"], + "routes": [ + "/proxmox/overview", + "/proxmox/storage", + "/docker/overview", + "/kubernetes/overview", + "/truenas/overview", + "/vmware/health", + "/standalone/machines" + ], "viewports": [ { "width": 1280, - "height": 720 + "height": 800 }, { "width": 390, @@ -27,13 +41,15 @@ } ], "states": [ - "Proxmox workload table with a fine hover-capable desktop pointer", - "Proxmox workload table with touch emulation reporting a coarse primary pointer", - "expanded artifact-cache-310 inline guest drawer on the narrow viewport" + "collapsed storage-pool and physical-disk rows at narrow width without visible disclosure chevrons", + "expanded and collapsed storage detail rows activated from the whole summary row", + "provider overview rows across Proxmox, Docker, Kubernetes, TrueNAS, vSphere, and standalone machines with accessible but visually suppressed mobile disclosure buttons", + "desktop storage rows retaining their explicit disclosure affordance" ], "interactions": [ - "tapped the artifact-cache-310 workload row at 390x844 and confirmed the inline guest drawer opened without a portal or singleton tooltip", - "hovered a workload CPU metric at 1280x720 and confirmed the CPU Details tooltip remained available for a fine pointer", - "confirmed the touch and desktop media capability queries selected the intended tooltip behavior before restoring the browser viewport" + "opened and closed a Proxmox storage-pool detail from the whole row at 390px", + "switched from Storage to Physical Disks and confirmed its whole-row disclosure buttons are visually clipped at 390px", + "audited seven loaded platform routes and found zero visible Expand or Collapse disclosure buttons at 390px while retaining 232 accessible controls", + "inspected the Proxmox storage table at 1280px and confirmed desktop disclosure layout and row density remain intact" ] } diff --git a/frontend-modern/src/components/Infrastructure/UnifiedResourceHostTableCard.tsx b/frontend-modern/src/components/Infrastructure/UnifiedResourceHostTableCard.tsx index 3f8d4dae7..c6bbdd20c 100644 --- a/frontend-modern/src/components/Infrastructure/UnifiedResourceHostTableCard.tsx +++ b/frontend-modern/src/components/Infrastructure/UnifiedResourceHostTableCard.tsx @@ -408,6 +408,7 @@ export const UnifiedResourceHostTableCard: Component table.toggleExpand(resource.id)} onPreviewClear={() => tableProps.onHoverChange?.(null)} /> diff --git a/frontend-modern/src/components/Infrastructure/UnifiedResourcePBSTableSection.tsx b/frontend-modern/src/components/Infrastructure/UnifiedResourcePBSTableSection.tsx index a1f5708b6..e5280682f 100644 --- a/frontend-modern/src/components/Infrastructure/UnifiedResourcePBSTableSection.tsx +++ b/frontend-modern/src/components/Infrastructure/UnifiedResourcePBSTableSection.tsx @@ -184,6 +184,7 @@ export const UnifiedResourcePBSTableSection: Component table.toggleExpand(resource.id)} onPreviewClear={() => tableProps.onHoverChange?.(null)} /> diff --git a/frontend-modern/src/components/Infrastructure/UnifiedResourcePMGTableSection.tsx b/frontend-modern/src/components/Infrastructure/UnifiedResourcePMGTableSection.tsx index 986270470..2f8e07537 100644 --- a/frontend-modern/src/components/Infrastructure/UnifiedResourcePMGTableSection.tsx +++ b/frontend-modern/src/components/Infrastructure/UnifiedResourcePMGTableSection.tsx @@ -198,6 +198,7 @@ export const UnifiedResourcePMGTableSection: Component table.toggleExpand(resource.id)} onPreviewClear={() => tableProps.onHoverChange?.(null)} /> diff --git a/frontend-modern/src/components/Infrastructure/__tests__/UnifiedResourceTable.performance.contract.test.tsx b/frontend-modern/src/components/Infrastructure/__tests__/UnifiedResourceTable.performance.contract.test.tsx index 109591435..1aa7cbe4a 100644 --- a/frontend-modern/src/components/Infrastructure/__tests__/UnifiedResourceTable.performance.contract.test.tsx +++ b/frontend-modern/src/components/Infrastructure/__tests__/UnifiedResourceTable.performance.contract.test.tsx @@ -821,6 +821,7 @@ describe('UnifiedResourceTable performance contract', () => { expect(source).toContain('data-summary-row-active'); expect(source).toContain('createSummaryInteractiveRowPreviewHandlers'); expect(source).toContain('SummaryRowActionButton'); + expect(source).toContain('hideWhenRowTappableOnMobile'); expect(source).not.toContain('bg-blue-50 dark:bg-blue-900 ring-1 ring-blue-300'); expect(source).not.toContain('bg-blue-100 dark:bg-blue-800'); } diff --git a/frontend-modern/src/components/Storage/DiskList.tsx b/frontend-modern/src/components/Storage/DiskList.tsx index 6f25d92d7..280ebce9c 100644 --- a/frontend-modern/src/components/Storage/DiskList.tsx +++ b/frontend-modern/src/components/Storage/DiskList.tsx @@ -361,6 +361,7 @@ export const DiskList: Component = (props) => { subjectLabel={data.model || 'disk'} expanded={isSelected()} controlsId={detailControlsId} + hideWhenRowTappableOnMobile onAction={() => model.toggleSelectedDisk(disk)} onPreviewClear={() => props.onHoverChange?.(null)} /> diff --git a/frontend-modern/src/components/Storage/StorageGroupRow.tsx b/frontend-modern/src/components/Storage/StorageGroupRow.tsx index c812928dc..87390cc25 100644 --- a/frontend-modern/src/components/Storage/StorageGroupRow.tsx +++ b/frontend-modern/src/components/Storage/StorageGroupRow.tsx @@ -53,6 +53,7 @@ export const StorageGroupRow: Component = (props) => { kind="disclosure" subjectLabel={row().label} expanded={props.expanded} + hideWhenRowTappableOnMobile={false} onAction={props.onToggle} onPreviewClear={() => props.onHoverChange?.(null)} class="inline-flex items-center justify-center" diff --git a/frontend-modern/src/components/Storage/StoragePoolRow.tsx b/frontend-modern/src/components/Storage/StoragePoolRow.tsx index 18f3c5c03..58e8e6d2a 100644 --- a/frontend-modern/src/components/Storage/StoragePoolRow.tsx +++ b/frontend-modern/src/components/Storage/StoragePoolRow.tsx @@ -100,6 +100,7 @@ export const StoragePoolRow: Component = (props) => { subjectLabel={props.record.name} expanded={props.expanded} controlsId={detailControlsId()} + hideWhenRowTappableOnMobile onAction={props.onToggleExpand} onPreviewClear={() => props.onHoverChange?.(null)} /> diff --git a/frontend-modern/src/components/shared/SharedPrimitives.guardrails.test.ts b/frontend-modern/src/components/shared/SharedPrimitives.guardrails.test.ts index 67e355b86..4b531494b 100644 --- a/frontend-modern/src/components/shared/SharedPrimitives.guardrails.test.ts +++ b/frontend-modern/src/components/shared/SharedPrimitives.guardrails.test.ts @@ -1555,7 +1555,7 @@ describe('shared primitive guardrails', () => { expect(summaryRowActionButtonSource).toContain('aria-expanded'); expect(summaryRowActionButtonSource).toContain('aria-pressed'); expect(summaryRowActionButtonSource).toContain('data-row-action="true"'); - expect(summaryRowActionButtonSource).toContain('hideWhenRowTappableOnMobile'); + expect(summaryRowActionButtonSource).toContain('hideWhenRowTappableOnMobile: boolean'); expect(summaryRowActionButtonSource).toContain('sm:not-sr-only'); expect(platformResourceDetailTableRowSource).toContain('PlatformResourceDetailToggleButton'); expect(platformResourceDetailTableRowSource).toContain('SummaryRowActionButton'); @@ -1563,7 +1563,6 @@ describe('shared primitive guardrails', () => { for (const source of [ guestRowSource, - storageGroupRowSource, storagePoolRowSource, diskListSource, unifiedResourceHostTableCardSource, @@ -1572,8 +1571,13 @@ describe('shared primitive guardrails', () => { ]) { expect(source).toContain('createSummaryInteractiveRowPreviewHandlers'); expect(source).toContain('SummaryRowActionButton'); + expect(source).toContain('hideWhenRowTappableOnMobile'); } + expect(storageGroupRowSource).toContain('createSummaryInteractiveRowPreviewHandlers'); + expect(storageGroupRowSource).toContain('SummaryRowActionButton'); + expect(storageGroupRowSource).toContain('hideWhenRowTappableOnMobile={false}'); + expect(workloadPanelSource).toContain('createSummaryInteractiveRowPreviewHandlers'); expect(workloadPanelSource).not.toContain('kind="scope"'); expect(storageGroupRowSource).not.toContain('kind="scope"'); diff --git a/frontend-modern/src/components/shared/SummaryRowActionButton.tsx b/frontend-modern/src/components/shared/SummaryRowActionButton.tsx index 209f88742..a8cb59548 100644 --- a/frontend-modern/src/components/shared/SummaryRowActionButton.tsx +++ b/frontend-modern/src/components/shared/SummaryRowActionButton.tsx @@ -11,7 +11,7 @@ type SummaryRowActionButtonProps = subjectLabel: string; controlsId?: string; class?: string; - hideWhenRowTappableOnMobile?: boolean; + hideWhenRowTappableOnMobile: boolean; onAction: () => void; onPreviewClear?: () => void; } diff --git a/frontend-modern/src/components/shared/__tests__/SummaryRowActionButton.test.tsx b/frontend-modern/src/components/shared/__tests__/SummaryRowActionButton.test.tsx index 18ec42ade..00a1d284d 100644 --- a/frontend-modern/src/components/shared/__tests__/SummaryRowActionButton.test.tsx +++ b/frontend-modern/src/components/shared/__tests__/SummaryRowActionButton.test.tsx @@ -11,6 +11,7 @@ describe('SummaryRowActionButton', () => { subjectLabel="alpha" expanded={true} controlsId="summary-row-detail-alpha" + hideWhenRowTappableOnMobile={false} onAction={onAction} /> )); @@ -78,6 +79,7 @@ describe('SummaryRowActionButton', () => { subjectLabel="alpha" expanded={false} controlsId="summary-row-detail-alpha" + hideWhenRowTappableOnMobile={false} onAction={onAction} /> ));