diff --git a/frontend-modern/browser-verification.json b/frontend-modern/browser-verification.json index f57203232..4ab69924b 100644 --- a/frontend-modern/browser-verification.json +++ b/frontend-modern/browser-verification.json @@ -1,79 +1,67 @@ { "version": 1, - "base_sha": "2d9ba270651ed5e1028ae4aa15843ab34303c7c9", - "verified_at": "2026-08-06T14:52:48Z", + "base_sha": "bbd6910f490f01035cfc13dd5717078e4427f8c6", + "verified_at": "2026-08-06T15:20:24Z", "result": "passed", "changed_paths": [ - "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/Alerts/AlertResourceTableMobile.tsx", + "frontend-modern/src/components/Alerts/ThresholdsTable.tsx", + "frontend-modern/src/components/Settings/AIModelSelectionSection.tsx", + "frontend-modern/src/components/Settings/AvailabilitySettingsPanel.tsx", + "frontend-modern/src/components/Settings/DataHandlingPanel.tsx", + "frontend-modern/src/components/shared/Form.ts", "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" + "frontend-modern/src/components/shared/filterButtonGroupModel.ts", + "frontend-modern/src/components/shared/statusBadgeModel.ts" ], "content_sha256": { - "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" + "frontend-modern/src/components/Alerts/AlertResourceTableMobile.tsx": "84616766c7196430a7a1995442a9ea0ffefae0e0ca4abda7f0dc268904b008d2", + "frontend-modern/src/components/Alerts/ThresholdsTable.tsx": "d6e93e46de34fb9755c4826aa89e358cf395dd3073907fd39440e6b9b1d8ff21", + "frontend-modern/src/components/Settings/AIModelSelectionSection.tsx": "beefb76f9aaa62222e0e2ff77f178de29358503fe69d3f9418aa3e40efb8b15c", + "frontend-modern/src/components/Settings/AvailabilitySettingsPanel.tsx": "9223a9bdd37dc51919d39139cbcc468c16fcbb8342c8e34f1eace9a4003e0d20", + "frontend-modern/src/components/Settings/DataHandlingPanel.tsx": "01c01935c2d018dd9887333fa764bdf47d1d4bebcdee9cad0d30f1441c7ac21f", + "frontend-modern/src/components/shared/Form.ts": "aa51b7534812bea0b48f4a9f4eb43d213afd22d0553f0aa253da66b959429e6b", + "frontend-modern/src/components/shared/buttonModel.ts": "2a87957549fc36abf67f7c51c3f45728e40f5fdb023081bd683ba3b7e172dac4", + "frontend-modern/src/components/shared/filterButtonGroupModel.ts": "5807252b9ee47e8d9844643cb9029c774a8916588a3baf965c1b1e52ff5d91c4", + "frontend-modern/src/components/shared/statusBadgeModel.ts": "a8df2b2feb02d865bec69563a9d6a2bb5ac57f82bd84f675fbb4a5d7a5d95344" }, "routes": [ - "/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" + "/proxmox/overview", + "/proxmox/storage?tab=disks&source=proxmox-all", + "/proxmox/replication", + "/proxmox/backups?view=coverage", + "/proxmox/backups?view=date", + "/proxmox/ceph", + "/proxmox/mail", + "/alerts/overview", + "/alerts/history", + "/alerts/thresholds", + "/alerts/notifications", + "/alerts/schedule", + "/actions", + "/patrol", + "/settings/infrastructure", + "/settings/monitoring/availability", + "/settings/pulse-intelligence/provider", + "/settings/pulse-intelligence/patrol", + "/settings/pulse-intelligence/assistant", + "/settings/system-general", + "/settings/system-network", + "/settings/system-updates", + "/settings/system-recovery", + "/settings/system-relay", + "/settings/support/diagnostics", + "/settings/support/reporting", + "/settings/support/logs", + "/settings/security/api", + "/settings/security-overview", + "/settings/security-data-handling", + "/settings/security-auth", + "/settings/security-sso", + "/settings/security-roles", + "/settings/security-users", + "/settings/security-audit", + "/settings/security-webhooks" ], "viewports": [ { @@ -94,22 +82,22 @@ } ], "states": [ - "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" + "Mock-data Proxmox overview, storage, replication, backup coverage, backup date, Ceph, and mail tables with dense inventories and query-state variants", + "Alerts overview, history, thresholds, notifications, and schedule plus Actions and Patrol at phone and desktop widths", + "All available Settings routes under mock entitlements, including expected organization-route redirects to Infrastructure", + "Phone layouts prioritizing operator identity, status, health, capacity, and recency while progressively restoring secondary context at tablet and desktop widths", + "Availability checks list and manage dialog with populated form controls, status badges, and row actions", + "Provider and model configuration with cost time-range controls, refresh action, API-key fields, and provider configuration", + "Threshold quick tips, expanded resource groups, global defaults, row overrides, status toggles, and dense mocked resource inventories", + "Consistent full-width main content across platform, Alerts, Actions, Patrol, and Settings surfaces" ], "interactions": [ - "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" + "Navigated every listed route and inspected visible information priority, clipping, document overflow, and page-width stability across 320px, 390px, 768px, and 1280px viewports", + "Added responsive context progressively instead of scaling the application or relying on horizontal table scrolling", + "Opened the Availability manage dialog at 320px, measured its bounds and all form and action controls, then closed it and confirmed focus returned to the exact originating Manage button", + "Selected the 7-day AI cost range and confirmed pressed state, 44px phone controls, and compact desktop density", + "Measured Availability Refresh, Test, Pause, and Manage actions at phone and desktop widths and confirmed the dialog remains inside the 320px viewport", + "Measured Provider Refresh, Threshold dismiss, expand, collapse, numeric input, toggle, and edit controls at phone widths and retained compact desktop sizing", + "Confirmed no horizontal document overflow on the audited routes and interactive states, then reset the viewport and finalized the browser session" ] } diff --git a/frontend-modern/src/components/Alerts/AlertResourceTableMobile.tsx b/frontend-modern/src/components/Alerts/AlertResourceTableMobile.tsx index 3fd466f85..275732225 100644 --- a/frontend-modern/src/components/Alerts/AlertResourceTableMobile.tsx +++ b/frontend-modern/src/components/Alerts/AlertResourceTableMobile.tsx @@ -173,7 +173,7 @@ export function AlertResourceTableMobile(props: AlertResourceTableMobileProps) { value={isOff() ? '' : value()} placeholder={getAlertResourceTableMetricPlaceholder(isOff())} disabled={isOff()} - class={`w-full text-sm p-1 rounded border text-center ${isOff() ? 'bg-surface-hover' : ' border-border'}`} + class={`min-h-11 w-full rounded border p-1 text-center text-sm sm:min-h-0 ${isOff() ? 'bg-surface-hover' : ' border-border'}`} onInput={(e) => { const nextValue = parseFloat(e.currentTarget.value); // An empty box is mid-edit, not a disable request: diff --git a/frontend-modern/src/components/Alerts/ResourceTable.test.tsx b/frontend-modern/src/components/Alerts/ResourceTable.test.tsx index 702836b7e..e87054adc 100644 --- a/frontend-modern/src/components/Alerts/ResourceTable.test.tsx +++ b/frontend-modern/src/components/Alerts/ResourceTable.test.tsx @@ -338,6 +338,9 @@ describe('ResourceTable', () => { expect(alertResourceTableMobileSource).toContain('AlertResourceGroupHeader'); expect(alertResourceTableMobileSource).toContain('FormTextarea'); expect(alertResourceTableMobileSource).toContain('ActionIconButton'); + expect(alertResourceTableMobileSource).toContain( + 'min-h-11 w-full rounded border p-1 text-center text-sm sm:min-h-0', + ); expect(alertResourceTableMobileSource).not.toContain(['<', 'textarea'].join('')); expect(alertResourceTableMobileSource).not.toContain(rawSvgTag); expect(alertResourceGroupHeaderSource).toContain('export function AlertResourceGroupHeader'); diff --git a/frontend-modern/src/components/Alerts/ThresholdsTable.tsx b/frontend-modern/src/components/Alerts/ThresholdsTable.tsx index ee2795aac..0432f9412 100644 --- a/frontend-modern/src/components/Alerts/ThresholdsTable.tsx +++ b/frontend-modern/src/components/Alerts/ThresholdsTable.tsx @@ -92,7 +92,7 @@ export function ThresholdsTable(props: ThresholdsTableProps) { @@ -153,7 +153,7 @@ export function ThresholdsTable(props: ThresholdsTableProps) { diff --git a/frontend-modern/src/components/Alerts/__tests__/ThresholdsTable.test.tsx b/frontend-modern/src/components/Alerts/__tests__/ThresholdsTable.test.tsx index bf7a0c2b7..2d8e43eb4 100644 --- a/frontend-modern/src/components/Alerts/__tests__/ThresholdsTable.test.tsx +++ b/frontend-modern/src/components/Alerts/__tests__/ThresholdsTable.test.tsx @@ -355,6 +355,7 @@ describe('ThresholdsTable basics', () => { it('allows dismissing the help banner', () => { render(() => ); const dismissButton = screen.getByLabelText(/Dismiss tips/i); + expect(dismissButton).toHaveClass('min-h-11', 'min-w-11', 'sm:min-h-0', 'sm:min-w-0'); fireEvent.click(dismissButton); expect(screen.queryByText(/Quick tips:/i)).not.toBeInTheDocument(); }); diff --git a/frontend-modern/src/components/Settings/AIModelSelectionSection.tsx b/frontend-modern/src/components/Settings/AIModelSelectionSection.tsx index 1d9d5332e..32cc9c140 100644 --- a/frontend-modern/src/components/Settings/AIModelSelectionSection.tsx +++ b/frontend-modern/src/components/Settings/AIModelSelectionSection.tsx @@ -507,7 +507,7 @@ export const AIModelSelectionSection: Component = type="button" onClick={state.loadModels} disabled={state.modelsLoading()} - class="inline-flex min-h-10 sm:min-h-9 items-center gap-1 rounded-md px-2 py-1.5 text-sm text-blue-600 dark:text-blue-400 hover:text-blue-800 dark:hover:text-blue-300 disabled:opacity-50" + class="inline-flex min-h-11 sm:min-h-9 items-center gap-1 rounded-md px-2 py-1.5 text-sm text-blue-600 dark:text-blue-400 hover:text-blue-800 dark:hover:text-blue-300 disabled:opacity-50" title="Refresh model list from all configured providers" > { type="button" variant="secondary" size="mdCompact" - class="min-h-9 gap-2" + class="min-h-11 gap-2 sm:min-h-9" onClick={() => void loadTargets()} disabled={loading()} > @@ -214,7 +214,7 @@ export const AvailabilitySettingsPanel: Component = () => { type="button" variant="primary" size="mdCompact" - class="min-h-9 gap-2" + class="min-h-11 gap-2 sm:min-h-9" onClick={() => openAddDialog('service')} > @@ -306,7 +306,7 @@ export const AvailabilitySettingsPanel: Component = () => { type="button" variant="outline" size="xs" - class="min-h-8" + class="min-h-11 sm:min-h-8" onClick={() => void testTarget(target)} disabled={pending()} > @@ -316,7 +316,7 @@ export const AvailabilitySettingsPanel: Component = () => { type="button" variant="outline" size="xs" - class="min-h-8" + class="min-h-11 sm:min-h-8" onClick={() => void toggleTarget(target)} disabled={pending()} > @@ -326,7 +326,7 @@ export const AvailabilitySettingsPanel: Component = () => { type="button" variant="outline" size="xs" - class="min-h-8" + class="min-h-11 sm:min-h-8" onClick={() => { setDeleteConfirmingId(null); setDialog({ mode: 'edit', target }); diff --git a/frontend-modern/src/components/Settings/DataHandlingPanel.tsx b/frontend-modern/src/components/Settings/DataHandlingPanel.tsx index 97dff3175..af1a73b7e 100644 --- a/frontend-modern/src/components/Settings/DataHandlingPanel.tsx +++ b/frontend-modern/src/components/Settings/DataHandlingPanel.tsx @@ -160,6 +160,7 @@ export const DataHandlingPanel: Component = () => { type="button" variant="outline" size="sm" + class="min-h-11 sm:min-h-0" isLoading={resources.loading()} onClick={() => { void resources.refetch(); diff --git a/frontend-modern/src/components/Settings/__tests__/AvailabilitySettingsPanel.test.tsx b/frontend-modern/src/components/Settings/__tests__/AvailabilitySettingsPanel.test.tsx index 5a0296e34..63dc90b25 100644 --- a/frontend-modern/src/components/Settings/__tests__/AvailabilitySettingsPanel.test.tsx +++ b/frontend-modern/src/components/Settings/__tests__/AvailabilitySettingsPanel.test.tsx @@ -110,6 +110,18 @@ describe('AvailabilitySettingsPanel', () => { expect(screen.getByText('TCP 1883')).toBeInTheDocument(); expect(screen.getByText('http://service.local/health')).toBeInTheDocument(); expect(screen.getByText('Online ยท 8 ms')).toBeInTheDocument(); + expect(screen.getAllByRole('button', { name: 'Test' })[0]).toHaveClass( + 'min-h-11', + 'sm:min-h-8', + ); + expect(screen.getAllByRole('button', { name: 'Pause' })[0]).toHaveClass( + 'min-h-11', + 'sm:min-h-8', + ); + expect(screen.getAllByRole('button', { name: 'Manage' })[0]).toHaveClass( + 'min-h-11', + 'sm:min-h-8', + ); }); it('attributes probe-reported checks to the agent host that ran them', async () => { diff --git a/frontend-modern/src/components/Settings/__tests__/DataHandlingPanel.test.tsx b/frontend-modern/src/components/Settings/__tests__/DataHandlingPanel.test.tsx index 37b59ff49..1df64b9ce 100644 --- a/frontend-modern/src/components/Settings/__tests__/DataHandlingPanel.test.tsx +++ b/frontend-modern/src/components/Settings/__tests__/DataHandlingPanel.test.tsx @@ -56,6 +56,10 @@ describe('DataHandlingPanel', () => { '/settings/infrastructure', ); expect(screen.queryByText('Governed Resources')).not.toBeInTheDocument(); + expect(screen.getByRole('button', { name: 'Refresh' })).toHaveClass( + 'min-h-11', + 'sm:min-h-0', + ); }); it('shows policy posture metrics once resources exist', () => { diff --git a/frontend-modern/src/components/shared/Button.test.tsx b/frontend-modern/src/components/shared/Button.test.tsx index 435f20a4f..063e0fdc4 100644 --- a/frontend-modern/src/components/shared/Button.test.tsx +++ b/frontend-modern/src/components/shared/Button.test.tsx @@ -51,9 +51,9 @@ describe('Button', () => { expect(buttonModelSource).toContain('export const BUTTON_SIZE_CLASSES'); expect(buttonModelSource).toContain("xs: 'px-2.5 py-1 text-xs'"); expect(buttonModelSource).toContain("mdCompact: 'px-3 py-2 text-sm'"); - expect(buttonModelSource).toContain("settingsAction: 'min-h-10 px-3 py-2 text-sm sm:min-h-9'"); + expect(buttonModelSource).toContain("settingsAction: 'min-h-11 px-3 py-2 text-sm sm:min-h-9'"); expect(buttonModelSource).toContain( - "settingsActionXs: 'min-h-10 px-3 py-2 text-xs sm:min-h-9'", + "settingsActionXs: 'min-h-11 px-3 py-2 text-xs sm:min-h-9'", ); expect(buttonModelSource).toContain("chip: 'gap-1 px-1.5 py-0.5 text-[10px]'"); expect(buttonModelSource).toContain("iconMd: 'h-9 w-9 p-0'"); @@ -128,12 +128,12 @@ describe('Button', () => { )); const saveButton = screen.getByRole('button', { name: 'Save source' }); - expect(saveButton).toHaveClass('min-h-10'); + expect(saveButton).toHaveClass('min-h-11'); expect(saveButton).toHaveClass('sm:min-h-9'); expect(saveButton).toHaveClass('bg-blue-600'); const removeButton = screen.getByRole('button', { name: 'Remove source' }); - expect(removeButton).toHaveClass('min-h-10'); + expect(removeButton).toHaveClass('min-h-11'); expect(removeButton).toHaveClass('border-rose-300'); expect(removeButton).toHaveClass('text-rose-700'); @@ -164,7 +164,7 @@ describe('Button', () => { expect(infoButton).toHaveClass('text-blue-700'); const previewButton = screen.getByRole('button', { name: 'Preview payload' }); - expect(previewButton).toHaveClass('min-h-10'); + expect(previewButton).toHaveClass('min-h-11'); expect(previewButton).toHaveClass('text-xs'); expect(previewButton).toHaveClass('px-3'); }); diff --git a/frontend-modern/src/components/shared/FilterButtonGroup.test.tsx b/frontend-modern/src/components/shared/FilterButtonGroup.test.tsx index a155eedc3..8c1c8ee11 100644 --- a/frontend-modern/src/components/shared/FilterButtonGroup.test.tsx +++ b/frontend-modern/src/components/shared/FilterButtonGroup.test.tsx @@ -158,6 +158,27 @@ describe('FilterButtonGroup', () => { expect(inactiveButton.className).toContain('text-muted'); }); + it('keeps every filter option touch-sized on phones and compact above the phone breakpoint', () => { + render(() => ( + undefined} + variant="compact" + /> + )); + + const rangeButton = screen.getByRole('button', { name: '1d' }); + expect(rangeButton).toHaveClass('min-h-11', 'sm:min-h-0'); + expect(filterButtonGroupModelSource).toContain('sm:min-h-9'); + expect(filterButtonGroupModelSource).toContain('sm:min-h-10'); + expect(filterButtonGroupModelSource).toContain('sm:min-h-8'); + }); + it('keeps visual icon labels on one compact line', () => { render(() => ( { const select = screen.getByLabelText('Delivery mode'); expect(select).toBe(screen.getByTestId('delivery-mode')); expect(select).toHaveValue('cli'); + expect(select).toHaveClass('min-h-11', 'sm:min-h-9'); }); it('preserves explicit ids and compact styling hooks', () => { diff --git a/frontend-modern/src/components/shared/StatusBadge.test.tsx b/frontend-modern/src/components/shared/StatusBadge.test.tsx index 12302ad6a..00a433b17 100644 --- a/frontend-modern/src/components/shared/StatusBadge.test.tsx +++ b/frontend-modern/src/components/shared/StatusBadge.test.tsx @@ -56,7 +56,9 @@ describe('StatusBadge', () => { render(() => ); - fireEvent.click(screen.getByRole('button', { name: 'Enabled' })); + const button = screen.getByRole('button', { name: 'Enabled' }); + expect(button).toHaveClass('min-h-11', 'sm:min-h-0'); + fireEvent.click(button); expect(onToggle).toHaveBeenCalledTimes(1); }); }); diff --git a/frontend-modern/src/components/shared/buttonModel.ts b/frontend-modern/src/components/shared/buttonModel.ts index ec067d9f1..dff37b411 100644 --- a/frontend-modern/src/components/shared/buttonModel.ts +++ b/frontend-modern/src/components/shared/buttonModel.ts @@ -54,8 +54,8 @@ export const BUTTON_SIZE_CLASSES: Record = { xs: 'px-2.5 py-1 text-xs', sm: 'px-2.5 py-1.5 text-xs', mdCompact: 'px-3 py-2 text-sm', - settingsAction: 'min-h-10 px-3 py-2 text-sm sm:min-h-9', - settingsActionXs: 'min-h-10 px-3 py-2 text-xs sm:min-h-9', + settingsAction: 'min-h-11 px-3 py-2 text-sm sm:min-h-9', + settingsActionXs: 'min-h-11 px-3 py-2 text-xs sm:min-h-9', md: 'px-4 py-2 text-sm', lg: 'px-6 py-3 text-base', icon: 'p-2', diff --git a/frontend-modern/src/components/shared/filterButtonGroupModel.ts b/frontend-modern/src/components/shared/filterButtonGroupModel.ts index cea112db4..71148677a 100644 --- a/frontend-modern/src/components/shared/filterButtonGroupModel.ts +++ b/frontend-modern/src/components/shared/filterButtonGroupModel.ts @@ -81,7 +81,7 @@ export function getFilterButtonGroupButtonClass( ): string { if (variant === 'settings') { return [ - 'flex items-center justify-center gap-1.5 min-h-10 sm:min-h-9 px-3 py-2 text-sm rounded-md transition-all whitespace-nowrap outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2', + 'flex min-h-11 items-center justify-center gap-1.5 px-3 py-2 text-sm rounded-md transition-all whitespace-nowrap outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2 sm:min-h-9', active ? 'bg-surface text-base-content shadow-sm' : 'text-muted hover:text-base-content', disabled ? 'opacity-60 cursor-not-allowed' : '', ].join(' '); @@ -89,7 +89,7 @@ export function getFilterButtonGroupButtonClass( if (variant === 'prominent') { return [ - 'w-full flex items-center justify-center gap-2 min-h-10 rounded-md border px-4 py-2.5 text-sm font-medium transition-all whitespace-nowrap outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2', + 'flex min-h-11 w-full items-center justify-center gap-2 rounded-md border px-4 py-2.5 text-sm font-medium transition-all whitespace-nowrap outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2 sm:min-h-10', active ? 'bg-blue-50 border-blue-500 text-blue-700 dark:bg-blue-900 dark:text-blue-300 dark:border-blue-500' : 'border-border text-base-content hover:bg-surface-alt', @@ -99,7 +99,7 @@ export function getFilterButtonGroupButtonClass( if (variant === 'compact') { return [ - 'inline-flex items-center justify-center gap-1.5 rounded px-2.5 py-1 text-xs font-medium transition-all whitespace-nowrap outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2', + 'inline-flex min-h-11 items-center justify-center gap-1.5 rounded px-2.5 py-1 text-xs font-medium transition-all whitespace-nowrap outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2 sm:min-h-0', active ? `bg-surface shadow-sm ring-1 ${activeToneClassByOptionTone[tone]}` : 'text-muted hover:bg-surface-hover hover:text-base-content', @@ -109,7 +109,7 @@ export function getFilterButtonGroupButtonClass( if (variant === 'segmented') { return [ - 'flex-1 min-h-8 px-2 py-1.5 text-xs font-semibold rounded-md transition-all whitespace-nowrap outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2', + 'min-h-11 flex-1 rounded-md px-2 py-1.5 text-xs font-semibold transition-all whitespace-nowrap outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2 sm:min-h-8', active ? 'bg-surface text-blue-600 shadow-sm dark:text-blue-400' : 'text-muted hover:text-base-content hover:bg-surface-hover', @@ -118,7 +118,7 @@ export function getFilterButtonGroupButtonClass( } return [ - 'flex flex-1 justify-center sm:flex-none sm:justify-start items-center gap-2 px-3 sm:px-4 py-2.5 sm:py-2 text-sm font-medium rounded-md transition-all whitespace-nowrap outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2', + 'flex min-h-11 flex-1 items-center justify-center gap-2 rounded-md px-3 py-2.5 text-sm font-medium transition-all whitespace-nowrap outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2 sm:min-h-0 sm:flex-none sm:justify-start sm:px-4 sm:py-2', active ? 'bg-surface border border-border text-blue-600 dark:text-blue-400 shadow-sm' : 'text-muted border border-transparent hover:text-base-content hover:bg-surface-hover', diff --git a/frontend-modern/src/components/shared/statusBadgeModel.ts b/frontend-modern/src/components/shared/statusBadgeModel.ts index ad438f88b..94a70ccdd 100644 --- a/frontend-modern/src/components/shared/statusBadgeModel.ts +++ b/frontend-modern/src/components/shared/statusBadgeModel.ts @@ -13,7 +13,7 @@ export interface StatusBadgeProps { } const STATUS_BADGE_BASE_CLASS = - 'inline-flex items-center justify-center text-xs font-medium rounded-md transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-1 focus-visible:ring-blue-400'; + 'inline-flex min-h-11 items-center justify-center rounded-md text-xs font-medium transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-1 focus-visible:ring-blue-400 sm:min-h-0'; const STATUS_BADGE_PADDING_BY_SIZE: Record = { sm: 'px-2 py-0.5', md: 'px-2.5 py-1',