From 737bde7362c1664e85b858712467453ffd7c78a7 Mon Sep 17 00:00:00 2001 From: rcourtman Date: Thu, 6 Aug 2026 15:55:11 +0100 Subject: [PATCH] fix(ui): refine responsive platform tables Contract-Neutral: responsive presentation and touch-target adjustments only; no API or payload changes --- frontend-modern/browser-verification.json | 119 ++++++++++++++---- .../scripts/shared-template-registry.json | 4 +- .../ResourceDetailDrawerSupportDisclosure.tsx | 2 +- .../ResourceOperatorStateSection.tsx | 24 ++-- .../ResourceOperatorStateSection.test.ts | 15 +++ .../Workloads/GuestDrawerHistory.tsx | 2 +- ...loadsSurface.performance.contract.test.tsx | 1 + .../SharedPrimitives.guardrails.test.ts | 8 +- .../src/components/shared/buttonModel.ts | 4 +- .../src/features/docker/DockerImagesTable.tsx | 4 +- .../features/docker/DockerServicesTable.tsx | 26 ++-- .../features/docker/DockerSwarmNodesTable.tsx | 12 +- .../src/features/docker/DockerTasksTable.tsx | 8 +- .../kubernetes/KubernetesClustersTable.tsx | 2 +- .../kubernetes/KubernetesConfigTable.tsx | 4 +- .../kubernetes/KubernetesDeploymentsTable.tsx | 7 +- .../kubernetes/KubernetesNetworkingTable.tsx | 4 +- .../kubernetes/KubernetesNodesTable.tsx | 12 +- .../kubernetes/KubernetesPodsTable.tsx | 8 +- .../kubernetes/KubernetesPolicyTable.tsx | 4 +- .../kubernetes/KubernetesServicesTable.tsx | 4 +- .../kubernetes/KubernetesStorageTable.tsx | 8 +- .../platformOverviewLayout.guardrails.test.ts | 82 ++++++++++++ .../standalone/AgentsMachinesTable.tsx | 5 +- .../truenas/TrueNASStorageTopologyTable.tsx | 18 +-- .../TrueNASStorageTopologyTable.test.tsx | 14 +-- .../features/vmware/VsphereNetworksTable.tsx | 8 +- 27 files changed, 294 insertions(+), 115 deletions(-) diff --git a/frontend-modern/browser-verification.json b/frontend-modern/browser-verification.json index 08bc4c4f7..f57203232 100644 --- a/frontend-modern/browser-verification.json +++ b/frontend-modern/browser-verification.json @@ -1,46 +1,115 @@ { "version": 1, - "base_sha": "c25d393fff818067d9e75f2fed01ad5f47f41ec2", - "verified_at": "2026-08-06T14:51:21Z", + "base_sha": "2d9ba270651ed5e1028ae4aa15843ab34303c7c9", + "verified_at": "2026-08-06T14:52:48Z", "result": "passed", "changed_paths": [ - "frontend-modern/src/AppLayout.tsx", - "frontend-modern/src/components/Brand/PulseBrandMark.tsx", - "frontend-modern/src/components/Login.tsx", - "frontend-modern/src/components/SetupWizard/steps/WelcomeStep.tsx" + "frontend-modern/src/components/Infrastructure/ResourceDetailDrawerSupportDisclosure.tsx", + "frontend-modern/src/components/Infrastructure/ResourceOperatorStateSection.tsx", + "frontend-modern/src/components/Workloads/GuestDrawerHistory.tsx", + "frontend-modern/src/components/shared/buttonModel.ts", + "frontend-modern/src/features/docker/DockerImagesTable.tsx", + "frontend-modern/src/features/docker/DockerServicesTable.tsx", + "frontend-modern/src/features/docker/DockerSwarmNodesTable.tsx", + "frontend-modern/src/features/docker/DockerTasksTable.tsx", + "frontend-modern/src/features/kubernetes/KubernetesClustersTable.tsx", + "frontend-modern/src/features/kubernetes/KubernetesConfigTable.tsx", + "frontend-modern/src/features/kubernetes/KubernetesDeploymentsTable.tsx", + "frontend-modern/src/features/kubernetes/KubernetesNetworkingTable.tsx", + "frontend-modern/src/features/kubernetes/KubernetesNodesTable.tsx", + "frontend-modern/src/features/kubernetes/KubernetesPodsTable.tsx", + "frontend-modern/src/features/kubernetes/KubernetesPolicyTable.tsx", + "frontend-modern/src/features/kubernetes/KubernetesServicesTable.tsx", + "frontend-modern/src/features/kubernetes/KubernetesStorageTable.tsx", + "frontend-modern/src/features/standalone/AgentsMachinesTable.tsx", + "frontend-modern/src/features/truenas/TrueNASStorageTopologyTable.tsx", + "frontend-modern/src/features/vmware/VsphereNetworksTable.tsx" ], "content_sha256": { - "frontend-modern/src/AppLayout.tsx": "5f0ee2909e3c08840af0c5b8eac78cd6c833aeb60f12250d770aea9fc319ce68", - "frontend-modern/src/components/Brand/PulseBrandMark.tsx": "af8766f84e628b00f22cfc2c5b786a0e0914b8555f2796748e8634265d8b7f0b", - "frontend-modern/src/components/Login.tsx": "c81e3c29a7e59f1217945bb373dced6aa28b047c6d6a7929e46681994eb9305f", - "frontend-modern/src/components/SetupWizard/steps/WelcomeStep.tsx": "f3f77aebdc6c26291a731506da5d1a40184dc485079fc60f7b5c57795557bb03" + "frontend-modern/src/components/Infrastructure/ResourceDetailDrawerSupportDisclosure.tsx": "c51b9e1530cd64dfe119adf16ec37e0ac1014be8ce52593c34735776ccaf7fbc", + "frontend-modern/src/components/Infrastructure/ResourceOperatorStateSection.tsx": "35e44d398281f05e48a770801ece59ef120150bc613e2f17d3bed0f30e349b34", + "frontend-modern/src/components/Workloads/GuestDrawerHistory.tsx": "3bfcbffa02f3e1b2512e925e3f33a9ff490b269fffb6b946e0211f31d31854e2", + "frontend-modern/src/components/shared/buttonModel.ts": "bd672d8673cab1b467afc72b932a1b1ef62bd25eac28d7c2e40e74477da71e04", + "frontend-modern/src/features/docker/DockerImagesTable.tsx": "dc37ef6b662b2fbbecce312894fa5e8c19d2224cf1895683e63fdaca930d6301", + "frontend-modern/src/features/docker/DockerServicesTable.tsx": "5e793d307797c96b2496b7c5cef4807890f480fc5fb22cc1c4bc9a506f85383e", + "frontend-modern/src/features/docker/DockerSwarmNodesTable.tsx": "bb0b23679387945dd5bab4a86231754549419d0e2350fa1a1aca7f26ee317403", + "frontend-modern/src/features/docker/DockerTasksTable.tsx": "201200a74d8da6bcf246e5ec5de1e8df018c05833f5fae66f4f6435768a27cb4", + "frontend-modern/src/features/kubernetes/KubernetesClustersTable.tsx": "c50271a335b9008196ce832f5ab18f90b93996ee0516570f731ec036377e84f0", + "frontend-modern/src/features/kubernetes/KubernetesConfigTable.tsx": "ce3bd1d75937b4d8a914e09385f71b32ef3e0ac7b898680f2d31c1aab6dd043e", + "frontend-modern/src/features/kubernetes/KubernetesDeploymentsTable.tsx": "237338df289a7fddbf3b6c8b889e55e8357fc2c43b8e9195ffd7ddf1c8e78f13", + "frontend-modern/src/features/kubernetes/KubernetesNetworkingTable.tsx": "348fd5f8c7b76de1dd971328b7622d46314f2c8c995110883ffb278aab7d26bb", + "frontend-modern/src/features/kubernetes/KubernetesNodesTable.tsx": "9dab059b7f6375ece604b760cbf05db93b7bfedd20a1e1e0f345b80e4188c5f3", + "frontend-modern/src/features/kubernetes/KubernetesPodsTable.tsx": "fba4eb11395cac6aa8555807bd92f07b15b1013a1d19606b1b70eddffa143813", + "frontend-modern/src/features/kubernetes/KubernetesPolicyTable.tsx": "b486880d9147a3af212ba9c1b570959242900d4fd8c7b4c2c7a72d30046e5edb", + "frontend-modern/src/features/kubernetes/KubernetesServicesTable.tsx": "2ba5c960a276a25b34992c2c8f59b783220e84852f60b0c6069b277cd9e89efb", + "frontend-modern/src/features/kubernetes/KubernetesStorageTable.tsx": "f09c277ca2ee298d585207acf4e1d50fae4a7472b818233df69842a772f53a58", + "frontend-modern/src/features/standalone/AgentsMachinesTable.tsx": "3ede12e55a4975563f9f4c7c4db7e9ffced3d4aae9ff79c9806eb443e78da58c", + "frontend-modern/src/features/truenas/TrueNASStorageTopologyTable.tsx": "d85fc6739a235a295b483044b9250b2e59a542a875ae3c8b6b46bb507091385c", + "frontend-modern/src/features/vmware/VsphereNetworksTable.tsx": "219fe6f2a4726b9bff7f5b8fdd51ef05b622a931743a3fecf9541de9894f2020" }, "routes": [ - "/", - "/proxmox/overview" + "/docker/overview", + "/docker/images", + "/docker/storage", + "/docker/networks", + "/docker/swarm", + "/kubernetes/overview", + "/kubernetes/nodes", + "/kubernetes/workloads", + "/kubernetes/services", + "/kubernetes/storage", + "/kubernetes/configuration", + "/kubernetes/events", + "/truenas/overview", + "/truenas/storage", + "/truenas/services", + "/truenas/apps", + "/truenas/vms", + "/truenas/shares", + "/truenas/protection", + "/vmware/overview", + "/vmware/storage", + "/vmware/networks", + "/vmware/health", + "/vmware/activity", + "/standalone/machines", + "/standalone/availability" ], "viewports": [ { "width": 1280, - "height": 720 + "height": 900 }, { - "width": 375, - "height": 812 + "width": 768, + "height": 900 + }, + { + "width": 390, + "height": 900 + }, + { + "width": 320, + "height": 900 } ], "states": [ - "Login screen brand mark in app-dark theme, computed disc #3b82f6 with ring stroke and centre fill both #dbeafe", - "Login screen brand mark in app-light theme while the OS colour scheme was dark, computed disc #2563eb with ring stroke and centre fill both #ffffff, confirming the mark follows the app theme class rather than the OS preference", - "Authenticated header brand lockup in app-light and app-dark themes at 1280x720, ring stroke and centre fill matching in both", - "Authenticated header brand lockup at 375x812 mobile, mark visible at 20x20 with dark palette intact", - "SetupWizard WelcomeStep was NOT exercised live: it renders only when the instance has no authentication configured, and the dev backend is configured. Covered by WelcomeStep.test.tsx and by the shared PulseBrandMark component verified on the other two surfaces" + "Mock-data inventory tables across Docker, Kubernetes, TrueNAS, vSphere, and standalone Machines and Availability routes", + "Phone layouts with operator-priority identity, health, capacity, state, and update columns while secondary context is hidden", + "Tablet layouts with namespace, scope, role, host, mode, address, class, kind, disk, and node context restored where space permits", + "Desktop layouts with the complete field set and stable platform-page width", + "TrueNAS nested storage topology at multiple depths with long resource names and usage and health values", + "Expanded machine and Docker image resource drawers with overview, history, discovery, support disclosure, and operator override sections", + "Operator maintenance scheduler open and cancelled without saving, plus machine action menu open and closed" ], "interactions": [ - "Signed in with local credentials from the login form to move from the unauthenticated to the authenticated header surface", - "Toggled pulseThemePreference between light and dark in localStorage and reloaded, reading getComputedStyle fill and stroke off .pulse-bg, .pulse-ring and .pulse-center each time", - "Held the OS colour scheme at dark while forcing the app theme to light to prove the inline mark tracks the app theme, the divergence an cannot represent", - "Read getAnimations() on the extracted mark and confirmed pulse-brand-logo, pulse-brand-bg, pulse-brand-ring and pulse-brand-center are all running, proving the .animate-pulse-brand descendant selectors still reach the circles after the SVG moved into a component", - "Resized from 1280x720 to 375x812 and re-measured the header mark bounding box and computed ring stroke" + "Navigated every listed platform route and inspected table headers, visible information priority, truncation, and document overflow at phone, tablet, and desktop widths", + "Added responsive context progressively between 320px, 390px, 768px, and 1280px instead of scaling the application or forcing horizontal table scrolling", + "Expanded and collapsed resource rows, switched drawer tabs, opened support and detail disclosures, and measured drawer header, select, schedule, preset, form, and history-range controls", + "Verified changed phone controls meet a 44px touch target while returning to the compact treatment above the phone breakpoint", + "Opened the machine row action menu at 320px and confirmed its bounds stay inside the viewport", + "Pressed Escape on the open machine action menu and confirmed it closes and restores focus to the originating action button", + "Closed an expanded machine detail drawer at 320px and confirmed focus returns to the exact row expand button", + "Confirmed no horizontal document overflow on the audited pages, including expanded drawer states, then reset the viewport and finalized the browser session" ] } diff --git a/frontend-modern/scripts/shared-template-registry.json b/frontend-modern/scripts/shared-template-registry.json index 85ea9eb6c..a6df4ce44 100644 --- a/frontend-modern/scripts/shared-template-registry.json +++ b/frontend-modern/scripts/shared-template-registry.json @@ -5373,7 +5373,7 @@ "scopes": ["src/components", "src/features", "src/pages"], "extensions": [".tsx"], "allPatterns": [ - "inline-flex h-8 items-center gap-1.5 rounded border border-border bg-surface px-2 text-xs font-medium text-base-content transition-colors hover:bg-surface-hover focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500" + "inline-flex h-8 min-h-11 min-w-11 items-center gap-1.5 rounded border border-border bg-surface px-2 text-xs font-medium text-base-content transition-colors hover:bg-surface-hover focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500" ], "legacyReason": "Retired migration debt. Drawer header Assistant and context-copy actions belong to the shared Button primitive family.", "allowedPaths": [], @@ -5395,7 +5395,7 @@ "scopes": ["src/components", "src/features", "src/pages"], "extensions": [".tsx"], "allPatterns": [ - "inline-flex h-8 w-8 items-center justify-center rounded-md hover:bg-surface-hover hover:text-base-content focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500" + "inline-flex h-8 w-8 min-h-11 min-w-11 items-center justify-center rounded-md hover:bg-surface-hover hover:text-base-content focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500" ], "legacyReason": "Retired migration debt. Drawer header close and icon actions belong to the shared Button primitive family.", "allowedPaths": [], diff --git a/frontend-modern/src/components/Infrastructure/ResourceDetailDrawerSupportDisclosure.tsx b/frontend-modern/src/components/Infrastructure/ResourceDetailDrawerSupportDisclosure.tsx index 222f0938c..7d0cbbec0 100644 --- a/frontend-modern/src/components/Infrastructure/ResourceDetailDrawerSupportDisclosure.tsx +++ b/frontend-modern/src/components/Infrastructure/ResourceDetailDrawerSupportDisclosure.tsx @@ -46,7 +46,7 @@ export const ResourceDetailDrawerSupportDisclosure: Component< onClick={props.onToggle} class={ props.buttonClass ?? - 'inline-flex shrink-0 items-center rounded-md border border-border bg-surface px-2.5 py-1 text-[10px] font-medium text-base-content transition-colors hover:bg-base' + 'inline-flex min-h-11 shrink-0 items-center rounded-md border border-border bg-surface px-2.5 py-1 text-[10px] font-medium text-base-content transition-colors hover:bg-base sm:min-h-0' } > {props.expanded ? props.hideLabel : props.showLabel} diff --git a/frontend-modern/src/components/Infrastructure/ResourceOperatorStateSection.tsx b/frontend-modern/src/components/Infrastructure/ResourceOperatorStateSection.tsx index 48495d12a..b7ff9f9ff 100644 --- a/frontend-modern/src/components/Infrastructure/ResourceOperatorStateSection.tsx +++ b/frontend-modern/src/components/Infrastructure/ResourceOperatorStateSection.tsx @@ -508,7 +508,7 @@ export const ResourceOperatorStateSection: Component setCriticality(e.currentTarget.value as ResourceCriticality)} disabled={saving()} - class="mt-1 block w-full text-xs rounded border border-border bg-surface px-2 py-1.5 text-base-content focus:outline-none focus:ring-1 focus:ring-blue-400 disabled:opacity-50" + class="mt-1 block min-h-11 w-full text-xs rounded border border-border bg-surface px-2 py-1.5 text-base-content focus:outline-none focus:ring-1 focus:ring-blue-400 disabled:opacity-50 sm:min-h-0" > @@ -552,7 +552,7 @@ export const ResourceOperatorStateSection: Component Schedule window @@ -563,7 +563,7 @@ export const ResourceOperatorStateSection: Component Edit window @@ -571,7 +571,7 @@ export const ResourceOperatorStateSection: Component Cancel window @@ -590,7 +590,7 @@ export const ResourceOperatorStateSection: Component setScheduleStart(e.currentTarget.value)} - class="mt-0.5 w-full text-xs rounded border border-border bg-surface px-2 py-1 text-base-content focus:outline-none focus:ring-1 focus:ring-blue-400" + class="mt-0.5 min-h-11 w-full text-xs rounded border border-border bg-surface px-2 py-1 text-base-content focus:outline-none focus:ring-1 focus:ring-blue-400 sm:min-h-0" disabled={saving()} /> @@ -600,7 +600,7 @@ export const ResourceOperatorStateSection: Component setScheduleEnd(e.currentTarget.value)} - class="mt-0.5 w-full text-xs rounded border border-border bg-surface px-2 py-1 text-base-content focus:outline-none focus:ring-1 focus:ring-blue-400" + class="mt-0.5 min-h-11 w-full text-xs rounded border border-border bg-surface px-2 py-1 text-base-content focus:outline-none focus:ring-1 focus:ring-blue-400 sm:min-h-0" disabled={saving()} /> @@ -612,7 +612,7 @@ export const ResourceOperatorStateSection: Component applyPresetDuration(1)} disabled={saving()} - class="px-1.5 py-0.5 rounded border border-border hover:bg-surface-hover disabled:opacity-50" + class="min-h-11 min-w-11 px-1.5 py-0.5 rounded border border-border hover:bg-surface-hover disabled:opacity-50 sm:min-h-0 sm:min-w-0" > 1h @@ -620,7 +620,7 @@ export const ResourceOperatorStateSection: Component applyPresetDuration(4)} disabled={saving()} - class="px-1.5 py-0.5 rounded border border-border hover:bg-surface-hover disabled:opacity-50" + class="min-h-11 min-w-11 px-1.5 py-0.5 rounded border border-border hover:bg-surface-hover disabled:opacity-50 sm:min-h-0 sm:min-w-0" > 4h @@ -628,7 +628,7 @@ export const ResourceOperatorStateSection: Component applyPresetDuration(24)} disabled={saving()} - class="px-1.5 py-0.5 rounded border border-border hover:bg-surface-hover disabled:opacity-50" + class="min-h-11 min-w-11 px-1.5 py-0.5 rounded border border-border hover:bg-surface-hover disabled:opacity-50 sm:min-h-0 sm:min-w-0" > 24h @@ -641,7 +641,7 @@ export const ResourceOperatorStateSection: Component setScheduleReason(e.currentTarget.value)} placeholder="e.g. Q3 storage upgrade, kernel patch reboot" - class="mt-0.5 w-full text-xs rounded border border-border bg-surface px-2 py-1 text-base-content focus:outline-none focus:ring-1 focus:ring-blue-400" + class="mt-0.5 min-h-11 w-full text-xs rounded border border-border bg-surface px-2 py-1 text-base-content focus:outline-none focus:ring-1 focus:ring-blue-400 sm:min-h-0" disabled={saving()} maxLength={200} /> @@ -656,7 +656,7 @@ export const ResourceOperatorStateSection: Component setSchedulerOpen(false)} disabled={saving()} - class="px-2.5 py-1 text-xs font-medium text-muted hover:bg-surface-hover rounded transition-colors disabled:opacity-50" + class="min-h-11 px-2.5 py-1 text-xs font-medium text-muted hover:bg-surface-hover rounded transition-colors disabled:opacity-50 sm:min-h-0" > Cancel @@ -664,7 +664,7 @@ export const ResourceOperatorStateSection: Component {saving() ? 'Saving…' : 'Save window'} diff --git a/frontend-modern/src/components/Infrastructure/__tests__/ResourceOperatorStateSection.test.ts b/frontend-modern/src/components/Infrastructure/__tests__/ResourceOperatorStateSection.test.ts index 3be72c7c7..2952ece87 100644 --- a/frontend-modern/src/components/Infrastructure/__tests__/ResourceOperatorStateSection.test.ts +++ b/frontend-modern/src/components/Infrastructure/__tests__/ResourceOperatorStateSection.test.ts @@ -12,6 +12,11 @@ const overviewTabSource = readFileSync( 'utf-8', ); +const supportDisclosureSource = readFileSync( + resolve(__dirname, '..', 'ResourceDetailDrawerSupportDisclosure.tsx'), + 'utf-8', +); + describe('ResourceOperatorStateSection', () => { it('exposes operator-set controls bound to the canonical API client', () => { // The section is the operator's window into the per-resource state @@ -160,6 +165,16 @@ describe('ResourceOperatorStateSection', () => { expect(sectionSource).toContain('maintenanceEndAt: undefined,'); }); + it('keeps drawer disclosure and operator controls touch-safe on phones', () => { + expect(supportDisclosureSource).toContain('inline-flex min-h-11 shrink-0 items-center'); + expect(sectionSource).toContain('min-h-11 w-full text-xs rounded'); + expect(sectionSource).toContain('min-h-11 min-w-11 px-1.5 py-0.5'); + expect(sectionSource).toContain('min-h-11 px-2.5 py-1 text-xs font-medium text-base-content'); + expect(sectionSource).toContain( + 'min-h-11 px-2.5 py-1 text-xs font-medium text-white bg-blue-600', + ); + }); + it('renders a maintenance-window-active badge when the persisted window covers now', () => { // The section must gate the active badge on the // now-falls-within-window check, not just on the presence of a diff --git a/frontend-modern/src/components/Workloads/GuestDrawerHistory.tsx b/frontend-modern/src/components/Workloads/GuestDrawerHistory.tsx index e557bef04..1b62a8f48 100644 --- a/frontend-modern/src/components/Workloads/GuestDrawerHistory.tsx +++ b/frontend-modern/src/components/Workloads/GuestDrawerHistory.tsx @@ -104,7 +104,7 @@ export const GuestDrawerHistoryRangeSelect: Component props.onRangeChange(event.currentTarget.value as HistoryTimeRange)} diff --git a/frontend-modern/src/components/Workloads/__tests__/WorkloadsSurface.performance.contract.test.tsx b/frontend-modern/src/components/Workloads/__tests__/WorkloadsSurface.performance.contract.test.tsx index 72a8c5aaa..d579b032d 100644 --- a/frontend-modern/src/components/Workloads/__tests__/WorkloadsSurface.performance.contract.test.tsx +++ b/frontend-modern/src/components/Workloads/__tests__/WorkloadsSurface.performance.contract.test.tsx @@ -1425,6 +1425,7 @@ describe('Workloads performance contract', () => { expect(guestDrawerSource).not.toContain('Open related infrastructure'); expect(guestDrawerHistorySource).toContain('data-testid="guest-history-hover-time"'); expect(guestDrawerHistorySource).toContain('data-testid="guest-history-range-control"'); + expect(guestDrawerHistorySource).toContain('h-7 min-h-11 py-0 text-[11px] sm:min-h-0'); expect(guestDrawerHistorySource).toContain('onPointerMove={handleHoverMove}'); expect(guestDrawerHistorySource).toContain('fallbackMetrics'); expect(guestDrawerHistorySource).toContain('props.groups ?? GUEST_DRAWER_HISTORY_GROUPS'); diff --git a/frontend-modern/src/components/shared/SharedPrimitives.guardrails.test.ts b/frontend-modern/src/components/shared/SharedPrimitives.guardrails.test.ts index 516f82b81..f6d06ea39 100644 --- a/frontend-modern/src/components/shared/SharedPrimitives.guardrails.test.ts +++ b/frontend-modern/src/components/shared/SharedPrimitives.guardrails.test.ts @@ -3815,7 +3815,7 @@ describe('shared primitive guardrails', () => { expect(drawerHeaderActionGuard?.canonical?.path).toBe('src/components/shared/Button.tsx'); expect(drawerHeaderActionGuard?.canonical?.export).toBe('DrawerHeaderActionButton'); expect(drawerHeaderActionGuard?.allPatterns).toEqual([ - 'inline-flex h-8 items-center gap-1.5 rounded border border-border bg-surface px-2 text-xs font-medium text-base-content transition-colors hover:bg-surface-hover focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500', + 'inline-flex h-8 min-h-11 min-w-11 items-center gap-1.5 rounded border border-border bg-surface px-2 text-xs font-medium text-base-content transition-colors hover:bg-surface-hover focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500', ]); expect(drawerHeaderActionGuard?.scopes).toEqual([ 'src/components', @@ -3829,7 +3829,7 @@ describe('shared primitive guardrails', () => { expect(drawerHeaderIconGuard?.canonical?.path).toBe('src/components/shared/Button.tsx'); expect(drawerHeaderIconGuard?.canonical?.export).toBe('DrawerHeaderIconButton'); expect(drawerHeaderIconGuard?.allPatterns).toEqual([ - 'inline-flex h-8 w-8 items-center justify-center rounded-md hover:bg-surface-hover hover:text-base-content focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500', + 'inline-flex h-8 w-8 min-h-11 min-w-11 items-center justify-center rounded-md hover:bg-surface-hover hover:text-base-content focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500', ]); expect(drawerHeaderIconGuard?.scopes).toEqual(['src/components', 'src/features', 'src/pages']); expect(drawerHeaderIconGuard?.allowedPaths ?? []).toHaveLength(0); @@ -4147,10 +4147,10 @@ describe('shared primitive guardrails', () => { expect(drawerSource).toContain('DrawerHeaderActionButton'); expect(drawerSource).toContain('DrawerHeaderIconButton'); expect(drawerSource).not.toContain( - 'inline-flex h-8 items-center gap-1.5 rounded border border-border bg-surface px-2 text-xs font-medium text-base-content transition-colors hover:bg-surface-hover focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500', + 'inline-flex h-8 min-h-11 min-w-11 items-center gap-1.5 rounded border border-border bg-surface px-2 text-xs font-medium text-base-content transition-colors hover:bg-surface-hover focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500', ); expect(drawerSource).not.toContain( - 'inline-flex h-8 w-8 items-center justify-center rounded-md hover:bg-surface-hover hover:text-base-content focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500', + 'inline-flex h-8 w-8 min-h-11 min-w-11 items-center justify-center rounded-md hover:bg-surface-hover hover:text-base-content focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500', ); } expect(chatMessagesSource).toContain('@/components/shared/Button'); diff --git a/frontend-modern/src/components/shared/buttonModel.ts b/frontend-modern/src/components/shared/buttonModel.ts index 070e57c06..ec067d9f1 100644 --- a/frontend-modern/src/components/shared/buttonModel.ts +++ b/frontend-modern/src/components/shared/buttonModel.ts @@ -183,10 +183,10 @@ export const getActionIconButtonClass = (options: ActionIconButtonClassOptions = export const DRAWER_HEADER_ACTION_GROUP_CLASS = 'flex shrink-0 items-center gap-1.5'; export const DRAWER_HEADER_ACTION_BUTTON_CLASS = - 'inline-flex h-8 items-center gap-1.5 rounded border border-border bg-surface px-2 text-xs font-medium text-base-content transition-colors hover:bg-surface-hover focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 disabled:cursor-wait disabled:opacity-60'; + 'inline-flex h-8 min-h-11 min-w-11 items-center gap-1.5 rounded border border-border bg-surface px-2 text-xs font-medium text-base-content transition-colors hover:bg-surface-hover focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 disabled:cursor-wait disabled:opacity-60 sm:min-h-0 sm:min-w-0'; export const DRAWER_HEADER_ICON_BUTTON_CLASS = - 'inline-flex h-8 w-8 items-center justify-center rounded-md hover:bg-surface-hover hover:text-base-content focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500'; + 'inline-flex h-8 w-8 min-h-11 min-w-11 items-center justify-center rounded-md hover:bg-surface-hover hover:text-base-content focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 sm:min-h-0 sm:min-w-0'; export const getDrawerHeaderActionGroupClass = (className?: string): string => [DRAWER_HEADER_ACTION_GROUP_CLASS, className].filter(Boolean).join(' '); diff --git a/frontend-modern/src/features/docker/DockerImagesTable.tsx b/frontend-modern/src/features/docker/DockerImagesTable.tsx index 5d8eaea20..bc570a3d0 100644 --- a/frontend-modern/src/features/docker/DockerImagesTable.tsx +++ b/frontend-modern/src/features/docker/DockerImagesTable.tsx @@ -162,7 +162,7 @@ export const DockerImagesTable: Component< kind="numeric-value" sort={sort} sortKey="size" - class="md:w-[12%]" + class="hidden sm:table-cell md:w-[12%]" > Size @@ -225,7 +225,7 @@ export const DockerImagesTable: Component< diff --git a/frontend-modern/src/features/docker/DockerServicesTable.tsx b/frontend-modern/src/features/docker/DockerServicesTable.tsx index f60ee114f..cd9474c95 100644 --- a/frontend-modern/src/features/docker/DockerServicesTable.tsx +++ b/frontend-modern/src/features/docker/DockerServicesTable.tsx @@ -187,12 +187,13 @@ export const DockerServicesTable: Component<{ header={ <> {/* - Desktop widths: Service and Image take the lion's share + Wide desktop widths: Service and Image take the lion's share because their content is long (registry refs, fully qualified service names). Mode / Desired / Running trim to short text and 1-2 digit counts. Update, Ports, and Host get middle slices for rollout state, port lists, and - hostnames. Mobile widths are unchanged. + hostnames. Phones keep the decision-making replica pair; + the basic container tier restores mode, update, and host. */} Image - + @@ -241,7 +247,7 @@ export const DockerServicesTable: Component<{ kind="text" sort={sort} sortKey="update" - class="hidden md:table-cell md:w-[12%]" + class="hidden sm:table-cell md:w-[12%]" > Update @@ -256,7 +262,7 @@ export const DockerServicesTable: Component<{ kind="text" sort={sort} sortKey="host" - class="hidden md:table-cell md:w-[10%]" + class="hidden sm:table-cell md:w-[10%]" > Host @@ -303,12 +309,12 @@ export const DockerServicesTable: Component<{ @@ -318,7 +324,7 @@ export const DockerServicesTable: Component<{ diff --git a/frontend-modern/src/features/docker/DockerSwarmNodesTable.tsx b/frontend-modern/src/features/docker/DockerSwarmNodesTable.tsx index b62cb2999..a4e836e77 100644 --- a/frontend-modern/src/features/docker/DockerSwarmNodesTable.tsx +++ b/frontend-modern/src/features/docker/DockerSwarmNodesTable.tsx @@ -166,7 +166,7 @@ export const DockerSwarmNodesTable: Component = (props) kind="text" sort={sort} sortKey="reachability" - class="hidden md:table-cell md:w-[14%]" + class="md:w-[14%]" > Reachability @@ -190,7 +190,7 @@ export const DockerSwarmNodesTable: Component = (props) kind="numeric-value" sort={sort} sortKey="memory" - class="md:w-[10%]" + class="hidden sm:table-cell md:w-[10%]" > @@ -198,7 +198,7 @@ export const DockerSwarmNodesTable: Component = (props) kind="text" sort={sort} sortKey="address" - class="hidden md:table-cell md:w-[10%]" + class="hidden sm:table-cell md:w-[10%]" > Address @@ -233,7 +233,7 @@ export const DockerSwarmNodesTable: Component = (props) {dockerTextValue(resource.docker?.availability)} @@ -250,12 +250,12 @@ export const DockerSwarmNodesTable: Component = (props) {dockerCpuValue(resource.docker?.nanoCpus)} diff --git a/frontend-modern/src/features/kubernetes/KubernetesClustersTable.tsx b/frontend-modern/src/features/kubernetes/KubernetesClustersTable.tsx index 2ea6e0e45..6e0dfd9d3 100644 --- a/frontend-modern/src/features/kubernetes/KubernetesClustersTable.tsx +++ b/frontend-modern/src/features/kubernetes/KubernetesClustersTable.tsx @@ -315,7 +315,7 @@ export const KubernetesClustersTable: Component<{