feat(security): reflow the node Security page for mobile (#1372)

* feat(security): reflow the node Security page for mobile

Below the md breakpoint the Security page now reads as a phone surface
instead of a squeezed desktop, with no change to the desktop layout.

- Masthead stat cluster moves into a full-width 3-cell strip
  (critical / high / last scan) below the tab strip, since the masthead
  hides its inline cluster on a phone.
- The eight-section tab strip becomes a horizontally scrollable mono row
  with an edge mask-fade and a cyan underline on the active tab; every
  section stays reachable by scroll.
- The six totals render as a 3x2 hairline-divided grid instead of the
  640px-wide rail that forced a horizontal scroll.
- The Images tab becomes a filterable, scrollable list (severity dot,
  truncated ref, freshness, critical/high count tags) with a chip row,
  in place of the desktop table.
- A freshness footer band states scan recency and scanner version.

All mobile treatment is gated by useIsMobile() or max-md: utilities, so
the desktop view is byte-identical. Charts are reused full-width.

* feat(security): make the mobile Security page a bespoke masthead-led screen

On a phone the Security page now drops the global top bar and leads with
its masthead (the notifications + more-menu cluster moves into the
masthead's right slot), matching Home and Fleet so the mobile shell is
continuous across pages. The view is reclassified bespoke and rendered
through the masthead-led path; the desktop layout is unchanged.

The mobile "more" menu now lists every destination instead of omitting
the bottom-tab views, so the same menu opens the same set on every
screen rather than changing contents from page to page.
This commit is contained in:
Anso
2026-06-14 21:28:13 -04:00
committed by GitHub
parent a5109e7916
commit 0066887cee
12 changed files with 712 additions and 96 deletions
@@ -0,0 +1,49 @@
import { describe, it, expect, vi } from 'vitest';
import { render, screen, fireEvent } from '@testing-library/react';
import { Home, Radar, Boxes, ShieldCheck } from 'lucide-react';
import { MobileMoreMenu } from './MobileMoreMenu';
import type { NavItem } from './EditorLayout/hooks/useViewNavigationState';
// Includes a bottom-tab primary (dashboard) and secondary destinations; the
// menu must list every one so navigation is identical on every screen.
const navItems: NavItem[] = [
{ value: 'dashboard', label: 'Home', icon: Home },
{ value: 'fleet', label: 'Fleet', icon: Radar },
{ value: 'resources', label: 'Resources', icon: Boxes },
{ value: 'security', label: 'Security', icon: ShieldCheck },
];
function open(over: Partial<React.ComponentProps<typeof MobileMoreMenu>> = {}) {
const props: React.ComponentProps<typeof MobileMoreMenu> = {
navItems,
activeView: 'security',
onNavigate: vi.fn(),
...over,
};
render(<MobileMoreMenu {...props} />);
// The destination list lives inside a Sheet that is closed until the trigger
// is clicked, so open it before querying the nav buttons.
fireEvent.click(screen.getByRole('button', { name: 'More destinations' }));
return props;
}
describe('MobileMoreMenu', () => {
it('lists every nav item, including the bottom-tab primaries', () => {
open();
for (const item of navItems) {
expect(screen.getByRole('button', { name: item.label })).toBeInTheDocument();
}
});
it('navigates to the chosen destination on click', () => {
const { onNavigate } = open();
fireEvent.click(screen.getByRole('button', { name: 'Resources' }));
expect(onNavigate).toHaveBeenCalledWith('resources');
});
it('marks the active destination', () => {
open({ activeView: 'security' });
expect(screen.getByRole('button', { name: 'Security' }).className).toContain('bg-glass-highlight');
expect(screen.getByRole('button', { name: 'Resources' }).className).not.toContain('font-medium');
});
});