From bb75580073b6b161bcfe773307a54ac17217fa88 Mon Sep 17 00:00:00 2001 From: rcourtman Date: Thu, 6 Aug 2026 22:57:04 +0100 Subject: [PATCH] fix(ui): keep responsive filter panels reachable --- frontend-modern/browser-verification.json | 94 +++++++++++++------ .../shared/FilterBar/FilterBar.test.tsx | 1 + .../shared/FilterBar/SavedViewsMenu.test.tsx | 7 +- .../shared/FilterBar/SavedViewsMenu.tsx | 8 +- .../shared/FilterBar/ViewOptionsMenu.test.tsx | 3 +- .../shared/FilterBar/ViewOptionsMenu.tsx | 8 +- .../src/components/shared/FilterToolbar.tsx | 1 + .../components/shared/SearchTipsPopover.tsx | 2 +- .../SharedPrimitives.guardrails.test.ts | 8 +- .../__tests__/SearchTipsPopover.test.tsx | 79 +++++++++++++++- ...rchTipsPopoverModel.branchcov0712c.test.ts | 8 +- .../shared/searchTipsPopoverModel.ts | 2 +- .../shared/useSearchTipsPopoverState.ts | 19 +++- frontend-modern/src/index.css | 32 +++++++ 14 files changed, 211 insertions(+), 61 deletions(-) diff --git a/frontend-modern/browser-verification.json b/frontend-modern/browser-verification.json index bc47001b1..5e4f6c9a2 100644 --- a/frontend-modern/browser-verification.json +++ b/frontend-modern/browser-verification.json @@ -1,49 +1,81 @@ { "version": 1, - "base_sha": "ed0f3613aeda63f41248cd1ef981b5ca2639bd8b", - "verified_at": "2026-08-06T21:47:15Z", + "base_sha": "b7651974260b3bccb8085754b002bd3727040832", + "verified_at": "2026-08-06T20:28:35Z", "result": "passed", "changed_paths": [ - "frontend-modern/src/features/alerts/alertOverridesModel.ts", - "frontend-modern/src/features/alerts/thresholds/hooks/useThresholdsAvailabilityMutations.ts", - "frontend-modern/src/features/alerts/thresholds/hooks/useThresholdsDockerData.ts", - "frontend-modern/src/features/alerts/thresholds/thresholdsResourceModel.ts", - "frontend-modern/src/features/docker/DockerContainersTable.tsx", - "frontend-modern/src/utils/alertThresholdsPresentation.ts" + "frontend-modern/src/components/shared/FilterToolbar.tsx", + "frontend-modern/src/components/shared/FilterBar/SavedViewsMenu.tsx", + "frontend-modern/src/components/shared/FilterBar/ViewOptionsMenu.tsx", + "frontend-modern/src/components/shared/SearchTipsPopover.tsx", + "frontend-modern/src/components/shared/searchTipsPopoverModel.ts", + "frontend-modern/src/components/shared/useSearchTipsPopoverState.ts", + "frontend-modern/src/index.css" ], "content_sha256": { - "frontend-modern/src/features/alerts/alertOverridesModel.ts": "9729ff9ad413c3b9ece2a1ab3449939cba563bf80acd16e678b52d668751ed63", - "frontend-modern/src/features/alerts/thresholds/hooks/useThresholdsAvailabilityMutations.ts": "b8b89dd3d756b13dc730f6cc7e0c3fe2f4a79525753eeaa6fa203766c7e592ba", - "frontend-modern/src/features/alerts/thresholds/hooks/useThresholdsDockerData.ts": "92b0fd9836d86930cae26e5ccecb3dc56babdba2170a454e820a12dd27549f2b", - "frontend-modern/src/features/alerts/thresholds/thresholdsResourceModel.ts": "e71f230e5fb8ad5f970cf9385afa31b18221aedd81453febbce64a0d0f60783c", - "frontend-modern/src/features/docker/DockerContainersTable.tsx": "45b125474f0d5058e9861c44fab0a33cf93b4a82952001d7f9010f18419dac4e", - "frontend-modern/src/utils/alertThresholdsPresentation.ts": "6605976a9b561c26578f1eb78785ccd5d6739c2a0a3b91246e23828c8433a49c" + "frontend-modern/src/components/shared/FilterToolbar.tsx": "e866a9276b5ab6a901ffea4adab59f9f1438376295017e4290a1e58f47e775f0", + "frontend-modern/src/components/shared/FilterBar/SavedViewsMenu.tsx": "0266c7abf8ab90941d53b977f0fd1af7cd1f795d0c5849fdabe425a01d6cb02b", + "frontend-modern/src/components/shared/FilterBar/ViewOptionsMenu.tsx": "877570b2fbdbd7378a0cd94bb3c65a9edb0a22348dd8ab9a106b0f15b92f0aa8", + "frontend-modern/src/components/shared/SearchTipsPopover.tsx": "d570941c7f089dad7805c514b51c6f7a21647c91d6409f3b85f71f91e8b73d6a", + "frontend-modern/src/components/shared/searchTipsPopoverModel.ts": "b558ce94eea59a645fd2878bc6bd2fd1f0184cc86cfd649ac6991f303f888131", + "frontend-modern/src/components/shared/useSearchTipsPopoverState.ts": "be3c9840f580d1e975096a98f94da6ca6a0e554b9d93b1d0e2aac77403eb5b60", + "frontend-modern/src/index.css": "ac6b159890d180e044153555e6e9299d02d7775991f9aab3f15a6a31f113c509" }, - "routes": [ - "/alerts/thresholds/docker", - "/docker" - ], + "routes": ["/proxmox/overview", "/standalone/machines"], "viewports": [ { - "width": 1280, - "height": 800 + "width": 390, + "height": 844 }, { - "width": 375, - "height": 812 + "width": 568, + "height": 320 + }, + { + "width": 844, + "height": 390 + }, + { + "width": 1180, + "height": 650 + }, + { + "width": 1279, + "height": 650 + }, + { + "width": 1280, + "height": 500 + }, + { + "width": 1280, + "height": 650 + }, + { + "width": 1280, + "height": 768 } ], "states": [ - "Docker thresholds tab with mock container rows grouped by host, ignored-container-patterns card showing the new wildcard copy", - "loki container rows rendering the persisted docker:orion-2-mock/loki name-keyed override as disabled after a full page reload", - "alerts.json converged to name-keyed overrides after live monitor-sync migration re-homed a seeded legacy container-ID key and a unified-hash key and pruned an orphaned 12-hex key", - "Docker page containers table with CPU/memory meters resolving thresholds through the new name-first candidate chain", - "mobile 375px docker thresholds view rendering the wildcard copy and container sections" + "Short-height landscape platform tables with fixed bottom navigation", + "Expanded Proxmox node details and mobile filter actions", + "View preferences with the inline Proxmox column picker expanded", + "View preferences with all eleven Machine column choices expanded", + "Saved Views empty state and save-name form", + "Search Tips with content taller than the available landscape space", + "Exact bottom-navigation to desktop-navigation transition at 1279px and 1280px", + "Short desktop windows below the safe anchored-popover height" ], "interactions": [ - "searched containers for 'loki' in the thresholds table", - "toggled the per-container alert disable switch on 'loki' and clicked Save Changes; verified alerts.json stored docker:orion-2-mock/loki (host+name key)", - "reloaded /alerts/thresholds/docker and confirmed the saved override re-bound to the container rows via the name candidate", - "seeded legacy docker:{host}/{containerID}, docker:{host}/{unified-hash} and orphan hex overrides via PUT /api/alerts/config and watched the monitor sync migrate all of them to the name key on disk" + "Verified the horizontally scrollable mobile platform rail keeps Machines reachable and centres it after activation", + "Expanded a Proxmox node at 568x320 and confirmed the table shell and detail row create no horizontal document overflow", + "Opened View preferences at 568x320, 844x390, 1180x650, 1279x650, 1280x500, and 1280x768", + "Expanded the Machine column picker, scrolled internally to Kernel, toggled it, and reset the saved preference", + "Removed and restored the Proxmox Disk column through the compact View panel", + "Opened Saved Views and its save-name form at compact mobile and short desktop sizes", + "Confirmed View and Saved Views remain fully above the bottom navigation and entirely inside the viewport", + "Confirmed short desktop windows use a viewport-fixed panel while 1280x768 returns to the toolbar anchor", + "Opened Search Tips at 568x320, 844x390, 1280x500, and 1280x768 and verified collision-aware height, internal scrolling, and desktop anchoring", + "Measured zero document overflow throughout the interaction pass" ] } diff --git a/frontend-modern/src/components/shared/FilterBar/FilterBar.test.tsx b/frontend-modern/src/components/shared/FilterBar/FilterBar.test.tsx index c5c4c9ccd..62c91f2f2 100644 --- a/frontend-modern/src/components/shared/FilterBar/FilterBar.test.tsx +++ b/frontend-modern/src/components/shared/FilterBar/FilterBar.test.tsx @@ -310,6 +310,7 @@ describe('FilterBar', () => { expect(label).toHaveClass('sr-only'); expect(select).toHaveClass('min-h-11'); expect(select).toHaveClass('sm:h-7'); + expect(select).toHaveClass('sm:min-h-0'); expect(select).toHaveClass('w-[7.5rem]'); expect(select).not.toHaveClass('min-w-[7.5rem]'); expect(select.parentElement).not.toHaveClass('p-0.5'); diff --git a/frontend-modern/src/components/shared/FilterBar/SavedViewsMenu.test.tsx b/frontend-modern/src/components/shared/FilterBar/SavedViewsMenu.test.tsx index 0c2891917..4387c37e4 100644 --- a/frontend-modern/src/components/shared/FilterBar/SavedViewsMenu.test.tsx +++ b/frontend-modern/src/components/shared/FilterBar/SavedViewsMenu.test.tsx @@ -51,16 +51,13 @@ describe('SavedViewsMenu', () => { expect(trigger).toHaveFocus(); }); - it('anchors from the leading edge on narrow rails and the trailing edge on desktop', () => { + it('stays above mobile navigation and returns to the trailing anchor on desktop', () => { renderMenu('responsive-anchor-contract'); fireEvent.click(screen.getByRole('button', { name: 'Saved views' })); expect(screen.getByRole('dialog', { name: 'Saved views' })).toHaveClass( - 'left-0', - 'right-auto', - 'md:left-auto', - 'md:right-0', + 'filter-bottom-nav-aware-panel', ); }); diff --git a/frontend-modern/src/components/shared/FilterBar/SavedViewsMenu.tsx b/frontend-modern/src/components/shared/FilterBar/SavedViewsMenu.tsx index 761361958..bc41c4231 100644 --- a/frontend-modern/src/components/shared/FilterBar/SavedViewsMenu.tsx +++ b/frontend-modern/src/components/shared/FilterBar/SavedViewsMenu.tsx @@ -11,7 +11,11 @@ import BookmarkIcon from 'lucide-solid/icons/bookmark'; import PlusIcon from 'lucide-solid/icons/plus'; import StarIcon from 'lucide-solid/icons/star'; import XIcon from 'lucide-solid/icons/x'; -import { FilterPopoverTrigger, FilterToolbarPanel } from '@/components/shared/FilterToolbar'; +import { + FilterPopoverTrigger, + FilterToolbarPanel, + filterBottomNavAwarePanelClass, +} from '@/components/shared/FilterToolbar'; import { useSavedViews, type SavedView } from './useSavedViews'; interface SavedViewsMenuProps { @@ -124,7 +128,7 @@ export const SavedViewsMenu: Component = (props) => { role="dialog" aria-label="Saved views" widthClass="w-64 max-w-[calc(100vw-2rem)]" - class="left-0 right-auto top-[calc(100%+0.25rem)] z-50 p-0 md:left-auto md:right-0" + class={`${filterBottomNavAwarePanelClass} z-50 p-0`} > { expect(screen.getByRole('dialog', { name: 'View preferences' })).toBeInTheDocument(); expect(screen.getByRole('dialog', { name: 'View preferences' })).toHaveClass( - 'left-auto', - 'right-0', + 'filter-bottom-nav-aware-panel', ); expect(screen.getByRole('button', { name: 'View' }).parentElement).toHaveClass( 'static', diff --git a/frontend-modern/src/components/shared/FilterBar/ViewOptionsMenu.tsx b/frontend-modern/src/components/shared/FilterBar/ViewOptionsMenu.tsx index 1646d8912..cb310de0c 100644 --- a/frontend-modern/src/components/shared/FilterBar/ViewOptionsMenu.tsx +++ b/frontend-modern/src/components/shared/FilterBar/ViewOptionsMenu.tsx @@ -9,7 +9,11 @@ import { } from 'solid-js'; import SlidersHorizontalIcon from 'lucide-solid/icons/sliders-horizontal'; -import { FilterPopoverTrigger, FilterToolbarPanel } from '@/components/shared/FilterToolbar'; +import { + FilterPopoverTrigger, + FilterToolbarPanel, + filterBottomNavAwarePanelClass, +} from '@/components/shared/FilterToolbar'; interface ViewOptionsMenuProps { children: JSX.Element; @@ -69,7 +73,7 @@ export const ViewOptionsMenu: Component = (props) => { role="dialog" aria-label={props.label ?? 'View preferences'} widthClass="w-80 max-w-[calc(100vw-2rem)]" - class="left-auto right-0 top-[calc(100%+0.25rem)] z-50 max-h-[min(38rem,calc(100vh-8rem))] overflow-y-auto p-3" + class={`${filterBottomNavAwarePanelClass} z-50 p-3`} >
diff --git a/frontend-modern/src/components/shared/FilterToolbar.tsx b/frontend-modern/src/components/shared/FilterToolbar.tsx index fb1820bb1..50ace728d 100644 --- a/frontend-modern/src/components/shared/FilterToolbar.tsx +++ b/frontend-modern/src/components/shared/FilterToolbar.tsx @@ -27,6 +27,7 @@ export const filterDividerClass = 'hidden h-5 w-px bg-surface-hover sm:block'; export const filterPanelClass = 'absolute right-0 top-[calc(100%+0.5rem)] z-[80] rounded-md border border-border bg-surface p-3 shadow-lg'; export const filterPanelDefaultWidthClass = 'w-[min(40rem,calc(100vw-2rem))]'; +export const filterBottomNavAwarePanelClass = 'filter-bottom-nav-aware-panel'; export const filterPanelTitleClass = 'text-sm font-medium text-base-content'; export const filterPanelDescriptionClass = 'text-xs text-muted'; export const mobileFiltersButtonClass = diff --git a/frontend-modern/src/components/shared/SearchTipsPopover.tsx b/frontend-modern/src/components/shared/SearchTipsPopover.tsx index 80d459422..f4c1c5482 100644 --- a/frontend-modern/src/components/shared/SearchTipsPopover.tsx +++ b/frontend-modern/src/components/shared/SearchTipsPopover.tsx @@ -67,7 +67,7 @@ export const SearchTipsPopover: Component = (props) => { role="dialog" aria-label={title()} 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`} + class={`!fixed ${positionClass()} z-50 w-auto overflow-y-auto rounded-md border bg-surface text-left shadow-sm xl:!absolute xl:mt-2 xl:w-72`} >
{title()} diff --git a/frontend-modern/src/components/shared/SharedPrimitives.guardrails.test.ts b/frontend-modern/src/components/shared/SharedPrimitives.guardrails.test.ts index 5263cf87d..74dfb04be 100644 --- a/frontend-modern/src/components/shared/SharedPrimitives.guardrails.test.ts +++ b/frontend-modern/src/components/shared/SharedPrimitives.guardrails.test.ts @@ -8275,13 +8275,15 @@ describe('shared primitive guardrails', () => { expect(savedViewsMenuSource).toContain("from './useSavedViews';"); expect(filterToolbarSource).toContain('export const FilterPopoverTrigger'); + expect(filterToolbarSource).toContain('export const filterBottomNavAwarePanelClass'); expect(filterToolbarSource).toContain( "'min-h-11 rounded-md text-xs font-medium sm:h-7 sm:min-h-0'", ); expect(viewOptionsMenuSource).toContain(' { afterEach(() => { cleanup(); + vi.restoreAllMocks(); }); it('keeps search tips popover on shell, runtime, and model owners', () => { @@ -23,14 +24,16 @@ describe('SearchTipsPopover', () => { expect(searchTipsPopoverStateSource).toContain('createEffect'); expect(searchTipsPopoverStateSource).toContain('window.addEventListener'); expect(searchTipsPopoverStateSource).toContain('pointerInside'); - expect(searchTipsPopoverStateSource).toContain('window.innerWidth >= 640'); + expect(searchTipsPopoverStateSource).toContain('window.innerWidth >= 1280'); + expect(searchTipsPopoverStateSource).toContain('window.innerHeight >= 768'); expect(searchTipsPopoverStateSource).toContain('window.innerWidth - viewportMargin - width'); + expect(searchTipsPopoverStateSource).toContain('nav[aria-label="Mobile navigation"]'); 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(searchTipsPopoverSource).toContain('!fixed ${positionClass()}'); + expect(searchTipsPopoverSource).toContain('xl:!absolute xl:mt-2 xl:w-72'); expect(searchTipsPopoverModelSource).toContain('getSearchTipsPopoverTriggerClass'); expect(searchTipsPopoverModelSource).toContain('getSearchTipsPopoverPositionClass'); @@ -80,4 +83,70 @@ describe('SearchTipsPopover', () => { fireEvent.keyDown(window, { key: 'Escape' }); expect(screen.queryByRole('dialog', { name: 'Search tips' })).toBeNull(); }); + + it('bounds landscape tips above the visible mobile navigation', async () => { + vi.spyOn(window, 'innerWidth', 'get').mockReturnValue(844); + vi.spyOn(window, 'innerHeight', 'get').mockReturnValue(390); + + render(() => ( + <> +