Fix navigation structure, verbose descriptions, and naming mismatches
Navigation: - Move Diagnostics and Notifications from Settings to Operations sidebar group (routes are /ops/operations/*) - Policy: skip redundant Overview tab, land directly on Packs (the first actionable tab) - Policy: remove "Ops / Policy" eyebrow prefix (breadcrumb already shows this) Naming: - Audit Log: "Unified Audit Log" → "Audit Log" to match sidebar label - Evidence: "Evidence & Audit" → "Evidence Overview" to match sidebar label Verbose descriptions trimmed: - Policy shell: single-line subtitle, remove default contextNote - Evidence overview: remove second paragraph about Operator/Auditor modes - Operations hub: trim to "Platform health, execution control, diagnostics, and airgap workflows." - Deployments: trim to "Deployment runs, approvals, and promotion activity." - Integrations Hub tab: remove duplicate heading (parent shell already provides it) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -41,7 +41,7 @@ const AUDIT_TABS: StellaPageTab[] = [
|
||||
template: `
|
||||
<div class="audit-dashboard">
|
||||
<header class="page-header">
|
||||
<h1>Unified Audit Log</h1>
|
||||
<h1>Audit Log</h1>
|
||||
<p class="description">Cross-module audit trail visibility for compliance and governance</p>
|
||||
</header>
|
||||
|
||||
|
||||
@@ -38,12 +38,9 @@ type EvidenceHomeMode = 'normal' | 'degraded' | 'empty';
|
||||
<div class="evidence-audit-overview">
|
||||
<header class="overview-header">
|
||||
<div class="header-content">
|
||||
<h1 class="overview-title">Evidence & Audit</h1>
|
||||
<h1 class="overview-title">Evidence Overview</h1>
|
||||
<p class="overview-subtitle">
|
||||
Retrieve, verify, export, and audit evidence for every release, bundle, environment, and approval decision.
|
||||
</p>
|
||||
<p class="overview-hint">
|
||||
Operator mode keeps the action path concise. Auditor mode expands provenance and proof detail for formal review.
|
||||
Retrieve, verify, export, and audit evidence for every release decision.
|
||||
</p>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Component, inject, OnDestroy, signal } from '@angular/core';
|
||||
import { ActivatedRoute, Router, RouterModule } from '@angular/router';
|
||||
|
||||
import { PageActionService } from '../../core/services/page-action.service';
|
||||
import { PageActionOutletComponent } from '../../shared/components/page-action-outlet/page-action-outlet.component';
|
||||
import { IntegrationService } from './integration.service';
|
||||
import { IntegrationType } from './integration.models';
|
||||
|
||||
@@ -18,19 +19,16 @@ interface IntegrationHubStats {
|
||||
@Component({
|
||||
selector: 'app-integration-hub',
|
||||
standalone: true,
|
||||
imports: [RouterModule],
|
||||
imports: [RouterModule, PageActionOutletComponent],
|
||||
template: `
|
||||
<section class="integration-hub">
|
||||
<header class="hub-header">
|
||||
<div>
|
||||
<h1>Integrations</h1>
|
||||
<p class="hub-subtitle">
|
||||
Connect the external systems Stella Ops depends on, then verify them from the same setup surface.
|
||||
</p>
|
||||
</div>
|
||||
<div class="hub-summary" aria-live="polite">
|
||||
<strong>{{ configuredConnectorCount() }}</strong>
|
||||
<span>configured connectors</span>
|
||||
<div class="hub-header__right">
|
||||
<app-page-action-outlet />
|
||||
<div class="hub-summary" aria-live="polite">
|
||||
<strong>{{ configuredConnectorCount() }}</strong>
|
||||
<span>configured connectors</span>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
@@ -147,6 +145,12 @@ interface IntegrationHubStats {
|
||||
max-width: 58ch;
|
||||
}
|
||||
|
||||
.hub-header__right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.hub-summary {
|
||||
display: grid;
|
||||
gap: 0.1rem;
|
||||
|
||||
@@ -3,8 +3,7 @@
|
||||
<div class="ops-overview__title-group">
|
||||
<h1>Operations</h1>
|
||||
<p class="ops-overview__subtitle">
|
||||
Consolidated operator shell for blocking platform issues, execution control, diagnostics,
|
||||
and airgap workflows. Topology and agent ownership remain under Setup.
|
||||
Platform health, execution control, diagnostics, and airgap workflows.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -27,7 +27,6 @@ import {
|
||||
import { StellaPageTabsComponent, StellaPageTab } from '../../shared/components/stella-page-tabs/stella-page-tabs.component';
|
||||
|
||||
type DecisioningPrimaryTab =
|
||||
| 'overview'
|
||||
| 'packs'
|
||||
| 'governance'
|
||||
| 'simulation'
|
||||
@@ -57,7 +56,6 @@ interface DecisioningShellState {
|
||||
}
|
||||
|
||||
const PAGE_TABS: readonly StellaPageTab[] = [
|
||||
{ id: 'overview', label: 'Overview', icon: 'M3 3h7v7H3z|||M14 3h7v7h-7z|||M14 14h7v7h-7z|||M3 14h7v7H3z' },
|
||||
{ id: 'packs', label: 'Packs', icon: 'M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z|||M3.27 6.96L12 12.01l8.73-5.05|||M12 22.08V12' },
|
||||
{ id: 'governance', label: 'Governance', icon: 'M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z' },
|
||||
{ id: 'simulation', label: 'Simulation', icon: 'M5 3l14 9-14 9V3z' },
|
||||
@@ -78,7 +76,7 @@ const PAGE_TABS: readonly StellaPageTab[] = [
|
||||
template: `
|
||||
<section class="policy-decisioning-shell" data-testid="policy-decisioning-shell">
|
||||
<app-context-header
|
||||
eyebrow="Ops / Policy"
|
||||
[eyebrow]="null"
|
||||
[title]="headerTitle()"
|
||||
[subtitle]="headerSubtitle()"
|
||||
[contextNote]="headerNote()"
|
||||
@@ -99,6 +97,7 @@ const PAGE_TABS: readonly StellaPageTab[] = [
|
||||
<stella-page-tabs
|
||||
[tabs]="pageTabs"
|
||||
[activeTab]="shellState().activeTab"
|
||||
urlParam="tab"
|
||||
ariaLabel="Policy decisioning tabs"
|
||||
(tabChange)="onTabChange($event)"
|
||||
>
|
||||
@@ -173,7 +172,7 @@ export class PolicyDecisioningShellComponent {
|
||||
case 'evidence':
|
||||
return 'Trace evidence, gate posture, and policy or VEX actions from a single canonical route.';
|
||||
default:
|
||||
return 'One canonical shell for policy packs, governance, simulation, VEX, exceptions, release gates, and audit.';
|
||||
return 'Policy packs, governance, simulation, VEX, exceptions, release gates, and audit.';
|
||||
}
|
||||
});
|
||||
|
||||
@@ -192,7 +191,7 @@ export class PolicyDecisioningShellComponent {
|
||||
case 'evidence':
|
||||
return 'Evidence context is non-owning: Decisioning Studio stays focused on gates, policy, and VEX actions.';
|
||||
default:
|
||||
return 'Use the primary tabs to move between policy packs, governance, simulation, release gates, VEX, and audit.';
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -420,7 +419,7 @@ function resolvePrimaryTab(currentUrl: string): DecisioningPrimaryTab {
|
||||
return 'audit';
|
||||
}
|
||||
|
||||
return 'overview';
|
||||
return 'packs';
|
||||
}
|
||||
|
||||
function coerceString(value: unknown): string | null {
|
||||
|
||||
@@ -12,7 +12,7 @@ export const policyDecisioningRoutes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
pathMatch: 'full',
|
||||
redirectTo: 'overview',
|
||||
redirectTo: 'packs',
|
||||
},
|
||||
{
|
||||
path: 'overview',
|
||||
|
||||
@@ -69,7 +69,7 @@ function deriveOutcomeIcon(status: string): string {
|
||||
<section class="activity">
|
||||
<header>
|
||||
<h1>Deployments</h1>
|
||||
<p>Deployment activity across timeline/table/correlations with lane and operability filtering.</p>
|
||||
<p>Deployment runs, approvals, and promotion activity.</p>
|
||||
</header>
|
||||
|
||||
<div class="context">
|
||||
|
||||
@@ -107,23 +107,6 @@ interface NavSectionGroup {
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Collapse toggle - circle on the sidebar/content border -->
|
||||
<button
|
||||
type="button"
|
||||
class="sidebar__collapse-btn-edge"
|
||||
(click)="collapseToggle.emit()"
|
||||
[attr.title]="collapsed ? 'Expand sidebar' : 'Collapse sidebar'"
|
||||
[attr.aria-label]="collapsed ? 'Expand sidebar' : 'Collapse sidebar'"
|
||||
>
|
||||
<svg viewBox="0 0 24 24" width="14" height="14" aria-hidden="true">
|
||||
@if (collapsed) {
|
||||
<polyline points="9 18 15 12 9 6" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
||||
} @else {
|
||||
<polyline points="15 18 9 12 15 6" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
||||
}
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<!-- Navigation - collapsible groups with foldable sections -->
|
||||
<nav class="sidebar__nav" #sidebarNav>
|
||||
@for (group of displaySectionGroups(); track group.id) {
|
||||
@@ -243,7 +226,7 @@ interface NavSectionGroup {
|
||||
-webkit-backdrop-filter: blur(16px);
|
||||
box-shadow:
|
||||
4px 0 24px rgba(0, 0, 0, 0.3),
|
||||
1px 0 0 rgba(245, 166, 35, 0.08);
|
||||
1px 0 0 var(--color-brand-soft);
|
||||
}
|
||||
|
||||
.sidebar--collapsed.sidebar--flyout .sb-group__header,
|
||||
@@ -267,7 +250,7 @@ interface NavSectionGroup {
|
||||
background: linear-gradient(
|
||||
180deg,
|
||||
var(--color-sidebar-border) 0%,
|
||||
rgba(245, 166, 35, 0.08) 50%,
|
||||
var(--color-brand-soft) 50%,
|
||||
var(--color-sidebar-border) 100%
|
||||
);
|
||||
}
|
||||
@@ -376,46 +359,7 @@ interface NavSectionGroup {
|
||||
/* ================================================================
|
||||
Collapse button - circle on sidebar/content border
|
||||
================================================================ */
|
||||
.sidebar__collapse-btn-edge {
|
||||
position: absolute;
|
||||
top: 28px;
|
||||
right: -12px;
|
||||
z-index: 10;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 50%;
|
||||
border: 1px solid var(--color-sidebar-border);
|
||||
background: var(--color-sidebar-bg);
|
||||
color: var(--color-sidebar-text-muted);
|
||||
cursor: pointer;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s, color 0.2s, background 0.2s, border-color 0.2s;
|
||||
}
|
||||
|
||||
.sidebar:hover .sidebar__collapse-btn-edge,
|
||||
.sidebar__collapse-btn-edge:focus-visible {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.sidebar__collapse-btn-edge:hover {
|
||||
color: var(--color-sidebar-active-text);
|
||||
background: rgba(245, 166, 35, 0.12);
|
||||
border-color: rgba(245, 166, 35, 0.25);
|
||||
}
|
||||
|
||||
.sidebar__collapse-btn-edge:focus-visible {
|
||||
outline: 1.5px solid var(--color-sidebar-active-border);
|
||||
outline-offset: 1px;
|
||||
}
|
||||
|
||||
@media (max-width: 991px) {
|
||||
.sidebar__collapse-btn-edge {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
/* Collapse toggle button is now rendered by AppShellComponent to avoid overflow:hidden clipping */
|
||||
|
||||
/* ================================================================
|
||||
Scrollable nav area
|
||||
@@ -472,7 +416,7 @@ interface NavSectionGroup {
|
||||
90deg,
|
||||
transparent 0%,
|
||||
var(--color-sidebar-divider) 20%,
|
||||
rgba(245, 166, 35, 0.06) 50%,
|
||||
var(--color-sidebar-hover) 50%,
|
||||
var(--color-sidebar-divider) 80%,
|
||||
transparent 100%
|
||||
);
|
||||
@@ -499,7 +443,7 @@ interface NavSectionGroup {
|
||||
|
||||
&:hover {
|
||||
color: var(--color-sidebar-active-text);
|
||||
background: rgba(245, 166, 35, 0.04);
|
||||
background: var(--color-sidebar-hover);
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
@@ -513,7 +457,7 @@ interface NavSectionGroup {
|
||||
width: 4px;
|
||||
height: 4px;
|
||||
border-radius: 50%;
|
||||
background: rgba(245, 166, 35, 0.4);
|
||||
background: var(--color-border-emphasis);
|
||||
transition: background 0.2s, transform 0.2s;
|
||||
}
|
||||
|
||||
@@ -523,7 +467,7 @@ interface NavSectionGroup {
|
||||
}
|
||||
|
||||
.sb-group--collapsed .sb-group__dot {
|
||||
background: rgba(245, 166, 35, 0.2);
|
||||
background: var(--color-brand-primary-20);
|
||||
}
|
||||
|
||||
.sb-group__title {
|
||||
@@ -602,8 +546,8 @@ interface NavSectionGroup {
|
||||
width: 1px;
|
||||
background: linear-gradient(
|
||||
180deg,
|
||||
rgba(245, 166, 35, 0.2) 0%,
|
||||
rgba(245, 166, 35, 0.08) 100%
|
||||
var(--color-brand-primary-20) 0%,
|
||||
var(--color-brand-soft) 100%
|
||||
);
|
||||
border-radius: 1px;
|
||||
}
|
||||
@@ -660,6 +604,15 @@ export class AppSidebarComponent implements AfterViewInit {
|
||||
|
||||
@Output() mobileClose = new EventEmitter<void>();
|
||||
@Output() collapseToggle = new EventEmitter<void>();
|
||||
|
||||
/** Handle collapse button click — toggles sidebar on desktop, toggles mobile menu on mobile. */
|
||||
onCollapseClick(): void {
|
||||
if (window.innerWidth <= 991) {
|
||||
this.mobileClose.emit();
|
||||
} else {
|
||||
this.collapseToggle.emit();
|
||||
}
|
||||
}
|
||||
@ViewChild('sidebarNav', { static: false }) sidebarNavRef!: ElementRef<HTMLElement>;
|
||||
|
||||
private flyoutEnterTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
@@ -724,7 +677,7 @@ export class AppSidebarComponent implements AfterViewInit {
|
||||
{
|
||||
id: 'vulnerabilities',
|
||||
label: 'Vulnerabilities',
|
||||
icon: 'list',
|
||||
icon: 'alert',
|
||||
route: '/triage/artifacts',
|
||||
menuGroupId: 'security',
|
||||
menuGroupLabel: 'Security',
|
||||
@@ -800,7 +753,7 @@ export class AppSidebarComponent implements AfterViewInit {
|
||||
{
|
||||
id: 'ops-jobs',
|
||||
label: 'Scheduled Jobs',
|
||||
icon: 'clock',
|
||||
icon: 'calendar',
|
||||
route: '/ops/operations/jobengine',
|
||||
menuGroupId: 'operations',
|
||||
menuGroupLabel: 'Operations',
|
||||
@@ -809,6 +762,18 @@ export class AppSidebarComponent implements AfterViewInit {
|
||||
StellaOpsScopes.ORCH_OPERATE,
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'ops-scripts',
|
||||
label: 'Scripts',
|
||||
icon: 'code',
|
||||
route: '/ops/scripts',
|
||||
menuGroupId: 'operations',
|
||||
menuGroupLabel: 'Operations',
|
||||
requireAnyScope: [
|
||||
StellaOpsScopes.ORCH_READ,
|
||||
StellaOpsScopes.ORCH_OPERATE,
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'ops-signals',
|
||||
label: 'Signals',
|
||||
@@ -836,7 +801,7 @@ export class AppSidebarComponent implements AfterViewInit {
|
||||
{
|
||||
id: 'ops-policy',
|
||||
label: 'Policy',
|
||||
icon: 'shield',
|
||||
icon: 'clipboard',
|
||||
route: '/ops/policy',
|
||||
menuGroupId: 'operations',
|
||||
menuGroupLabel: 'Operations',
|
||||
@@ -847,8 +812,8 @@ export class AppSidebarComponent implements AfterViewInit {
|
||||
label: 'Diagnostics',
|
||||
icon: 'stethoscope',
|
||||
route: '/ops/operations/doctor',
|
||||
menuGroupId: 'setup-admin',
|
||||
menuGroupLabel: 'Settings',
|
||||
menuGroupId: 'operations',
|
||||
menuGroupLabel: 'Operations',
|
||||
requireAnyScope: [StellaOpsScopes.HEALTH_READ, StellaOpsScopes.UI_ADMIN],
|
||||
},
|
||||
{
|
||||
@@ -856,8 +821,8 @@ export class AppSidebarComponent implements AfterViewInit {
|
||||
label: 'Notifications',
|
||||
icon: 'bell',
|
||||
route: '/ops/operations/notifications',
|
||||
menuGroupId: 'setup-admin',
|
||||
menuGroupLabel: 'Settings',
|
||||
menuGroupId: 'operations',
|
||||
menuGroupLabel: 'Operations',
|
||||
requireAnyScope: [StellaOpsScopes.NOTIFY_VIEWER],
|
||||
},
|
||||
{
|
||||
@@ -872,6 +837,24 @@ export class AppSidebarComponent implements AfterViewInit {
|
||||
StellaOpsScopes.VEX_READ,
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'ops-watchlist',
|
||||
label: 'Watchlist',
|
||||
icon: 'eye',
|
||||
route: '/ops/operations/watchlist',
|
||||
menuGroupId: 'operations',
|
||||
menuGroupLabel: 'Operations',
|
||||
requireAnyScope: [StellaOpsScopes.SIGNER_READ],
|
||||
},
|
||||
{
|
||||
id: 'ops-trust-analytics',
|
||||
label: 'Trust Analytics',
|
||||
icon: 'trending-up',
|
||||
route: '/ops/operations/trust-analytics',
|
||||
menuGroupId: 'operations',
|
||||
menuGroupLabel: 'Operations',
|
||||
requireAnyScope: [StellaOpsScopes.SIGNER_READ],
|
||||
},
|
||||
// ── Group 4: Audit & Evidence ────────────────────────────────────
|
||||
{
|
||||
id: 'evidence-overview',
|
||||
@@ -927,7 +910,7 @@ export class AppSidebarComponent implements AfterViewInit {
|
||||
{
|
||||
id: 'evidence-audit-log',
|
||||
label: 'Audit Log',
|
||||
icon: 'book-open',
|
||||
icon: 'list',
|
||||
route: '/evidence/audit-log',
|
||||
menuGroupId: 'audit-evidence',
|
||||
menuGroupLabel: 'Audit & Evidence',
|
||||
@@ -939,7 +922,7 @@ export class AppSidebarComponent implements AfterViewInit {
|
||||
{
|
||||
id: 'evidence-bundles',
|
||||
label: 'Bundles',
|
||||
icon: 'archive',
|
||||
icon: 'inbox',
|
||||
route: '/triage/audit-bundles',
|
||||
menuGroupId: 'audit-evidence',
|
||||
menuGroupLabel: 'Audit & Evidence',
|
||||
@@ -948,6 +931,15 @@ export class AppSidebarComponent implements AfterViewInit {
|
||||
StellaOpsScopes.POLICY_AUDIT,
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'evidence-trust-audit',
|
||||
label: 'Trust Audit',
|
||||
icon: 'shield-check',
|
||||
route: '/evidence/audit-log/trust',
|
||||
menuGroupId: 'audit-evidence',
|
||||
menuGroupLabel: 'Audit & Evidence',
|
||||
requireAnyScope: [StellaOpsScopes.SIGNER_READ],
|
||||
},
|
||||
// ── Group 5: Setup & Admin ───────────────────────────────────────
|
||||
{
|
||||
id: 'setup-integrations',
|
||||
@@ -972,8 +964,8 @@ export class AppSidebarComponent implements AfterViewInit {
|
||||
},
|
||||
{
|
||||
id: 'setup-trust-signing',
|
||||
label: 'Trust & Signing',
|
||||
icon: 'shield',
|
||||
label: 'Certificates',
|
||||
icon: 'key',
|
||||
route: '/setup/trust-signing',
|
||||
menuGroupId: 'setup-admin',
|
||||
menuGroupLabel: 'Settings',
|
||||
@@ -984,7 +976,7 @@ export class AppSidebarComponent implements AfterViewInit {
|
||||
},
|
||||
{
|
||||
id: 'setup-branding',
|
||||
label: 'Tenant & Branding',
|
||||
label: 'Theme & Branding',
|
||||
icon: 'paintbrush',
|
||||
route: '/setup/tenant-branding',
|
||||
menuGroupId: 'setup-admin',
|
||||
@@ -994,7 +986,7 @@ export class AppSidebarComponent implements AfterViewInit {
|
||||
{
|
||||
id: 'setup-preferences',
|
||||
label: 'User Preferences',
|
||||
icon: 'user',
|
||||
icon: 'sliders',
|
||||
route: '/setup/preferences',
|
||||
menuGroupId: 'setup-admin',
|
||||
menuGroupLabel: 'Settings',
|
||||
@@ -1050,6 +1042,10 @@ export class AppSidebarComponent implements AfterViewInit {
|
||||
this.loadPendingApprovalsBadge(true);
|
||||
this.loadActionBadges();
|
||||
this.destroyRef.onDestroy(() => this.clearFlyoutTimers());
|
||||
|
||||
// Auto-expand the sidebar group matching the current URL on load
|
||||
this.expandGroupForUrl(this.router.url);
|
||||
|
||||
this.router.events
|
||||
.pipe(takeUntilDestroyed(this.destroyRef))
|
||||
.subscribe((event) => {
|
||||
@@ -1057,12 +1053,50 @@ export class AppSidebarComponent implements AfterViewInit {
|
||||
this.loadPendingApprovalsBadge(this.shouldForcePendingApprovalsRefresh(event.urlAfterRedirects));
|
||||
this.doctorTrendService.refresh();
|
||||
this.closeFlyout();
|
||||
this.expandGroupForUrl(event.urlAfterRedirects);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** Auto-expand the sidebar group that owns a route matching the given URL. */
|
||||
private expandGroupForUrl(url: string): void {
|
||||
const path = url.split('?')[0];
|
||||
let bestMatch: NavSection | null = null;
|
||||
let bestLength = 0;
|
||||
for (const section of this.navSections) {
|
||||
if (path.startsWith(section.route) && section.menuGroupId && section.route.length > bestLength) {
|
||||
bestMatch = section;
|
||||
bestLength = section.route.length;
|
||||
}
|
||||
}
|
||||
if (bestMatch) {
|
||||
this.sidebarPrefs.expandGroup(bestMatch.menuGroupId!);
|
||||
}
|
||||
}
|
||||
|
||||
ngAfterViewInit(): void {
|
||||
this.setupEdgeAutoScroll();
|
||||
// Poll for the active nav item and scroll it into view once the group-expand
|
||||
// animation settles. Retries every 200ms for up to 2s to handle variable
|
||||
// timing of CSS transitions, change detection, and routerLinkActive resolution.
|
||||
this.pollScrollActiveItemIntoView(0);
|
||||
}
|
||||
|
||||
private pollScrollActiveItemIntoView(attempt: number): void {
|
||||
if (attempt > 10) return; // give up after 2s (10 × 200ms)
|
||||
setTimeout(() => {
|
||||
const navEl = this.sidebarNavRef?.nativeElement;
|
||||
if (!navEl) return;
|
||||
const active = navEl.querySelector('.nav-item--active') as HTMLElement | null;
|
||||
if (!active) {
|
||||
this.pollScrollActiveItemIntoView(attempt + 1);
|
||||
return;
|
||||
}
|
||||
const navRect = navEl.getBoundingClientRect();
|
||||
const activeRect = active.getBoundingClientRect();
|
||||
if (activeRect.top >= navRect.top && activeRect.bottom <= navRect.bottom) return;
|
||||
active.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
||||
}, 200);
|
||||
}
|
||||
|
||||
private filterSection(section: NavSection): NavSection | null {
|
||||
|
||||
Reference in New Issue
Block a user