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
- No nested routes: Existing routes work as-is
- Natural defaults:
/overviewwithout fragment shows default tab - Simpler code: ~80% less routing code
- 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
MainViewComponentchanges fromi >= 3toi >= 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.fragmentObservable - ✅ 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 Combination | URL |
|---|---|
| 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 URL | Redirects 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
/overviewwithout 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#transcriptand 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
onTabChangehandler 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:
- Revert TabService: Re-add Details and Raw Markdown tabs
- Revert OverviewComponent: Remove nested tabs, restore compliance summary section
- Revert MainViewComponent: Remove legacy URL redirect logic
- 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
-
Default nested tab: ✅ RESOLVED - Fragment-based approach naturally defaults to
explanation(index 0) when no fragment is present. No redirect needed! -
Page metadata collapse: Should page metadata (title, number, summary) be collapsible?
- Decision: Future enhancement.
-
Tab state persistence: Should we remember user's last selected nested tab per page?
- Decision: Out of scope for v1.
-
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
- State Persistence: Use localStorage to remember last selected nested tab per page
- Tab Icons: Add icons to nested tabs (e.g., 🤖 Explanation, 📄 Transcript, ✓ Compliance)
- Keyboard Shortcuts: Add shortcuts (e.g.,
Efor Explanation,Tfor Transcript,Cfor Compliance) - Loading Skeletons: Add skeleton loaders for nested tab content
- Animations: Add smooth transitions between nested tabs
References
- Material Tabs Documentation: https://material.angular.io/components/tabs/overview
- Angular Routing: https://angular.dev/guide/routing
- Issue #258: AI-Powered Plan Page Explanation
- PRD:
docs/04-prd/overview-nested-tabs.md