feat(ui): add Classic, Smart, and Compact desktop navigation styles (#1642)

* feat(ui): add Classic, Smart, and Compact desktop navigation styles

Introduce a shared app-nav registry and reachable model so TopBar, the
command palette, and mobile menus share one destination source. Smart bar
is the default; Appearance gains a Navigation subsection with quick links.

* fix(e2e): stop clearing top-nav prefs on every reload

The desktop-navigation suite used addInitScript to wipe mode storage,
which re-ran on reload and undid the classic/compact values under test.

* fix(ui): harden nav PR docs and Compact quick-link coverage

Drop the partial docs-refresh import that left missing image assets and a stale Display screenshot, keep navigation-scoped operator docs against main, and add an E2E path that proves Compact quick-link add, persist, and render after reload.

* fix(ui): polish Compact quick links and menu mastheads

Align Smart/Compact menus with Theme chrome, keep pin labels always visible, and drive add capacity from persisted pins (max five) with a trailing + picker and per-pin remove.

* test(e2e): exact-match Compact Networking pin locator

Avoid Playwright strict-mode clash with Actions for Networking.

* fix(ui): simplify Compact quick link removal and fix + button trailing

Remove the (...) dropdown per quick link in Compact mode. Right-click
context menu remains as the sole on-bar removal affordance. Fix the +
add-button to trail quick links rather than pinning to the far right
by removing flex-1 from the quick-link rail.
This commit is contained in:
Anso
2026-07-16 21:48:03 -04:00
committed by GitHub
parent d8e4ede94f
commit 25586fc8ab
24 changed files with 1871 additions and 216 deletions
@@ -18,6 +18,7 @@ import type { ActiveView } from './hooks/useViewNavigationState';
import type { StackUpdateInfo } from '@/types/imageUpdates';
import type { SecurityTab, FleetTab } from '@/lib/events';
import { isStackEditorDeepLink } from '@/lib/router/readUrlRouteState';
import type { NavDestination } from '@/lib/navigation/appNavRegistry';
// Paid-tier views are loaded on demand. Their internal PaidGate /
// CapabilityGate wrappers render
@@ -109,6 +110,7 @@ export interface ViewRouterProps {
stackUpdates: Record<string, StackUpdateInfo>;
urlHydratingStack: string | null;
isFileLoading: boolean;
quickLinkCandidates?: NavDestination[];
}
export function ViewRouter({
@@ -142,6 +144,7 @@ export function ViewRouter({
stackUpdates,
urlHydratingStack,
isFileLoading,
quickLinkCandidates,
}: ViewRouterProps): ReactNode {
const { can } = useAuth();
const { experimental, experimentalReady } = useExperimental();
@@ -153,6 +156,7 @@ export function ViewRouter({
muteRulePrefill={muteRulePrefill}
onMutePrefillConsumed={onMutePrefillConsumed}
onOpenMuteRulesWithPrefill={onOpenMuteRulesWithPrefill}
quickLinkCandidates={quickLinkCandidates}
/>
);
}
@@ -1,9 +1,4 @@
import { useState, useEffect, useMemo, useCallback } from 'react';
import {
Terminal, CloudDownload, Home, HardDrive, ScrollText,
Activity, Radar, RefreshCw, Clock, ShieldCheck, Network,
} from 'lucide-react';
import type { LucideIcon } from 'lucide-react';
import { useAuth } from '@/context/AuthContext';
import { useLicense } from '@/context/LicenseContext';
import { useNodes } from '@/context/NodeContext';
@@ -18,20 +13,18 @@ import { HUB_ONLY_VIEWS } from '@/lib/router/routeTypes';
import { readUrlRouteState } from '@/lib/router/readUrlRouteState';
import {
authzReady,
isViewHidden,
normalizeHiddenView,
type ReachabilityContext,
} from '@/lib/routing/reachability';
import { useExperimental } from '@/hooks/useExperimental';
import { buildNavigationModel } from '@/lib/navigation/buildNavigationModel';
import type { NavDestination } from '@/lib/navigation/appNavRegistry';
export type { ActiveView };
export { HUB_ONLY_VIEWS };
export interface NavItem {
value: ActiveView;
label: string;
icon: LucideIcon;
}
/** @deprecated Prefer NavDestination from appNavRegistry; alias kept for mobile/palette imports. */
export type NavItem = NavDestination;
interface UseViewNavigationStateOptions {
onNavigateToDashboard?: () => void;
@@ -124,39 +117,8 @@ export function useViewNavigationState(options?: UseViewNavigationStateOptions)
return () => window.removeEventListener(SENCHO_NAVIGATE_EVENT, handler);
}, []);
const navItems = useMemo((): NavItem[] => {
const items: NavItem[] = [
{ value: 'dashboard', label: 'Home', icon: Home },
];
if (!isViewHidden('fleet', reachCtx)) {
items.push({ value: 'fleet', label: 'Fleet', icon: Radar });
}
items.push(
{ value: 'resources', label: 'Resources', icon: HardDrive },
{ value: 'networking', label: 'Networking', icon: Network },
{ value: 'security', label: 'Security', icon: ShieldCheck },
{ value: 'templates', label: 'App Store', icon: CloudDownload },
);
if (!isViewHidden('global-observability', reachCtx)) {
items.push({ value: 'global-observability', label: 'Logs', icon: Activity });
}
if (!isViewHidden('auto-updates', reachCtx)) {
items.push({ value: 'auto-updates', label: 'Update', icon: RefreshCw });
}
if (!isViewHidden('scheduled-ops', reachCtx)) {
items.push({ value: 'scheduled-ops', label: 'Schedules', icon: Clock });
}
// Visual discovery fail-closed: omit Console until /meta settles and the
// flag is on. URL normalization still waits on experimentalReady inside
// isViewHidden so enabled deep links are not rewritten during cold load.
if (experimentalReady && experimental && !isViewHidden('host-console', reachCtx)) {
items.push({ value: 'host-console', label: 'Console', icon: Terminal });
}
if (!isViewHidden('audit-log', reachCtx)) {
items.push({ value: 'audit-log', label: 'Audit', icon: ScrollText });
}
return items;
}, [reachCtx, experimentalReady, experimental]);
const navModel = useMemo(() => buildNavigationModel(reachCtx), [reachCtx]);
const navItems = navModel.allPageItems;
useEffect(() => {
if (!authzReady(reachCtx)) return;
@@ -183,6 +145,7 @@ export function useViewNavigationState(options?: UseViewNavigationStateOptions)
openMuteRulesWithPrefill,
handleNavigate,
navItems,
navModel,
reachCtx,
} as const;
}