Skip to main content

TDD: Overview Tab Nested Navigation

Issue: #258 (Enhancement)
Status: Draft
Created: 2025-11-01
Related PRD: docs/04-prd/overview-nested-tabs.md

Architecture Overview

Current Architecture

┌─────────────────────────────────────────────────────────────┐
│ MainViewComponent │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ mat-tab-group │ │
│ │ ┌──────────┬──────────┬──────────┬──────────┬────────┐│ │
│ │ │ Overview │ Preview │Compliance│ Details │Raw MD ││ │
│ │ └──────────┴──────────┴──────────┴──────────┴────────┘│ │
│ │ ┌────────────────────────────────────────────────────┐│ │
│ │ │ <ng-container [ngComponentOutlet]="tab.component"> ││ │
│ │ │ → OverviewComponent / PreviewComponent / etc. ││ │
│ │ └────────────────────────────────────────────────────┘│ │
│ └────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘

TabService: 5 root tabs
- tabs[0]: Overview (OverviewComponent)
- tabs[1]: Preview (PreviewComponent)
- tabs[2]: Compliance (ComplianceComponent)
- tabs[3]: Details (DetailsComponent) ← to be nested
- tabs[4]: Raw Markdown (RawMarkdownComponent) ← to be nested

Proposed Architecture

┌─────────────────────────────────────────────────────────────┐
│ MainViewComponent │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ mat-tab-group (root) │ │
│ │ ┌──────────┬──────────┬────────────┐ │ │
│ │ │ Overview │ Preview │ Compliance │ + dynamic tabs │ │
│ │ └──────────┴──────────┴────────────┘ │ │
│ │ ┌────────────────────────────────────────────────────┐│ │
│ │ │ <ng-container [ngComponentOutlet]="tab.component"> ││ │
│ │ │ → OverviewComponent (enhanced) ││ │
│ │ │ ┌──────────────────────────────────────────┐ ││ │
│ │ │ │ mat-tab-group (nested in Overview) │ ││ │
│ │ │ │ ┌──────────┬──────────┬──────────┐ │ ││ │
│ │ │ │ │Explanation│Transcript│Compliance│ │ ││ │
│ │ │ │ └──────────┴──────────┴──────────┘ │ ││ │
│ │ │ │ ┌──────────────────────────────────────┐ │ ││ │
│ │ │ │ │ DetailsComponent │ │ ││ │
│ │ │ │ │ RawMarkdownComponent │ │ ││ │
│ │ │ │ │ Compliance Summary HTML │ │ ││ │
│ │ │ │ └──────────────────────────────────────┘ │ ││ │
│ │ │ └──────────────────────────────────────────┘ ││ │
│ │ └────────────────────────────────────────────────────┘│ │
│ └────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘

TabService: 3 root tabs only
- tabs[0]: Overview (OverviewComponent with nested tabs)
- tabs[1]: Preview (PreviewComponent)
- tabs[2]: Compliance (ComplianceComponent)

Implementation Details

1. URL Fragment Handling

No routing configuration changes needed! The existing route /files/:fileId/pages/:pageNumber/:tab handles everything.

Fragment-Based Navigation Benefits

  1. No nested routes: Existing routes work as-is
  2. Natural defaults: /overview without fragment shows default tab
  3. Simpler code: ~80% less routing code
  4. Semantic correctness: Fragments are designed for in-page navigation

Legacy URL Redirect Logic

Location: MainViewComponent.ngOnInit()

// Redirect legacy URLs to fragment-based structure
private handleLegacyUrls(): void {
const firstChild = this.route.snapshot.firstChild;
if (!firstChild) return;

const tab = firstChild.params['tab'];
const planId = this.route.snapshot.params['planId'];
const fileId = firstChild.params['fileId'] || '1';
const pageNumber = firstChild.params['pageNumber'];

// Redirect legacy tabs to overview with fragments
if (tab === 'details') {
this.router.navigate(
['/projects', planId, 'files', fileId, 'pages', pageNumber, 'overview'],
{ fragment: 'explanation', replaceUrl: true }
);
} else if (tab === 'raw-markdown') {
this.router.navigate(
['/projects', planId, 'files', fileId, 'pages', pageNumber, 'overview'],
{ fragment: 'transcript', replaceUrl: true }
);
}
// Note: '/overview' without fragment naturally defaults to 'explanation' tab
}

2. TabService Updates

File: web-ng-m3/src/app/shared/tab.service.ts

Remove Nested Tabs from Root Level

@Injectable({
providedIn: 'root'
})
export class TabService {
private tabs: Tab[] = [
{ label: 'Overview', component: OverviewComponent, tabId: 'overview' },
{ label: 'Preview', component: PreviewComponent, tabId: 'preview' },
{ label: 'Compliance', component: ComplianceComponent, tabId: 'compliance' },
// REMOVED: Details and Raw Markdown (now nested in Overview)
];

// ... rest of service unchanged
}

Impact:

  • Tabs array reduced from 5 to 3 elements
  • Tab indices shift: Preview (1→1), Compliance (2→2), Details (3→removed), Raw Markdown (4→removed)
  • Close button condition in MainViewComponent changes from i >= 3 to i >= 3 (still correct for dynamic tabs)

3. OverviewComponent Enhancement

File: web-ng-m3/src/app/components/project/pages/overview/overview.component.ts

Updated Component Class (Simplified with Fragments!)

import { Component, OnInit, OnDestroy } from '@angular/core';
import { CommonModule } from '@angular/common';
import { Router, ActivatedRoute } from '@angular/router';
import { MatCardModule } from '@angular/material/card';
import { MatProgressBarModule } from '@angular/material/progress-bar';
import { MatTabsModule } from '@angular/material/tabs';
import { ArchitecturalPlanPage } from '../../../../shared/models/architectural-plan-page.model';
import { PlanReviewerService } from '../../../../shared/plan-reviewer.service';
import { TabService } from '../../../../shared/tab.service';
import { DetailsComponent } from '../details/details.component';
import { RawMarkdownComponent } from '../raw-markdown/raw-markdown.component';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { marked } from 'marked';

/**
* Overview Component with Nested Tabs
*
* Displays page metadata and nested tabs for:
* 1. Explanation - AI-generated page explanation (DetailsComponent)
* 2. Transcript - Raw page.md content (RawMarkdownComponent)
* 3. Compliance - Compliance summary (inline HTML)
*
* Uses URL fragments for nested tab selection (#explanation, #transcript, #compliance)
*/
@Component({
selector: 'app-overview',
templateUrl: './overview.component.html',
styleUrls: ['./overview.component.css'],
standalone: true,
imports: [
CommonModule,
MatCardModule,
MatProgressBarModule,
MatTabsModule,
DetailsComponent,
RawMarkdownComponent
]
})
export class OverviewComponent implements OnInit, OnDestroy {
activePlanPage: ArchitecturalPlanPage | null = null;
loading = false;

// Compliance summary (now in nested tab)
complianceSummaryHtml = '';
isLoadingComplianceSummary = false;
hasComplianceSummaryError = false;

// Nested tab management
activeNestedTabIndex = 0;
nestedTabs = [
{ label: 'Explanation', tabId: 'explanation' },
{ label: 'Transcript', tabId: 'transcript' },
{ label: 'Compliance', tabId: 'compliance' }
];

private destroy$ = new Subject<void>();

constructor(
private planReviewerService: PlanReviewerService,
private tabService: TabService,
private router: Router,
private route: ActivatedRoute
) {}

ngOnInit() {
// Subscribe to active page changes
this.tabService.activePlanPage$
.pipe(takeUntil(this.destroy$))
.subscribe(page => {
this.activePlanPage = page;
if (page) {
this.fetchComplianceSummary();
}
});

// Subscribe to URL fragment changes for tab selection
this.route.fragment
.pipe(takeUntil(this.destroy$))
.subscribe(fragment => {
this.setActiveNestedTabFromFragment(fragment);
});
}

ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}

/**
* Sets the active nested tab based on URL fragment
* Defaults to index 0 (Explanation) if no fragment or invalid fragment
*/
private setActiveNestedTabFromFragment(fragment: string | null): void {
const tabIndex = this.nestedTabs.findIndex(t => t.tabId === fragment);
this.activeNestedTabIndex = tabIndex !== -1 ? tabIndex : 0;
}

/**
* Handles nested tab change events
* Updates URL fragment without full navigation
*/
onNestedTabChange(event: any): void {
const tabId = this.nestedTabs[event.index].tabId;

// Update URL fragment (creates history entry)
this.router.navigate([], {
fragment: tabId,
replaceUrl: false // Keep in history for back/forward navigation
});
}

/**
* Fetches compliance summary for the active page
*/
private async fetchComplianceSummary() {
if (!this.activePlanPage) return;

this.isLoadingComplianceSummary = true;
this.hasComplianceSummaryError = false;
this.complianceSummaryHtml = '';

try {
const complianceReport = await this.planReviewerService.getPageComplianceReport(this.activePlanPage);

if (complianceReport && complianceReport.getReportSummary) {
const summaryMd = complianceReport.getReportSummary();
if (summaryMd && summaryMd.trim().length > 0) {
this.complianceSummaryHtml = await marked.parse(summaryMd);
} else {
this.complianceSummaryHtml = '<p style="color: #888;">No compliance summary available for this page.</p>';
}
} else {
this.complianceSummaryHtml = '<p style="color: #888;">No compliance summary available for this page.</p>';
}
} catch (error) {
console.error('Error fetching compliance summary:', error);
this.hasComplianceSummaryError = true;
this.complianceSummaryHtml = '<p style="color: #d32f2f;">Error loading compliance summary.</p>';
} finally {
this.isLoadingComplianceSummary = false;
}
}
}

Key Simplifications:

  • ✅ No route parameter extraction logic
  • ✅ No complex route hierarchy traversal
  • ✅ Just subscribe to route.fragment Observable
  • ✅ Update fragment with router.navigate([], { fragment })
  • ✅ 50+ lines of code removed!

Updated Template

File: web-ng-m3/src/app/components/project/pages/overview/overview.component.html

<div class="overview-container">
<mat-progress-bar
*ngIf="loading"
mode="indeterminate"
class="loading-progress">
</mat-progress-bar>

<div class="overview-content" *ngIf="!loading">
<!-- Page Metadata (unchanged) -->
<div class="horizontal-details" *ngIf="activePlanPage">
<div class="detail-item detail-title">
<div class="label">Page Title</div>
<div class="value">{{ activePlanPage.getTitle() }}</div>
</div>
<div class="detail-item detail-page-number">
<div class="label">Page #</div>
<div class="value">{{ activePlanPage.getPageNumber() }}</div>
</div>
<div class="detail-item detail-sheet-id">
<div class="label">Sheet ID</div>
<div class="value">{{ activePlanPage.getPageId() }}</div>
</div>
</div>

<!-- Page Summary (unchanged) -->
<div class="detail-item" *ngIf="activePlanPage">
<div class="label">Summary</div>
<div class="value">{{ activePlanPage.getSummary() }}</div>
</div>

<!-- NEW: Nested Tab Group -->
<div class="nested-tabs-section" *ngIf="activePlanPage">
<mat-tab-group
[selectedIndex]="activeNestedTabIndex"
(selectedTabChange)="onNestedTabChange($event)"
class="nested-tab-group">

<!-- Explanation Tab -->
<mat-tab label="Explanation">
<ng-template matTabContent>
<app-details></app-details>
</ng-template>
</mat-tab>

<!-- Transcript Tab -->
<mat-tab label="Transcript">
<ng-template matTabContent>
<app-raw-markdown></app-raw-markdown>
</ng-template>
</mat-tab>

<!-- Compliance Tab -->
<mat-tab label="Compliance">
<ng-template matTabContent>
<div class="compliance-tab-content">
<div *ngIf="isLoadingComplianceSummary" class="compliance-summary-loading">
Loading compliance summary...
</div>
<div *ngIf="!isLoadingComplianceSummary && complianceSummaryHtml" class="compliance-summary-block">
<h3 class="compliance-summary-title">Compliance Summary</h3>
<div class="compliance-summary-content" [innerHTML]="complianceSummaryHtml"></div>
</div>
</div>
</ng-template>
</mat-tab>

</mat-tab-group>
</div>
</div>
</div>

Updated Styles

File: web-ng-m3/src/app/components/project/pages/overview/overview.component.css

/* Existing styles... */

/* Nested Tabs Section */
.nested-tabs-section {
margin-top: 32px;
}

.nested-tab-group {
border: 1px solid #e3e8ee;
border-radius: 8px;
overflow: hidden;
}

/* Tab content padding */
.nested-tab-group ::ng-deep .mat-mdc-tab-body-content {
padding: 0;
}

/* Compliance tab content styling */
.compliance-tab-content {
padding: 24px;
min-height: 400px;
}

.compliance-summary-loading {
color: #888;
font-style: italic;
padding: 16px 0;
}

.compliance-summary-block {
padding: 20px 24px;
background: #f7fafd;
border-radius: 8px;
border: 1px solid #e3e8ee;
}

.compliance-summary-title {
margin-top: 0;
margin-bottom: 16px;
color: #22508d;
font-size: 1.2em;
font-weight: 600;
}

.compliance-summary-content {
line-height: 1.6;
color: rgba(0, 0, 0, 0.87);
}

.compliance-summary-content p {
margin-bottom: 12px;
}

.compliance-summary-content p:last-child {
margin-bottom: 0;
}

.compliance-summary-content ul,
.compliance-summary-content ol {
margin-bottom: 12px;
padding-left: 24px;
}

.compliance-summary-content li {
margin-bottom: 4px;
}

.compliance-summary-content h1,
.compliance-summary-content h2,
.compliance-summary-content h3,
.compliance-summary-content h4,
.compliance-summary-content h5,
.compliance-summary-content h6 {
color: #22508d;
margin-top: 16px;
margin-bottom: 8px;
}

.compliance-summary-content h1:first-child,
.compliance-summary-content h2:first-child,
.compliance-summary-content h3:first-child,
.compliance-summary-content h4:first-child,
.compliance-summary-content h5:first-child,
.compliance-summary-content h6:first-child {
margin-top: 0;
}

4. MainViewComponent Updates

File: web-ng-m3/src/app/components/project/main-view/main-view.component.ts

Add Legacy URL Redirect Logic

ngOnInit() {
// Existing code...

// Handle legacy URL redirects
this.route.params
.pipe(takeUntil(this.destroy$))
.subscribe(() => {
this.handleLegacyUrls();
});

// Existing code...
}

/**
* Redirects legacy tab URLs to new nested structure
*/
private handleLegacyUrls(): void {
const firstChild = this.route.snapshot.firstChild;
if (!firstChild) return;

const tab = firstChild.params['tab'];
const planId = this.route.snapshot.params['planId'];
const fileId = firstChild.params['fileId'] || '1';
const pageNumber = firstChild.params['pageNumber'];

// Only redirect if we have the necessary params and it's a legacy tab
if (!planId || !pageNumber) return;

// Redirect legacy tabs to nested overview tabs
if (tab === 'details') {
this.router.navigate(
['/projects', planId, 'files', fileId, 'pages', pageNumber, 'overview', 'explanation'],
{ replaceUrl: true }
);
} else if (tab === 'raw-markdown') {
this.router.navigate(
['/projects', planId, 'files', fileId, 'pages', pageNumber, 'overview', 'transcript'],
{ replaceUrl: true }
);
} else if (tab === 'overview' && !firstChild.params['subtab']) {
// Legacy /overview without subtab → redirect to /overview/compliance (preserve existing default)
this.router.navigate(
['/projects', planId, 'files', fileId, 'pages', pageNumber, 'overview', 'compliance'],
{ replaceUrl: true }
);
}
}

Update Tab Change Handler

onTabChange(event: any): void {
const tabLabel = this.tabService.getTabLabel(event.index);
this.tabService.setActiveTab(event.index);

// Get parent and child params
const planId = this.route.snapshot.params['planId'];
const firstChild = this.route.snapshot.firstChild;
const pageNumber = firstChild ? firstChild.params['pageNumber'] : this.activePlanPage?.getPageNumber() || 1;
const fileId = firstChild ? firstChild.params['fileId'] : this.activeFileId || '1';

// If switching to Overview tab, default to 'explanation' fragment
if (tabLabel === 'overview') {
this.router.navigate(
['/projects', planId, 'files', fileId, 'pages', pageNumber, 'overview'],
{ fragment: 'explanation' }
);
} else {
this.router.navigate(['/projects', planId, 'files', fileId, 'pages', pageNumber, tabLabel]);
}
}

Update Route Parameter Reading

private handleActivePageAndTabs(): void {
if (!this.showingProjectSettings && !this.showingTasks) {
const firstChild = this.route.snapshot.firstChild;
const fileId = firstChild ? firstChild.params['fileId'] : undefined;
const pageNumber = firstChild ? +firstChild.params['pageNumber'] : NaN;
const tab = firstChild ? firstChild.params['tab'] : undefined;
const fragment = this.route.snapshot.fragment; // Fragment for nested tabs

console.log(`[MainView] Route params - fileId: ${fileId}, pageNumber: ${pageNumber}, tab: ${tab}, fragment: ${fragment}`);

// Find and set active page
this.activePlanPage = this.findPageByFileAndNumber(fileId, pageNumber) || this.pages[0] || null;

if (fileId) {
this.activeFileId = fileId;
}

if (this.activePlanPage) {
this.tabService.setActivePlanPage(this.activePlanPage);
this.planReviewerService.setSelectedPage(this.activePlanPage);
}

// Set active root tab (nested tab handled by OverviewComponent via fragment)
const tabIndex = this.tabService.getTabIndexByTabParam(tab);
this.tabService.setActiveTab(tabIndex !== -1 ? tabIndex : 0);
} else {
this.activePlanPage = null;
}
}

Note: Fragment handling is delegated to OverviewComponent - no logic needed here!

5. Component Reuse

DetailsComponent and RawMarkdownComponent

No changes required. These components are already standalone and can be embedded directly in the OverviewComponent template.

Verification:

  • Both components subscribe to tabService.activePlanPage$ for page changes ✓
  • Both components are standalone ✓
  • Both components have proper lifecycle hooks ✓

6. URL Structure

URL Patterns with Fragments

Tab CombinationURL
Overview → Explanation/projects/:planId/files/:fileId/pages/:pageNumber/overview#explanation
Overview → Transcript/projects/:planId/files/:fileId/pages/:pageNumber/overview#transcript
Overview → Compliance/projects/:planId/files/:fileId/pages/:pageNumber/overview#compliance
Overview (no fragment)/projects/:planId/files/:fileId/pages/:pageNumber/overview (defaults to #explanation)
Preview/projects/:planId/files/:fileId/pages/:pageNumber/preview
Compliance (root)/projects/:planId/files/:fileId/pages/:pageNumber/compliance

Legacy URL Redirects

Legacy URLRedirects To
/pages/1/details/pages/1/overview#explanation
/pages/1/raw-markdown/pages/1/overview#transcript
/pages/1/overview/pages/1/overview (no redirect needed, defaults to #explanation)

7. Data Flow

Nested Tab Selection Flow (Simplified with Fragments)

User clicks nested tab

onNestedTabChange(event) in OverviewComponent

Extract tabId from nestedTabs[event.index]

router.navigate([], { fragment: tabId })

Angular updates URL fragment and browser history

route.fragment Observable emits new value

setActiveNestedTabFromFragment(fragment)

activeNestedTabIndex updated

mat-tab-group [selectedIndex] updates

Key Difference: No route parameter extraction or traversal - just fragment handling!

Page Change Flow

User selects page in drawer

MainViewComponent.onPageActivated(page)

tabService.setActivePlanPage(page)

OverviewComponent subscribes to activePlanPage$

DetailsComponent subscribes to activePlanPage$

RawMarkdownComponent subscribes to activePlanPage$

All nested components react to page change

Testing Strategy

Unit Tests

TabService

describe('TabService', () => {
it('should have 3 root tabs', () => {
expect(service['tabs'].length).toBe(3);
});

it('should not include Details or Raw Markdown tabs', () => {
const tabIds = service['tabs'].map(t => t.tabId);
expect(tabIds).not.toContain('details');
expect(tabIds).not.toContain('raw-markdown');
});
});

OverviewComponent

describe('OverviewComponent', () => {
it('should have 3 nested tabs', () => {
expect(component.nestedTabs.length).toBe(3);
});

it('should default to Explanation tab (index 0) when no fragment', () => {
component.setActiveNestedTabFromFragment(null);
expect(component.activeNestedTabIndex).toBe(0);
});

it('should default to Explanation tab (index 0) when invalid fragment', () => {
component.setActiveNestedTabFromFragment('invalid-tab');
expect(component.activeNestedTabIndex).toBe(0);
});

it('should set correct tab index when valid fragment', () => {
component.setActiveNestedTabFromFragment('transcript');
expect(component.activeNestedTabIndex).toBe(1);
});

it('should update URL fragment on nested tab change', () => {
spyOn(component['router'], 'navigate');
component.onNestedTabChange({ index: 1 });
expect(component['router'].navigate).toHaveBeenCalledWith(
[],
jasmine.objectContaining({ fragment: 'transcript' })
);
});
});

Integration Tests

URL Navigation with Fragments

describe('Nested Tab Navigation', () => {
it('should navigate to overview with explanation fragment', async () => {
await router.navigate(['/projects', 'test-id', 'files', '1', 'pages', '1', 'overview'],
{ fragment: 'explanation' });
expect(location.path()).toContain('/overview#explanation');
});

it('should navigate to overview without fragment (defaults to explanation)', async () => {
await router.navigate(['/projects', 'test-id', 'files', '1', 'pages', '1', 'overview']);
expect(location.path()).toContain('/overview');
// Component should show explanation tab by default
});

it('should redirect legacy /details URL to overview#explanation', async () => {
await router.navigate(['/projects', 'test-id', 'files', '1', 'pages', '1', 'details']);
expect(location.path()).toContain('/overview#explanation');
});

it('should redirect legacy /raw-markdown URL to overview#transcript', async () => {
await router.navigate(['/projects', 'test-id', 'files', '1', 'pages', '1', 'raw-markdown']);
expect(location.path()).toContain('/overview#transcript');
});

it('should support browser back/forward with fragment changes', async () => {
await router.navigate(['/projects', 'test-id', 'files', '1', 'pages', '1', 'overview'],
{ fragment: 'explanation' });
await router.navigate([], { fragment: 'transcript' });

location.back();
expect(location.path()).toContain('#explanation');

location.forward();
expect(location.path()).toContain('#transcript');
});
});

Manual Testing Checklist

  • Navigate to /overview without fragment (should show Explanation tab)
  • Navigate to /overview#explanation (should show Explanation tab)
  • Navigate to /overview#transcript (should show Transcript tab)
  • Navigate to /overview#compliance (should show Compliance tab)
  • Navigate to /overview#invalid (should default to Explanation tab)
  • Switch between nested tabs (URL fragment should update)
  • Verify URL fragment updates on tab change
  • Use browser back button (should navigate between fragment states)
  • Use browser forward button (should navigate forward through fragment states)
  • Bookmark /overview#transcript and reload (should show Transcript tab)
  • Test legacy URL redirect: /details/overview#explanation
  • Test legacy URL redirect: /raw-markdown/overview#transcript
  • Test legacy URL: /overview (should work without redirect, defaults to Explanation)
  • Switch to Preview tab (should work unchanged)
  • Switch to Compliance tab root (should work unchanged)
  • Open compliance section report (dynamic tab should open)
  • Close dynamic compliance tab
  • Switch pages in drawer (nested tabs should reset to default fragment)
  • Verify compliance summary displays in Compliance nested tab
  • Verify page metadata always visible (not in tabs)
  • Verify fragment changes create browser history entries

Performance Considerations

Lazy Loading

Nested tab content uses matTabContent directive for lazy loading:

<mat-tab label="Explanation">
<ng-template matTabContent>
<app-details></app-details>
</ng-template>
</mat-tab>

Benefit: Components are only instantiated when tab is first activated.

Component Lifecycle

  • DetailsComponent: Loads explanation markdown on-demand
  • RawMarkdownComponent: Loads page.md on-demand
  • Compliance summary: Fetches data on page change (existing behavior)

No additional optimization needed - existing lazy loading handles performance.

Migration Plan

Step 1: Update TabService (5 minutes)

  • Remove Details and Raw Markdown tabs from root tabs array
  • Test: Verify 3 tabs shown in UI

Step 2: Update OverviewComponent (30 minutes)

  • Add MatTabsModule import
  • Add DetailsComponent and RawMarkdownComponent imports
  • Implement nested tab structure in template
  • Add fragment subscription logic (route.fragment)
  • Implement onNestedTabChange() handler
  • Migrate compliance summary section into Compliance nested tab
  • Test: Verify nested tabs render correctly

Step 3: Update MainViewComponent (15 minutes)

  • Add legacy URL redirect logic (handleLegacyUrls())
  • Update onTabChange handler to set fragment for Overview tab
  • Test: Verify legacy redirects work

Step 4: Update Styles (10 minutes)

  • Add nested tab styles to overview.component.css
  • Ensure Material Design theming applies correctly
  • Test: Verify visual appearance matches design

Step 5: Testing (1-2 hours)

  • Run unit tests (update tests to use fragments)
  • Run integration tests
  • Manual testing (full checklist - 21 items)
  • Cross-browser testing

Step 6: Deployment (30 minutes)

  • Deploy to test environment
  • UAT
  • Production deployment

Total Estimated Time: 3-4 hours (vs 6-8 hours with path-based routing)

Rollback Plan

If critical issues are discovered:

  1. Revert TabService: Re-add Details and Raw Markdown tabs
  2. Revert OverviewComponent: Remove nested tabs, restore compliance summary section
  3. Revert MainViewComponent: Remove legacy URL redirect logic
  4. Redeploy: Push reverted code

Estimated rollback time: 15-20 minutes (simpler than path-based approach)

Security Considerations

  • No new XSS risks: Compliance summary already uses [innerHTML] with sanitized content
  • No authentication changes: Existing auth guards apply
  • No authorization changes: RBAC unchanged

Accessibility

  • Keyboard Navigation: Material tabs support keyboard navigation (Tab, Arrow keys)
  • Screen Readers: Mat-tab-group has proper ARIA labels
  • Focus Management: Angular Material handles focus correctly
  • Color Contrast: Verify tab labels meet WCAG AA standards

Browser Compatibility

  • Chrome: Full support (Material 3 tested)
  • Firefox: Full support
  • Safari: Full support
  • Edge: Full support
  • Mobile browsers: Material responsive design handles mobile

Open Issues

  1. Default nested tab: ✅ RESOLVED - Fragment-based approach naturally defaults to explanation (index 0) when no fragment is present. No redirect needed!

  2. Page metadata collapse: Should page metadata (title, number, summary) be collapsible?

    • Decision: Future enhancement.
  3. Tab state persistence: Should we remember user's last selected nested tab per page?

    • Decision: Out of scope for v1.
  4. Fragment scroll behavior: Should we prevent automatic scrolling when fragment changes?

    • Decision: Test in practice - may need to add scroll prevention if Material tabs cause unwanted scrolling.

Future Enhancements

  1. State Persistence: Use localStorage to remember last selected nested tab per page
  2. Tab Icons: Add icons to nested tabs (e.g., 🤖 Explanation, 📄 Transcript, ✓ Compliance)
  3. Keyboard Shortcuts: Add shortcuts (e.g., E for Explanation, T for Transcript, C for Compliance)
  4. Loading Skeletons: Add skeleton loaders for nested tab content
  5. Animations: Add smooth transitions between nested tabs

References