mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-16 08:19:43 +00:00
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:
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user