fix(ui): improve mobile settings touch targets

Contract-Neutral: responsive presentation and touch-target adjustments only; no API or payload changes
This commit is contained in:
rcourtman
2026-08-06 16:23:33 +01:00
parent bbd6910f49
commit 93e4b764ff
18 changed files with 140 additions and 107 deletions
+70 -82
View File
@@ -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"
]
}
@@ -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:
@@ -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');
@@ -92,7 +92,7 @@ export function ThresholdsTable(props: ThresholdsTableProps) {
<button
type="button"
onClick={state.dismissHelpBanner}
class="absolute top-2 right-2 p-1 rounded-md text-blue-400 hover:text-blue-600 dark:text-blue-500 dark:hover:text-blue-300 hover:bg-blue-100 dark:hover:bg-blue-900 opacity-0 group-hover:opacity-100 transition-opacity"
class="absolute right-1 top-1 inline-flex min-h-11 min-w-11 items-center justify-center rounded-md p-1 text-blue-500 transition-colors hover:bg-blue-100 hover:text-blue-700 dark:text-blue-400 dark:hover:bg-blue-900 dark:hover:text-blue-200 sm:right-2 sm:top-2 sm:min-h-0 sm:min-w-0 sm:opacity-0 sm:group-hover:opacity-100"
title={state.getAlertThresholdsHelpDismissLabel()}
aria-label={state.getAlertThresholdsHelpDismissLabel()}
>
@@ -145,7 +145,7 @@ export function ThresholdsTable(props: ThresholdsTableProps) {
<button
type="button"
onClick={state.expandAll}
class="text-xs px-2 py-1 hover:text-muted hover:bg-surface-hover rounded transition-colors"
class="min-h-11 rounded px-2 py-1 text-xs transition-colors hover:bg-surface-hover hover:text-muted sm:min-h-0"
>
Expand all
</button>
@@ -153,7 +153,7 @@ export function ThresholdsTable(props: ThresholdsTableProps) {
<button
type="button"
onClick={state.collapseAll}
class="text-xs px-2 py-1 hover:text-muted hover:bg-surface-hover rounded transition-colors"
class="min-h-11 rounded px-2 py-1 text-xs transition-colors hover:bg-surface-hover hover:text-muted sm:min-h-0"
>
Collapse all
</button>
@@ -355,6 +355,7 @@ describe('ThresholdsTable basics', () => {
it('allows dismissing the help banner', () => {
render(() => <ThresholdsTable {...(baseProps() as any)} />);
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();
});
@@ -507,7 +507,7 @@ export const AIModelSelectionSection: Component<AIModelSelectionSectionProps> =
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"
>
<svg
@@ -203,7 +203,7 @@ export const AvailabilitySettingsPanel: Component = () => {
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')}
>
<Plus class="h-4 w-4" />
@@ -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 });
@@ -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();
@@ -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 () => {
@@ -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', () => {
@@ -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');
});
@@ -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(() => (
<FilterButtonGroup
ariaLabel="Range"
options={[
{ value: '1d', label: '1d' },
{ value: '7d', label: '7d' },
]}
value="1d"
onChange={() => 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(() => (
<FilterButtonGroup
@@ -2,7 +2,7 @@ const baseField = 'flex flex-col gap-1';
const baseLabel = 'text-sm font-medium text-base-content';
const baseHelp = 'text-xs text-muted';
const baseControl = [
'w-full min-h-10 sm:min-h-9 rounded-md border border-border bg-surface px-3 py-2.5 text-sm text-base-content',
'w-full min-h-11 sm:min-h-9 rounded-md border border-border bg-surface px-3 py-2.5 text-sm text-base-content',
'focus:outline-none focus:ring-0 focus:border-blue-500 transition-colors',
].join(' ');
const baseCheckbox =
@@ -19,6 +19,7 @@ describe('FormSelect', () => {
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', () => {
@@ -56,7 +56,9 @@ describe('StatusBadge', () => {
render(() => <StatusBadge isEnabled={true} onToggle={onToggle} />);
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);
});
});
@@ -54,8 +54,8 @@ export const BUTTON_SIZE_CLASSES: Record<ButtonSize, string> = {
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',
@@ -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',
@@ -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<StatusBadgeSize, string> = {
sm: 'px-2 py-0.5',
md: 'px-2.5 py-1',