fix(ui): standardize mobile interaction targets

Contract-Neutral: responsive interaction sizing and popover positioning only; no API or payload changes
This commit is contained in:
rcourtman
2026-08-06 17:03:28 +01:00
parent a94980797b
commit 2d04ffaa3b
25 changed files with 207 additions and 67 deletions
+90 -23
View File
@@ -1,46 +1,113 @@
{ {
"version": 1, "version": 1,
"base_sha": "93e4b764ffae445e3b501d4bce17c50a9e5ec4fb", "base_sha": "a94980797bba025285cb7fcb793a7866c4c62e0b",
"verified_at": "2026-08-06T15:37:44Z", "verified_at": "2026-08-06T16:01:54Z",
"result": "passed", "result": "passed",
"changed_paths": [ "changed_paths": [
"frontend-modern/src/components/Workloads/useWorkloadsControlsState.ts" "frontend-modern/src/components/AI/FindingsPanel.tsx",
"frontend-modern/src/components/Settings/SettingsPageShell.tsx",
"frontend-modern/src/components/shared/FilterToolbar.tsx",
"frontend-modern/src/components/shared/SearchField.tsx",
"frontend-modern/src/components/shared/SearchTipsPopover.tsx",
"frontend-modern/src/components/shared/Subtabs.tsx",
"frontend-modern/src/components/shared/SummaryRowActionButton.tsx",
"frontend-modern/src/components/shared/buttonModel.ts",
"frontend-modern/src/components/shared/searchInputEnhancementsModel.ts",
"frontend-modern/src/components/shared/searchTipsPopoverModel.ts",
"frontend-modern/src/components/shared/toggleModel.ts",
"frontend-modern/src/components/shared/useSearchTipsPopoverState.ts",
"frontend-modern/src/features/patrol/PatrolIntelligenceHeader.tsx"
], ],
"content_sha256": { "content_sha256": {
"frontend-modern/src/components/Workloads/useWorkloadsControlsState.ts": "d64a38fee5fef5c62b04814738e7d9342123114d63c0b7559268dbf1d8df98ef" "frontend-modern/src/components/AI/FindingsPanel.tsx": "fa76f7946c15b42e529338419dae5bc33395ae91440c404d3f50afb243050ed8",
"frontend-modern/src/components/Settings/SettingsPageShell.tsx": "cee289c0db5a36bbac8d323cf0435cd0b3ff75384c6e04818ebc71a357a29147",
"frontend-modern/src/components/shared/FilterToolbar.tsx": "7b18839d12aba7cef74d934d98658a56bcc26ad46d728f86207d2d0dc25c7dd0",
"frontend-modern/src/components/shared/SearchField.tsx": "9dab6fb6e8f35a8633a7777796df272263d375ee08ab6cf039c380c868fcdfd3",
"frontend-modern/src/components/shared/SearchTipsPopover.tsx": "22a8dfa12f5ac98c0dd4886d72f9d384805169d2b712cde9f2c8448b6387f1ad",
"frontend-modern/src/components/shared/Subtabs.tsx": "cebd908970f3512001f96ea065d24624496f2ed93182b52b8a4b615a35d3735d",
"frontend-modern/src/components/shared/SummaryRowActionButton.tsx": "e35943a58467e4fadc9c6d06fd6a484c0179b653c2d03851ee5bc51d988132a3",
"frontend-modern/src/components/shared/buttonModel.ts": "d6847c1d6e41165165c1a8617b7a5ea63d61fdc0ecc30c876d745949d551c7bd",
"frontend-modern/src/components/shared/searchInputEnhancementsModel.ts": "f1fc385fa542e59c14142d2eec6e5e5309b7f747341480de9c35d7c96240ce18",
"frontend-modern/src/components/shared/searchTipsPopoverModel.ts": "bd8e76fcdbf74c4111603729901b57f4e4ffff5d8b0660b17a4acb77338b4262",
"frontend-modern/src/components/shared/toggleModel.ts": "2f587b9f3bc2d2bf7c57ce4ed5406075bc4a856fd64262e674fa7507c86c3f85",
"frontend-modern/src/components/shared/useSearchTipsPopoverState.ts": "155d7479e39171f9f6c4dc324de2349a24b81b5a5c24989150dc6fc1b1b9b51d",
"frontend-modern/src/features/patrol/PatrolIntelligenceHeader.tsx": "de74125176e6d7421cec5717c963cd8e992f0ef0f11bf9304bb7b75c6f844658"
}, },
"routes": [ "routes": [
"/vmware", "/proxmox/overview",
"/proxmox?status=all" "/proxmox/storage?tab=disks&source=proxmox-all",
"/proxmox/replication",
"/proxmox/backups?view=coverage",
"/proxmox/backups?view=date",
"/proxmox/ceph",
"/proxmox/mail",
"/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",
"/alerts/thresholds",
"/actions",
"/patrol",
"/settings/monitoring/availability",
"/settings/pulse-intelligence/provider",
"/docs",
"/preview/setup-complete",
"/this-route-does-not-exist"
], ],
"viewports": [ "viewports": [
{ {
"width": 1900, "width": 320,
"height": 1100 "height": 900
}, },
{ {
"width": 1280, "width": 1280,
"height": 800 "height": 900
}, },
{ {
"width": 390, "width": 1920,
"height": 844 "height": 1080
} }
], ],
"states": [ "states": [
"vSphere VM table with a saved workloadsHiddenColumns:vmware-vms preference that predates the Backup default-hide: before the fix the Backup column rendered on all 18 VM rows, every cell reading None", "Mock-data tables across every Proxmox, Docker, Kubernetes, TrueNAS, vSphere, Machines, and Availability route",
"vSphere VM table after the one-time migration: Backup absent from the header, zero Backup cells, hidden list carries backup and the default-hidden-applied marker records it", "Phone inventory controls with table expanders, search history, search tips, filters, saved views, sort controls, and row action menus",
"vSphere VM table with Backup manually re-shown from the Columns control: 18 rows, every cell None", "Actions Open and History states plus a governed action review dialog with populated approval evidence",
"vSphere VM table reloaded after the manual re-show: Backup still visible, marker stops the migration re-hiding a column the user restored", "Patrol runtime controls, attention filters, findings sort controls, issue review buttons, and compact toggle hit area",
"Proxmox workload table unaffected at desktop width: Backup column still visible with real ages (1d, 9d, 12d, 15d, 21d) alongside fresh and None cells", "Settings Availability and Provider controls plus the compact Settings return action",
"Proxmox workload table at 390px: hidden list unchanged, backup never added to the Proxmox scope", "Search tips popover at phone and desktop widths, including viewport-edge and page-scroll positioning",
"vSphere VM table at 390px: 32 rows render, Backup hidden, no horizontal body overflow" "Machines table with default columns, every optional column enabled, responsive column priority, and restored user choices",
"Documentation, setup-complete preview, and not-found utility routes"
], ],
"interactions": [ "interactions": [
"Opened the View preferences popover on /vmware and read the Columns control reporting 4 hidden", "Scanned every listed local route at 320px for horizontal document overflow and visible primary controls below a 44px phone target",
"Opened the Columns multiselect and clicked the Backup toggle to re-show the column", "Opened and closed Docker and Machines search-tips popovers, measured their viewport bounds, and confirmed phone and desktop positioning remains fully visible",
"Reloaded /vmware after the manual re-show to confirm the migration marker holds", "Opened Docker filters and confirmed the add-filter selector is 44px tall and remains inside the phone viewport",
"Seeded and cleared the workloadsHiddenColumns:vmware-vms scope keys to reproduce the pre-migration state at each viewport", "Expanded and collapsed Docker host details and confirmed the disclosure control is 44px without introducing document overflow",
"Navigated between /vmware and /proxmox?status=all to compare the Backup column across scopes" "Switched Actions between Open and History, opened a populated action review dialog, measured its 288px phone bounds and 44px controls, and confirmed close restores focus to the originating action",
"Measured Patrol phone actions, filters, selects, findings review controls, and the compact switch's extended hit area, then confirmed compact desktop density remains unchanged",
"Enabled all optional Machines columns, confirmed priority columns only at 320px, useful operational columns at 1280px, all 16 columns at 1920px, and no horizontal document scrolling at any width",
"Restored the original Machines column choices after the stress test, reset the viewport override, and finalized the browser session"
] ]
} }
@@ -1336,7 +1336,7 @@ export const FindingsPanel: Component<FindingsPanelProps> = (props) => {
aria-expanded={expandedId() === finding.id} aria-expanded={expandedId() === finding.id}
aria-controls={`finding-${finding.id}-details`} aria-controls={`finding-${finding.id}-details`}
onClick={toggleExpanded} onClick={toggleExpanded}
class="inline-flex items-center gap-1 rounded border border-border bg-surface px-2 py-1 text-xs font-medium text-muted transition-colors hover:bg-surface-hover hover:text-base-content focus:outline-none focus-visible:ring-2 focus-visible:ring-primary/40" class="inline-flex min-h-11 items-center gap-1 rounded border border-border bg-surface px-2 py-1 text-xs font-medium text-muted transition-colors hover:bg-surface-hover hover:text-base-content focus:outline-none focus-visible:ring-2 focus-visible:ring-primary/40 sm:min-h-0"
title={ title={
isPatrolFindingsSource() isPatrolFindingsSource()
? expandedId() === finding.id ? expandedId() === finding.id
@@ -2049,7 +2049,7 @@ export const FindingsPanel: Component<FindingsPanelProps> = (props) => {
fieldBaseClass="contents" fieldBaseClass="contents"
value={sortBy()} value={sortBy()}
onChange={(e) => setSortBy(e.currentTarget.value as 'severity' | 'time')} onChange={(e) => setSortBy(e.currentTarget.value as 'severity' | 'time')}
selectBaseClass="text-xs px-2 py-1 rounded border border-border bg-surface" selectBaseClass="min-h-11 rounded border border-border bg-surface px-2 py-1 text-xs sm:min-h-0"
> >
<option value="severity">By Severity</option> <option value="severity">By Severity</option>
<option value="time">By Time</option> <option value="time">By Time</option>
@@ -942,6 +942,16 @@ describe('aiFindingPresentation', () => {
expect(findingsPanelSource).not.toContain('<select'); expect(findingsPanelSource).not.toContain('<select');
}); });
it('keeps Patrol review and sort controls touch-sized on phones', () => {
expect(findingsPanelSource).toContain(
'class="inline-flex min-h-11 items-center gap-1 rounded border border-border',
);
expect(findingsPanelSource).toContain(
'selectBaseClass="min-h-11 rounded border border-border',
);
expect(findingsPanelSource).toContain('sm:min-h-0');
});
it('hides the filter bar when there are no Patrol findings or special buckets to navigate', () => { it('hides the filter bar when there are no Patrol findings or special buckets to navigate', () => {
expect(findingsPanelSource).toContain('const showFilterControls = createMemo('); expect(findingsPanelSource).toContain('const showFilterControls = createMemo(');
expect(findingsPanelSource).toContain('const hasUnknownRunSnapshot = createMemo('); expect(findingsPanelSource).toContain('const hasUnknownRunSnapshot = createMemo(');
@@ -236,7 +236,7 @@ export const SettingsPageShell: Component<SettingsPageShellProps> = (props) => {
<button <button
type="button" type="button"
onClick={() => props.setIsMobileMenuOpen(true)} onClick={() => props.setIsMobileMenuOpen(true)}
class="flex min-h-10 items-center gap-1.5 rounded-md px-2 py-1.5 font-medium text-blue-600 transition-colors active:bg-blue-50 dark:text-blue-400 dark:active:bg-blue-900" class="flex min-h-11 items-center gap-1.5 rounded-md px-2 py-1.5 font-medium text-blue-600 transition-colors active:bg-blue-50 dark:text-blue-400 dark:active:bg-blue-900"
> >
<svg <svg
class="h-5 w-5 -ml-1 flex-shrink-0" class="h-5 w-5 -ml-1 flex-shrink-0"
@@ -45,6 +45,7 @@ describe('SettingsPageShell mobile navigation', () => {
expect(screen.getAllByText('Infrastructure')).toHaveLength(2); expect(screen.getAllByText('Infrastructure')).toHaveLength(2);
expect(screen.getAllByText('API Access')).toHaveLength(1); expect(screen.getAllByText('API Access')).toHaveLength(1);
expect(screen.getByRole('button', { name: 'Settings' })).toHaveClass('min-h-11');
setActiveTab('api'); setActiveTab('api');
@@ -49,14 +49,18 @@ describe('Button', () => {
); );
expect(buttonModelSource).toContain('dangerOutline:'); expect(buttonModelSource).toContain('dangerOutline:');
expect(buttonModelSource).toContain('export const BUTTON_SIZE_CLASSES'); expect(buttonModelSource).toContain('export const BUTTON_SIZE_CLASSES');
expect(buttonModelSource).toContain("xs: 'px-2.5 py-1 text-xs'"); expect(buttonModelSource).toContain("xs: 'min-h-11 px-2.5 py-1 text-xs sm:min-h-0'");
expect(buttonModelSource).toContain("mdCompact: 'px-3 py-2 text-sm'"); expect(buttonModelSource).toContain(
"mdCompact: 'min-h-11 px-3 py-2 text-sm sm:min-h-0'",
);
expect(buttonModelSource).toContain("settingsAction: 'min-h-11 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( expect(buttonModelSource).toContain(
"settingsActionXs: 'min-h-11 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("chip: 'gap-1 px-1.5 py-0.5 text-[10px]'");
expect(buttonModelSource).toContain("iconMd: 'h-9 w-9 p-0'"); expect(buttonModelSource).toContain(
"iconMd: 'h-9 w-9 min-h-11 min-w-11 p-0 sm:min-h-0 sm:min-w-0'",
);
expect(buttonModelSource).toContain('DRAWER_HEADER_ACTION_BUTTON_CLASS'); expect(buttonModelSource).toContain('DRAWER_HEADER_ACTION_BUTTON_CLASS');
expect(buttonModelSource).toContain('DRAWER_HEADER_ICON_BUTTON_CLASS'); expect(buttonModelSource).toContain('DRAWER_HEADER_ICON_BUTTON_CLASS');
expect(buttonModelSource).toContain('getDrawerHeaderActionButtonClass'); expect(buttonModelSource).toContain('getDrawerHeaderActionButtonClass');
@@ -76,6 +80,8 @@ describe('Button', () => {
expect(button).toHaveClass('bg-surface'); expect(button).toHaveClass('bg-surface');
expect(button).toHaveClass('border-border'); expect(button).toHaveClass('border-border');
expect(button).toHaveClass('px-3'); expect(button).toHaveClass('px-3');
expect(button).toHaveClass('min-h-11');
expect(button).toHaveClass('sm:min-h-0');
button.click(); button.click();
expect(onClick).toHaveBeenCalledTimes(1); expect(onClick).toHaveBeenCalledTimes(1);
@@ -157,6 +163,7 @@ describe('Button', () => {
expect(removeMemberButton).toHaveClass('border-transparent'); expect(removeMemberButton).toHaveClass('border-transparent');
expect(removeMemberButton).toHaveClass('text-red-600'); expect(removeMemberButton).toHaveClass('text-red-600');
expect(removeMemberButton).toHaveClass('text-xs'); expect(removeMemberButton).toHaveClass('text-xs');
expect(removeMemberButton).toHaveClass('min-h-11');
const infoButton = screen.getByRole('button', { name: 'View reference' }); const infoButton = screen.getByRole('button', { name: 'View reference' });
expect(infoButton).toHaveClass('border-blue-200'); expect(infoButton).toHaveClass('border-blue-200');
@@ -256,7 +256,8 @@ describe('FilterHeader', () => {
expect(filterPanelClass).toContain('z-[80]'); expect(filterPanelClass).toContain('z-[80]');
expect(filterPanelClass).not.toContain('w-[min(40rem,calc(100vw-2rem))]'); expect(filterPanelClass).not.toContain('w-[min(40rem,calc(100vw-2rem))]');
expect(filterPanelDefaultWidthClass).toContain('w-[min(40rem,calc(100vw-2rem))]'); expect(filterPanelDefaultWidthClass).toContain('w-[min(40rem,calc(100vw-2rem))]');
expect(filterToolbarSource).toContain("'flex min-h-10 items-center gap-1.5"); expect(filterToolbarSource).toContain("'flex min-h-11 items-center gap-1.5");
expect(filterToolbarSource).toContain('sm:h-7 sm:min-h-0');
}); });
it('keeps peer popover triggers aligned and exposes their disclosure state', () => { it('keeps peer popover triggers aligned and exposes their disclosure state', () => {
@@ -273,7 +274,9 @@ describe('FilterHeader', () => {
const trigger = screen.getByRole('button', { name: 'Saved' }); const trigger = screen.getByRole('button', { name: 'Saved' });
const chevron = trigger.querySelector('svg'); const chevron = trigger.querySelector('svg');
expect(trigger).toHaveClass('h-7'); expect(trigger).toHaveClass('min-h-11');
expect(trigger).toHaveClass('sm:h-7');
expect(trigger).toHaveClass('sm:min-h-0');
expect(trigger).toHaveClass('text-base-content'); expect(trigger).toHaveClass('text-base-content');
expect(trigger).toHaveAttribute('aria-expanded', 'false'); expect(trigger).toHaveAttribute('aria-expanded', 'false');
expect(chevron).not.toHaveClass('rotate-180'); expect(chevron).not.toHaveClass('rotate-180');
@@ -17,10 +17,12 @@ export const filterGroupClass =
'inline-flex items-center gap-1 rounded-md bg-surface-hover p-0.5 ring-1 ring-border-subtle'; 'inline-flex items-center gap-1 rounded-md bg-surface-hover p-0.5 ring-1 ring-border-subtle';
export const filterLabelClass = export const filterLabelClass =
'px-1.5 text-[9px] font-semibold uppercase tracking-wide text-muted'; 'px-1.5 text-[9px] font-semibold uppercase tracking-wide text-muted';
export const filterToolbarControlClass = 'h-7 rounded-md text-xs font-medium'; export const filterToolbarControlClass =
'min-h-11 rounded-md text-xs font-medium sm:h-7 sm:min-h-0';
export const filterActionButtonClass = `inline-flex items-center gap-1.5 ${filterToolbarControlClass} bg-surface-hover px-2.5 text-muted ring-1 ring-border-subtle transition-colors hover:bg-surface hover:text-base-content`; export const filterActionButtonClass = `inline-flex items-center gap-1.5 ${filterToolbarControlClass} bg-surface-hover px-2.5 text-muted ring-1 ring-border-subtle transition-colors hover:bg-surface hover:text-base-content`;
export const filterActionButtonActiveClass = 'bg-surface text-base-content shadow-sm'; export const filterActionButtonActiveClass = 'bg-surface text-base-content shadow-sm';
export const filterSelectClass = `${filterToolbarControlClass} border border-border bg-surface px-2 text-base-content outline-none focus:border-blue-500`; export const filterSelectClass =
'min-h-11 rounded-md border border-border bg-surface px-2 text-xs font-medium text-base-content outline-none focus:border-blue-500 sm:h-7 sm:min-h-0';
export const filterDividerClass = 'hidden h-5 w-px bg-surface-hover sm:block'; export const filterDividerClass = 'hidden h-5 w-px bg-surface-hover sm:block';
export const filterPanelClass = export const filterPanelClass =
'absolute right-0 top-[calc(100%+0.5rem)] z-[80] rounded-md border border-border bg-surface p-3 shadow-lg'; 'absolute right-0 top-[calc(100%+0.5rem)] z-[80] rounded-md border border-border bg-surface p-3 shadow-lg';
@@ -28,7 +30,7 @@ export const filterPanelDefaultWidthClass = 'w-[min(40rem,calc(100vw-2rem))]';
export const filterPanelTitleClass = 'text-sm font-medium text-base-content'; export const filterPanelTitleClass = 'text-sm font-medium text-base-content';
export const filterPanelDescriptionClass = 'text-xs text-muted'; export const filterPanelDescriptionClass = 'text-xs text-muted';
export const mobileFiltersButtonClass = export const mobileFiltersButtonClass =
'flex min-h-10 items-center gap-1.5 rounded-md bg-surface-hover px-2.5 py-1.5 text-xs font-medium text-muted'; 'flex min-h-11 items-center gap-1.5 rounded-md bg-surface-hover px-2.5 py-1.5 text-xs font-medium text-muted sm:min-h-10';
export const filterCountBadgeClass = export const filterCountBadgeClass =
'ml-0.5 rounded-full bg-blue-500 px-1.5 py-0.5 text-[10px] font-semibold leading-none text-white'; 'ml-0.5 rounded-full bg-blue-500 px-1.5 py-0.5 text-[10px] font-semibold leading-none text-white';
export const filterUtilityBadgeClass = export const filterUtilityBadgeClass =
@@ -19,7 +19,7 @@ export const SearchField: Component<SearchFieldProps> = (props) => {
onKeyDown={search.handleKeyDown} onKeyDown={search.handleKeyDown}
onBlur={search.handleBlur} onBlur={search.handleBlur}
aria-label={props.title ?? props.placeholder ?? 'Search'} aria-label={props.title ?? props.placeholder ?? 'Search'}
class={`min-h-10 w-full pl-8 sm:pl-9 ${search.inputPaddingRight()} py-1.5 sm:py-2 text-sm border border-border rounded-md class={`min-h-11 w-full pl-8 sm:min-h-10 sm:pl-9 ${search.inputPaddingRight()} py-1.5 sm:py-2 text-sm border border-border rounded-md
bg-surface text-base-content placeholder-muted bg-surface text-base-content placeholder-muted
focus:ring-2 focus:ring-blue-500 focus:border-blue-500 dark:focus:border-blue-400 outline-none transition-all disabled:opacity-60 disabled:cursor-not-allowed ${props.inputClass ?? ''}`} focus:ring-2 focus:ring-blue-500 focus:border-blue-500 dark:focus:border-blue-400 outline-none transition-all disabled:opacity-60 disabled:cursor-not-allowed ${props.inputClass ?? ''}`}
title={props.title} title={props.title}
@@ -46,7 +46,7 @@ export const SearchField: Component<SearchFieldProps> = (props) => {
<Show when={search.showClearButton()}> <Show when={search.showClearButton()}>
<button <button
type="button" type="button"
class="inline-flex h-10 w-10 items-center justify-center rounded-full bg-surface-hover text-muted transition-all duration-150 hover:bg-red-100 hover:text-red-600 active:scale-90 sm:h-6 sm:w-6 dark:hover:bg-red-900 dark:hover:text-red-400" class="inline-flex h-11 w-11 items-center justify-center rounded-full bg-surface-hover text-muted transition-all duration-150 hover:bg-red-100 hover:text-red-600 active:scale-90 sm:h-6 sm:w-6 dark:hover:bg-red-900 dark:hover:text-red-400"
onClick={() => props.onChange('')} onClick={() => props.onChange('')}
onMouseDown={props.onClearMouseDown} onMouseDown={props.onClearMouseDown}
aria-label="Clear search" aria-label="Clear search"
@@ -66,7 +66,8 @@ export const SearchTipsPopover: Component<SearchTipsPopoverProps> = (props) => {
id={popoverId()} id={popoverId()}
role="dialog" role="dialog"
aria-label={title()} aria-label={title()}
class={`absolute ${positionClass()} z-50 mt-2 w-72 overflow-hidden rounded-md border bg-surface text-left shadow-sm`} style={state.popoverStyle()}
class={`fixed ${positionClass()} z-50 w-auto overflow-hidden rounded-md border bg-surface text-left shadow-sm sm:absolute sm:mt-2 sm:w-72`}
> >
<div class="flex items-center justify-between border-b border-border-subtle px-3 py-2"> <div class="flex items-center justify-between border-b border-border-subtle px-3 py-2">
<span class="text-sm font-semibold text-base-content">{title()}</span> <span class="text-sm font-semibold text-base-content">{title()}</span>
@@ -8275,7 +8275,7 @@ describe('shared primitive guardrails', () => {
expect(savedViewsMenuSource).toContain("from './useSavedViews';"); expect(savedViewsMenuSource).toContain("from './useSavedViews';");
expect(filterToolbarSource).toContain('export const FilterPopoverTrigger'); expect(filterToolbarSource).toContain('export const FilterPopoverTrigger');
expect(filterToolbarSource).toContain("filterToolbarControlClass = 'h-7"); expect(filterToolbarSource).toContain("'min-h-11 rounded-md text-xs font-medium sm:h-7 sm:min-h-0'");
expect(viewOptionsMenuSource).toContain('<FilterPopoverTrigger'); expect(viewOptionsMenuSource).toContain('<FilterPopoverTrigger');
expect(viewOptionsMenuSource).toContain('class="static ml-auto shrink-0 sm:relative sm:ml-0"'); expect(viewOptionsMenuSource).toContain('class="static ml-auto shrink-0 sm:relative sm:ml-0"');
expect(viewOptionsMenuSource).toContain('class="left-auto right-0'); expect(viewOptionsMenuSource).toContain('class="left-auto right-0');
@@ -8427,10 +8427,11 @@ describe('shared primitive guardrails', () => {
it('keeps shared navigation and row actions usable at phone widths', () => { it('keeps shared navigation and row actions usable at phone widths', () => {
expect(subtabsSource).toContain('overflow-x-auto'); expect(subtabsSource).toContain('overflow-x-auto');
expect(subtabsSource).toContain('whitespace-nowrap'); expect(subtabsSource).toContain('whitespace-nowrap');
expect(subtabsSource).toContain('min-h-10'); expect(subtabsSource).toContain('min-h-11');
expect(summaryRowActionButtonSource).toContain('h-10 w-10'); expect(subtabsSource).toContain('sm:min-h-10');
expect(searchFieldSource).toContain('min-h-10'); expect(summaryRowActionButtonSource).toContain('h-11 w-11');
expect(filterToolbarSource).toContain('min-h-10'); expect(searchFieldSource).toContain('min-h-11');
expect(filterToolbarSource).toContain('min-h-11');
expect(inlineDetailTableRowSource).toContain('max-w-[calc(100vw-3.5rem)]'); expect(inlineDetailTableRowSource).toContain('max-w-[calc(100vw-3.5rem)]');
}); });
}); });
@@ -26,7 +26,7 @@ export const subtabsShellClass = 'border-b border-border';
export const subtabsListClass = 'flex min-w-0 items-center gap-6 overflow-x-auto scrollbar-hide'; export const subtabsListClass = 'flex min-w-0 items-center gap-6 overflow-x-auto scrollbar-hide';
export const subtabsTrailingRowClass = 'flex flex-wrap items-center justify-between gap-3'; export const subtabsTrailingRowClass = 'flex flex-wrap items-center justify-between gap-3';
export const subtabButtonClass = export const subtabButtonClass =
'inline-flex min-h-10 shrink-0 items-center whitespace-nowrap border-b-2 px-1 py-2 text-sm font-medium transition-colors'; 'inline-flex min-h-11 shrink-0 items-center whitespace-nowrap border-b-2 px-1 py-2 text-sm font-medium transition-colors sm:min-h-10';
export const subtabButtonActiveClass = 'border-blue-600 text-base-content'; export const subtabButtonActiveClass = 'border-blue-600 text-base-content';
export const subtabButtonInactiveClass = 'border-transparent text-muted hover:text-base-content'; export const subtabButtonInactiveClass = 'border-transparent text-muted hover:text-base-content';
export const Subtabs: Component<SubtabsProps> = (props) => { export const Subtabs: Component<SubtabsProps> = (props) => {
@@ -26,7 +26,7 @@ type SummaryRowActionButtonProps =
}; };
const DISCLOSURE_BUTTON_CLASS = [ const DISCLOSURE_BUTTON_CLASS = [
'inline-flex h-10 w-10 shrink-0 items-center justify-center rounded-md text-muted transition-colors sm:h-6 sm:w-6', 'inline-flex h-11 w-11 shrink-0 items-center justify-center rounded-md text-muted transition-colors sm:h-6 sm:w-6',
'hover:bg-surface hover:text-base-content', 'hover:bg-surface hover:text-base-content',
SUMMARY_ROW_ACTION_BUTTON_FOCUS_CLASS, SUMMARY_ROW_ACTION_BUTTON_FOCUS_CLASS,
].join(' '); ].join(' ');
@@ -27,6 +27,7 @@ describe('Toggle', () => {
expect(toggleModelSource).toContain('toggleSizeConfig'); expect(toggleModelSource).toContain('toggleSizeConfig');
expect(toggleModelSource).toContain('resolveToggleSize'); expect(toggleModelSource).toContain('resolveToggleSize');
expect(toggleModelSource).toContain('getToggleTrackClass'); expect(toggleModelSource).toContain('getToggleTrackClass');
expect(toggleModelSource).toContain("after:-inset-y-2.5 after:inset-x-0 after:content-['']");
expect(toggleModelSource).toContain('getToggleKnobClass'); expect(toggleModelSource).toContain('getToggleKnobClass');
expect(toggleModelSource).toContain('ToggleChangeEvent'); expect(toggleModelSource).toContain('ToggleChangeEvent');
}); });
@@ -38,7 +38,8 @@ describe('SearchField', () => {
)); ));
expect(screen.getByText('Cmd+K')).toBeInTheDocument(); expect(screen.getByText('Cmd+K')).toBeInTheDocument();
expect(screen.getByPlaceholderText('Search field')).toHaveClass('min-h-10'); expect(screen.getByPlaceholderText('Search field')).toHaveClass('min-h-11');
expect(screen.getByPlaceholderText('Search field')).toHaveClass('sm:min-h-10');
}); });
it('reserves mobile input space for every visible trailing action', () => { it('reserves mobile input space for every visible trailing action', () => {
@@ -109,8 +110,8 @@ describe('SearchField', () => {
)); ));
expect(screen.getByRole('button', { name: 'Clear search' })).toBeInTheDocument(); expect(screen.getByRole('button', { name: 'Clear search' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Clear search' })).toHaveClass('h-10'); expect(screen.getByRole('button', { name: 'Clear search' })).toHaveClass('h-11');
expect(screen.getByRole('button', { name: 'Clear search' })).toHaveClass('w-10'); expect(screen.getByRole('button', { name: 'Clear search' })).toHaveClass('w-11');
expect(screen.getByRole('button', { name: 'Extra' })).toBeInTheDocument(); expect(screen.getByRole('button', { name: 'Extra' })).toBeInTheDocument();
}); });
@@ -81,7 +81,7 @@ describe('SearchInput', () => {
expect(searchInputEnhancementsModelSource).toContain('SEARCH_HISTORY_MENU_CLASS'); expect(searchInputEnhancementsModelSource).toContain('SEARCH_HISTORY_MENU_CLASS');
expect(searchInputEnhancementsModelSource).toContain('w-full max-w-lg'); expect(searchInputEnhancementsModelSource).toContain('w-full max-w-lg');
expect(searchInputEnhancementsModelSource).not.toContain('left-0 right-0 top-full'); expect(searchInputEnhancementsModelSource).not.toContain('left-0 right-0 top-full');
expect(searchInputEnhancementsModelSource).toContain('h-10 w-10'); expect(searchInputEnhancementsModelSource).toContain('h-11 w-11');
expect(searchInputEnhancementsModelSource).toContain('sm:h-7 sm:w-7'); expect(searchInputEnhancementsModelSource).toContain('sm:h-7 sm:w-7');
}); });
@@ -23,12 +23,18 @@ describe('SearchTipsPopover', () => {
expect(searchTipsPopoverStateSource).toContain('createEffect'); expect(searchTipsPopoverStateSource).toContain('createEffect');
expect(searchTipsPopoverStateSource).toContain('window.addEventListener'); expect(searchTipsPopoverStateSource).toContain('window.addEventListener');
expect(searchTipsPopoverStateSource).toContain('pointerInside'); expect(searchTipsPopoverStateSource).toContain('pointerInside');
expect(searchTipsPopoverStateSource).toContain('window.innerWidth >= 640');
expect(searchTipsPopoverStateSource).toContain('window.innerWidth - viewportMargin - width');
expect(searchTipsPopoverStateSource).toContain("window.addEventListener('scroll', updatePopoverPosition, true)");
expect(searchTipsPopoverSource).toContain('style={state.popoverStyle()}');
expect(searchTipsPopoverSource).toContain('fixed ${positionClass()}');
expect(searchTipsPopoverSource).toContain('sm:absolute sm:mt-2 sm:w-72');
expect(searchTipsPopoverModelSource).toContain('getSearchTipsPopoverTriggerClass'); expect(searchTipsPopoverModelSource).toContain('getSearchTipsPopoverTriggerClass');
expect(searchTipsPopoverModelSource).toContain('getSearchTipsPopoverPositionClass'); expect(searchTipsPopoverModelSource).toContain('getSearchTipsPopoverPositionClass');
expect(searchTipsPopoverModelSource).toContain('getSearchTipsPopoverTriggerVariant'); expect(searchTipsPopoverModelSource).toContain('getSearchTipsPopoverTriggerVariant');
expect(searchTipsPopoverModelSource).toContain('shouldSearchTipsPopoverOpenOnHover'); expect(searchTipsPopoverModelSource).toContain('shouldSearchTipsPopoverOpenOnHover');
expect(searchTipsPopoverModelSource).toContain('h-10 w-10'); expect(searchTipsPopoverModelSource).toContain('h-11 w-11');
expect(searchTipsPopoverModelSource).toContain('sm:h-5 sm:w-5'); expect(searchTipsPopoverModelSource).toContain('sm:h-5 sm:w-5');
}); });
@@ -13,15 +13,15 @@ const TRIGGER_BASE_CLASSES =
describe('searchTipsPopoverModel.branchcov0712c', () => { describe('searchTipsPopoverModel.branchcov0712c', () => {
describe('getSearchTipsPopoverPositionClass', () => { describe('getSearchTipsPopoverPositionClass', () => {
it("returns 'left-0' when align === 'left' (truthy ternary arm)", () => { it("returns 'left-0' when align === 'left' (truthy ternary arm)", () => {
expect(getSearchTipsPopoverPositionClass('left')).toBe('left-0'); expect(getSearchTipsPopoverPositionClass('left')).toBe('sm:left-0');
}); });
it("returns 'right-0' when align === 'right' (falsy ternary arm)", () => { it("returns 'right-0' when align === 'right' (falsy ternary arm)", () => {
expect(getSearchTipsPopoverPositionClass('right')).toBe('right-0'); expect(getSearchTipsPopoverPositionClass('right')).toBe('sm:right-0');
}); });
it("returns 'right-0' when align is omitted (undefined -> falsy ternary arm)", () => { it("returns 'right-0' when align is omitted (undefined -> falsy ternary arm)", () => {
expect(getSearchTipsPopoverPositionClass()).toBe('right-0'); expect(getSearchTipsPopoverPositionClass()).toBe('sm:right-0');
}); });
it('returns the else-arm value for an unexpected align value (defensive)', () => { it('returns the else-arm value for an unexpected align value (defensive)', () => {
@@ -29,7 +29,7 @@ describe('searchTipsPopoverModel.branchcov0712c', () => {
// wrong-typed value that slips past the union at runtime) must fall to // wrong-typed value that slips past the union at runtime) must fall to
// the 'right-0' arm. // the 'right-0' arm.
const bogus = 'center' as unknown as Parameters<typeof getSearchTipsPopoverPositionClass>[0]; const bogus = 'center' as unknown as Parameters<typeof getSearchTipsPopoverPositionClass>[0];
expect(getSearchTipsPopoverPositionClass(bogus)).toBe('right-0'); expect(getSearchTipsPopoverPositionClass(bogus)).toBe('sm:right-0');
}); });
}); });
@@ -48,7 +48,7 @@ describe('searchTipsPopoverModel.branchcov0712c', () => {
it("returns the icon variant classes when triggerVariant === 'icon' (both ifs false -> default return)", () => { it("returns the icon variant classes when triggerVariant === 'icon' (both ifs false -> default return)", () => {
expect(getSearchTipsPopoverTriggerClass('icon')).toBe( expect(getSearchTipsPopoverTriggerClass('icon')).toBe(
`flex h-10 w-10 items-center justify-center rounded-full transition-colors hover:text-muted sm:h-5 sm:w-5 ${TRIGGER_BASE_CLASSES}`, `flex h-11 w-11 items-center justify-center rounded-full transition-colors hover:text-muted sm:h-5 sm:w-5 ${TRIGGER_BASE_CLASSES}`,
); );
}); });
@@ -57,7 +57,7 @@ describe('searchTipsPopoverModel.branchcov0712c', () => {
// the trailing default (the icon) return. // the trailing default (the icon) return.
const bogus = 'ghost' as unknown as Parameters<typeof getSearchTipsPopoverTriggerClass>[0]; const bogus = 'ghost' as unknown as Parameters<typeof getSearchTipsPopoverTriggerClass>[0];
expect(getSearchTipsPopoverTriggerClass(bogus)).toBe( expect(getSearchTipsPopoverTriggerClass(bogus)).toBe(
`flex h-10 w-10 items-center justify-center rounded-full transition-colors hover:text-muted sm:h-5 sm:w-5 ${TRIGGER_BASE_CLASSES}`, `flex h-11 w-11 items-center justify-center rounded-full transition-colors hover:text-muted sm:h-5 sm:w-5 ${TRIGGER_BASE_CLASSES}`,
); );
}); });
@@ -51,15 +51,15 @@ export const BUTTON_VARIANT_CLASSES: Record<ButtonVariant, string> = {
}; };
export const BUTTON_SIZE_CLASSES: Record<ButtonSize, string> = { export const BUTTON_SIZE_CLASSES: Record<ButtonSize, string> = {
xs: 'px-2.5 py-1 text-xs', xs: 'min-h-11 px-2.5 py-1 text-xs sm:min-h-0',
sm: 'px-2.5 py-1.5 text-xs', sm: 'min-h-11 px-2.5 py-1.5 text-xs sm:min-h-0',
mdCompact: 'px-3 py-2 text-sm', mdCompact: 'min-h-11 px-3 py-2 text-sm sm:min-h-0',
settingsAction: 'min-h-11 px-3 py-2 text-sm 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', settingsActionXs: 'min-h-11 px-3 py-2 text-xs sm:min-h-9',
md: 'px-4 py-2 text-sm', md: 'min-h-11 px-4 py-2 text-sm sm:min-h-0',
lg: 'px-6 py-3 text-base', lg: 'px-6 py-3 text-base',
icon: 'p-2', icon: 'min-h-11 min-w-11 p-2 sm:min-h-0 sm:min-w-0',
iconMd: 'h-9 w-9 p-0', iconMd: 'h-9 w-9 min-h-11 min-w-11 p-0 sm:min-h-0 sm:min-w-0',
}; };
export type ButtonClassOptions = { export type ButtonClassOptions = {
@@ -8,7 +8,7 @@ export const SEARCH_HISTORY_ENTRY_BUTTON_CLASS =
export const SEARCH_HISTORY_CLEAR_LABEL = 'Clear history'; export const SEARCH_HISTORY_CLEAR_LABEL = 'Clear history';
export function getSearchHistoryToggleButtonClass(isOpen: boolean): string { export function getSearchHistoryToggleButtonClass(isOpen: boolean): string {
return `flex h-10 w-10 items-center justify-center rounded-md transition-colors sm:h-7 sm:w-7 ${ return `flex h-11 w-11 items-center justify-center rounded-md transition-colors sm:h-7 sm:w-7 ${
isOpen isOpen
? 'bg-blue-100 dark:bg-blue-900 text-blue-600 dark:text-blue-400' ? 'bg-blue-100 dark:bg-blue-900 text-blue-600 dark:text-blue-400'
: 'text-muted hover:bg-surface-hover hover:text-base-content' : 'text-muted hover:bg-surface-hover hover:text-base-content'
@@ -18,7 +18,7 @@ export interface SearchTipsPopoverProps {
} }
export function getSearchTipsPopoverPositionClass(align?: 'left' | 'right'): string { export function getSearchTipsPopoverPositionClass(align?: 'left' | 'right'): string {
return align === 'left' ? 'left-0' : 'right-0'; return align === 'left' ? 'sm:left-0' : 'sm:right-0';
} }
export function getSearchTipsPopoverId(popoverId?: string): string { export function getSearchTipsPopoverId(popoverId?: string): string {
@@ -57,5 +57,5 @@ export function getSearchTipsPopoverTriggerClass(
return `rounded px-1 py-0.5 underline decoration-dotted underline-offset-4 transition-colors hover:text-base-content ${triggerBaseClasses}`; return `rounded px-1 py-0.5 underline decoration-dotted underline-offset-4 transition-colors hover:text-base-content ${triggerBaseClasses}`;
} }
return `flex h-10 w-10 items-center justify-center rounded-full transition-colors hover:text-muted sm:h-5 sm:w-5 ${triggerBaseClasses}`; return `flex h-11 w-11 items-center justify-center rounded-full transition-colors hover:text-muted sm:h-5 sm:w-5 ${triggerBaseClasses}`;
} }
@@ -49,7 +49,7 @@ export function getToggleTrackClass(
className?: string, className?: string,
): string { ): string {
const config = toggleSizeConfig[size]; const config = toggleSizeConfig[size];
return `relative inline-flex ${config.track} shrink-0 items-center rounded-full p-0.5 transition-all duration-300 ease-[cubic-bezier(0.34,1.56,0.64,1)] focus:outline-none focus:ring-0 ${ return `relative inline-flex ${config.track} shrink-0 items-center rounded-full p-0.5 transition-all duration-300 ease-[cubic-bezier(0.34,1.56,0.64,1)] after:absolute after:-inset-y-2.5 after:inset-x-0 after:content-[''] focus:outline-none focus:ring-0 ${
disabled ? TOGGLE_DISABLED_CLASS : checked ? TOGGLE_CHECKED_CLASS : TOGGLE_UNCHECKED_CLASS disabled ? TOGGLE_DISABLED_CLASS : checked ? TOGGLE_CHECKED_CLASS : TOGGLE_UNCHECKED_CLASS
} ${className ?? ''}`.trim(); } ${className ?? ''}`.trim();
} }
@@ -9,6 +9,7 @@ interface SearchTipsPopoverState {
handleMouseEnter: () => void; handleMouseEnter: () => void;
handleMouseLeave: () => void; handleMouseLeave: () => void;
isOpen: Accessor<boolean>; isOpen: Accessor<boolean>;
popoverStyle: Accessor<string | undefined>;
setPopoverRef: (el: HTMLDivElement) => void; setPopoverRef: (el: HTMLDivElement) => void;
setTriggerRef: (el: HTMLButtonElement) => void; setTriggerRef: (el: HTMLButtonElement) => void;
} }
@@ -22,11 +23,35 @@ export function useSearchTipsPopoverState(
options: SearchTipsPopoverStateOptions, options: SearchTipsPopoverStateOptions,
): SearchTipsPopoverState { ): SearchTipsPopoverState {
const [open, setOpen] = createSignal(false); const [open, setOpen] = createSignal(false);
const [popoverStyle, setPopoverStyle] = createSignal<string>();
let popoverRef: HTMLDivElement | undefined; let popoverRef: HTMLDivElement | undefined;
let triggerRef: HTMLButtonElement | undefined; let triggerRef: HTMLButtonElement | undefined;
let pointerInside = false; let pointerInside = false;
const close = () => setOpen(false); const close = () => setOpen(false);
const updatePopoverPosition = () => {
if (!triggerRef || !popoverRef || window.innerWidth >= 640) {
setPopoverStyle(undefined);
return;
}
const viewportMargin = 16;
const triggerGap = 8;
const width = Math.min(288, window.innerWidth - viewportMargin * 2);
const triggerRect = triggerRef.getBoundingClientRect();
const popoverHeight = popoverRef.getBoundingClientRect().height;
const left = Math.min(
Math.max(triggerRect.right - width, viewportMargin),
window.innerWidth - viewportMargin - width,
);
const belowTop = triggerRect.bottom + triggerGap;
const top =
belowTop + popoverHeight <= window.innerHeight - viewportMargin
? belowTop
: Math.max(viewportMargin, triggerRect.top - triggerGap - popoverHeight);
setPopoverStyle(`left:${left}px;top:${top}px;width:${width}px`);
};
createEffect(() => { createEffect(() => {
if (!open()) { if (!open()) {
@@ -51,10 +76,15 @@ export function useSearchTipsPopoverState(
window.addEventListener('pointerdown', handlePointerDown); window.addEventListener('pointerdown', handlePointerDown);
window.addEventListener('keydown', handleKeyDown); window.addEventListener('keydown', handleKeyDown);
window.addEventListener('resize', updatePopoverPosition);
window.addEventListener('scroll', updatePopoverPosition, true);
queueMicrotask(updatePopoverPosition);
onCleanup(() => { onCleanup(() => {
window.removeEventListener('pointerdown', handlePointerDown); window.removeEventListener('pointerdown', handlePointerDown);
window.removeEventListener('keydown', handleKeyDown); window.removeEventListener('keydown', handleKeyDown);
window.removeEventListener('resize', updatePopoverPosition);
window.removeEventListener('scroll', updatePopoverPosition, true);
}); });
}); });
@@ -82,8 +112,10 @@ export function useSearchTipsPopoverState(
setOpen(false); setOpen(false);
}, },
isOpen: open, isOpen: open,
popoverStyle,
setPopoverRef: (el) => { setPopoverRef: (el) => {
popoverRef = el; popoverRef = el;
queueMicrotask(updatePopoverPosition);
}, },
setTriggerRef: (el) => { setTriggerRef: (el) => {
triggerRef = el; triggerRef = el;
@@ -382,7 +382,7 @@ export function PatrolIntelligenceHeader(props: { state: PatrolIntelligenceState
<Show when={!state.shouldShowPatrolSetupOnly()}> <Show when={!state.shouldShowPatrolSetupOnly()}>
<div class="flex flex-wrap items-center gap-2 sm:ml-auto"> <div class="flex flex-wrap items-center gap-2 sm:ml-auto">
{renderRunControl( {renderRunControl(
'sm:hidden flex items-center gap-2 px-3 py-1.5 text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 disabled:bg-surface-alt disabled:text-muted rounded-md transition-colors', 'flex min-h-11 items-center gap-2 rounded-md bg-blue-600 px-3 py-1.5 text-sm font-medium text-white transition-colors hover:bg-blue-700 disabled:bg-surface-alt disabled:text-muted sm:hidden',
)} )}
<Show when={!runBlockedByProviderSetup()}> <Show when={!runBlockedByProviderSetup()}>
@@ -390,7 +390,7 @@ export function PatrolIntelligenceHeader(props: { state: PatrolIntelligenceState
href={settingsTabPath('system-ai-patrol')} href={settingsTabPath('system-ai-patrol')}
aria-label="Open Patrol settings" aria-label="Open Patrol settings"
title="Open Patrol settings" title="Open Patrol settings"
class="flex items-center gap-2 rounded-md border border-border px-3 py-1.5 text-sm font-medium text-base-content shadow-sm transition-colors hover:bg-surface-alt" class="flex min-h-11 items-center gap-2 rounded-md border border-border px-3 py-1.5 text-sm font-medium text-base-content shadow-sm transition-colors hover:bg-surface-alt sm:min-h-0"
> >
<SettingsIcon class="w-4 h-4" /> <SettingsIcon class="w-4 h-4" />
<span class="sr-only sm:not-sr-only">Settings</span> <span class="sr-only sm:not-sr-only">Settings</span>
@@ -91,6 +91,14 @@ describe('PatrolIntelligenceHeader', () => {
expect(headerSource).not.toContain('Refresh Patrol'); expect(headerSource).not.toContain('Refresh Patrol');
}); });
it('keeps primary Patrol actions touch-sized on phones without inflating desktop chrome', () => {
expect(headerSource).toContain('flex min-h-11 items-center gap-2 rounded-md bg-blue-600');
expect(headerSource).toContain(
'flex min-h-11 items-center gap-2 rounded-md border border-border',
);
expect(headerSource).toContain('sm:min-h-0');
});
it('makes Patrol mode a simple four-level choice without rendering plan-locked paid modes', () => { it('makes Patrol mode a simple four-level choice without rendering plan-locked paid modes', () => {
expect(PATROL_AUTONOMY_POLICY_PRESENTATION).toEqual({ expect(PATROL_AUTONOMY_POLICY_PRESENTATION).toEqual({
monitor: { monitor: {