Skip to content

Commit 581564e

Browse files
committed
feat: Add collapsible sections with summary navigation to HTML reports
Implements Issue #34 to make HTML reports easier to use by adding: - Clickable summary cards that navigate to corresponding sections - Collapsible sections with toggle buttons and smooth animations - Session storage to persist section states across page reloads - Sections start in collapsed state by default - Proper icon rotation (▶ when collapsed, ▼ when expanded) - Smooth scrolling navigation with highlight effects Technical changes: - main-layout.scriban: Added clickable summary cards and collapsible section structure - styles.css: Added collapsible section styles, hover effects, and responsive design - scripts.js: Added toggleSection, navigateToSection, and initializeSections functions with session storage All tests pass and HTML report generation works correctly.
1 parent 6bbeda1 commit 581564e

File tree

3 files changed

+248
-12
lines changed

3 files changed

+248
-12
lines changed

src/DotNetApiDiff/Reporting/HtmlTemplates/main-layout.scriban

Lines changed: 54 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -56,22 +56,50 @@
5656
<section class="summary">
5757
<h2>📈 Summary</h2>
5858
<div class="summary-cards">
59+
{{if result.summary.added_count > 0}}
60+
<div class="summary-card added clickable" onclick="navigateToSection('added-items')">
61+
<div class="card-number">{{ result.summary.added_count }}</div>
62+
<div class="card-label">Added</div>
63+
</div>
64+
{{else}}
5965
<div class="summary-card added">
6066
<div class="card-number">{{ result.summary.added_count }}</div>
6167
<div class="card-label">Added</div>
6268
</div>
69+
{{end}}
70+
{{if result.summary.removed_count > 0}}
71+
<div class="summary-card removed clickable" onclick="navigateToSection('removed-items')">
72+
<div class="card-number">{{ result.summary.removed_count }}</div>
73+
<div class="card-label">Removed</div>
74+
</div>
75+
{{else}}
6376
<div class="summary-card removed">
6477
<div class="card-number">{{ result.summary.removed_count }}</div>
6578
<div class="card-label">Removed</div>
6679
</div>
80+
{{end}}
81+
{{if result.summary.modified_count > 0}}
82+
<div class="summary-card modified clickable" onclick="navigateToSection('modified-items')">
83+
<div class="card-number">{{ result.summary.modified_count }}</div>
84+
<div class="card-label">Modified</div>
85+
</div>
86+
{{else}}
6787
<div class="summary-card modified">
6888
<div class="card-number">{{ result.summary.modified_count }}</div>
6989
<div class="card-label">Modified</div>
7090
</div>
91+
{{end}}
92+
{{if result.summary.breaking_changes_count > 0}}
93+
<div class="summary-card breaking clickable" onclick="navigateToSection('breaking-changes')">
94+
<div class="card-number">{{ result.summary.breaking_changes_count }}</div>
95+
<div class="card-label">Breaking</div>
96+
</div>
97+
{{else}}
7198
<div class="summary-card breaking">
7299
<div class="card-number">{{ result.summary.breaking_changes_count }}</div>
73100
<div class="card-label">Breaking</div>
74101
</div>
102+
{{end}}
75103
</div>
76104
</section>
77105

@@ -91,13 +119,19 @@
91119

92120
<!-- Breaking changes section -->
93121
{{if result.has_breaking_changes}}
94-
<section class="breaking-changes">
95-
<h2>⚠️ Breaking Changes</h2>
96-
<div class="alert alert-danger">
97-
<strong>Warning:</strong> The following changes may break compatibility with existing code.
122+
<section class="breaking-changes" id="breaking-changes">
123+
<div class="section-header">
124+
<h2>⚠️ Breaking Changes</h2>
125+
<button class="section-toggle collapsed" onclick="toggleSection('breaking-changes')">
126+
<span class="toggle-icon">▶</span>
127+
</button>
98128
</div>
99-
<!-- Breaking changes content directly inline for debugging -->
100-
<div class="breaking-changes-table">
129+
<div class="section-content collapsed" id="breaking-changes-content">
130+
<div class="alert alert-danger">
131+
<strong>Warning:</strong> The following changes may break compatibility with existing code.
132+
</div>
133+
<!-- Breaking changes content directly inline for debugging -->
134+
<div class="breaking-changes-table">
101135
<table>
102136
<thead>
103137
<tr>
@@ -119,17 +153,25 @@
119153
</tbody>
120154
</table>
121155
</div>
156+
</div>
122157
</section>
123158
{{end}}
124159

125160
<!-- Change sections -->
126161
{{for section in change_sections}}
127-
<section class="changes-section">
128-
<h2>{{ section.icon }} {{ section.title }} ({{ section.count }})</h2>
129-
{{if section.description}}
130-
<p class="section-description">{{ section.description }}</p>
131-
{{end}}
132-
{{ render_change_group section }}
162+
<section class="changes-section" id="{{ section.change_type }}-items">
163+
<div class="section-header">
164+
<h2>{{ section.icon }} {{ section.title }} ({{ section.count }})</h2>
165+
<button class="section-toggle collapsed" onclick="toggleSection('{{ section.change_type }}-items')">
166+
<span class="toggle-icon">▶</span>
167+
</button>
168+
</div>
169+
<div class="section-content collapsed" id="{{ section.change_type }}-items-content">
170+
{{if section.description}}
171+
<p class="section-description">{{ section.description }}</p>
172+
{{end}}
173+
{{ render_change_group section }}
174+
</div>
133175
</section>
134176
{{end}}
135177
</div>

src/DotNetApiDiff/Reporting/HtmlTemplates/scripts.js

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,3 +30,104 @@ function toggleConfig() {
3030
text.textContent = 'Show Configuration Details';
3131
}
3232
}
33+
34+
// Session storage helpers
35+
function setSectionState(sectionId, state) {
36+
try {
37+
sessionStorage.setItem('section-' + sectionId, state);
38+
} catch (e) {
39+
// Ignore storage errors
40+
}
41+
}
42+
43+
function getSectionState(sectionId) {
44+
try {
45+
return sessionStorage.getItem('section-' + sectionId);
46+
} catch (e) {
47+
return null;
48+
}
49+
}
50+
51+
// New collapsible section functionality
52+
function toggleSection(sectionId) {
53+
const section = document.getElementById(sectionId);
54+
const content = document.getElementById(sectionId + '-content');
55+
const button = section.querySelector('.section-toggle');
56+
const icon = button.querySelector('.toggle-icon');
57+
58+
if (content.classList.contains('collapsed')) {
59+
// Expand section
60+
content.classList.remove('collapsed');
61+
button.classList.remove('collapsed');
62+
icon.textContent = '▶'; // Keep using ▶ and let CSS handle rotation
63+
setSectionState(sectionId, 'expanded');
64+
} else {
65+
// Collapse section
66+
content.classList.add('collapsed');
67+
button.classList.add('collapsed');
68+
icon.textContent = '▶'; // Keep using ▶ and let CSS handle rotation
69+
setSectionState(sectionId, 'collapsed');
70+
}
71+
}
72+
73+
// Navigate to section with smooth scrolling and auto-expand
74+
function navigateToSection(sectionId) {
75+
const section = document.getElementById(sectionId);
76+
const content = document.getElementById(sectionId + '-content');
77+
78+
if (!section) return;
79+
80+
// Auto-expand if collapsed
81+
if (content && content.classList.contains('collapsed')) {
82+
toggleSection(sectionId);
83+
}
84+
85+
// Smooth scroll to section
86+
section.scrollIntoView({
87+
behavior: 'smooth',
88+
block: 'start'
89+
});
90+
91+
// Brief highlight effect
92+
section.style.transition = 'box-shadow 0.3s ease';
93+
section.style.boxShadow = '0 0 20px rgba(0, 123, 255, 0.3)';
94+
setTimeout(() => {
95+
section.style.boxShadow = '';
96+
}, 1500);
97+
}
98+
99+
// Initialize sections to collapsed state by default
100+
function initializeSections() {
101+
const sections = ['breaking-changes', 'added-items', 'removed-items', 'modified-items'];
102+
103+
sections.forEach(sectionId => {
104+
const section = document.getElementById(sectionId);
105+
const content = document.getElementById(sectionId + '-content');
106+
const button = section?.querySelector('.section-toggle');
107+
const icon = button?.querySelector('.toggle-icon');
108+
109+
if (!section || !content || !button || !icon) return;
110+
111+
// Check session storage first
112+
const savedState = getSectionState(sectionId);
113+
114+
if (savedState === 'expanded') {
115+
// Expand based on saved state
116+
content.classList.remove('collapsed');
117+
button.classList.remove('collapsed');
118+
icon.textContent = '▶';
119+
} else {
120+
// Default to collapsed (including null/new sessions)
121+
content.classList.add('collapsed');
122+
button.classList.add('collapsed');
123+
icon.textContent = '▶';
124+
// Save the default state if not already set
125+
if (savedState === null) {
126+
setSectionState(sectionId, 'collapsed');
127+
}
128+
}
129+
});
130+
}
131+
132+
// Initialize sections when the page loads
133+
document.addEventListener('DOMContentLoaded', initializeSections);

src/DotNetApiDiff/Reporting/HtmlTemplates/styles.css

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,84 @@ section h2 {
5454
font-size: 1.5rem;
5555
}
5656

57+
/* Collapsible Section Styles */
58+
.section-header {
59+
display: flex;
60+
justify-content: space-between;
61+
align-items: center;
62+
background: #f8f9fa;
63+
padding: 15px 20px;
64+
margin: 0;
65+
border-bottom: 1px solid #dee2e6;
66+
}
67+
68+
.section-header h2 {
69+
background: none;
70+
padding: 0;
71+
margin: 0;
72+
border: none;
73+
font-size: 1.5rem;
74+
}
75+
76+
.section-toggle {
77+
background: none;
78+
border: none;
79+
cursor: pointer;
80+
padding: 8px;
81+
border-radius: 4px;
82+
color: #6c757d;
83+
transition: all 0.2s ease;
84+
display: flex;
85+
align-items: center;
86+
justify-content: center;
87+
min-width: 32px;
88+
min-height: 32px;
89+
}
90+
91+
.section-toggle:hover {
92+
background-color: #e9ecef;
93+
color: #495057;
94+
}
95+
96+
.section-toggle:focus {
97+
outline: 2px solid #007bff;
98+
outline-offset: 2px;
99+
}
100+
101+
.toggle-icon {
102+
font-size: 14px;
103+
transition: transform 0.3s ease;
104+
}
105+
106+
.section-toggle.collapsed .toggle-icon {
107+
transform: rotate(0deg); /* No rotation - ▶ points right when collapsed */
108+
}
109+
110+
.section-toggle:not(.collapsed) .toggle-icon {
111+
transform: rotate(90deg); /* Rotate ▶ to point down when expanded */
112+
}
113+
114+
.section-content {
115+
transition: all 0.3s ease;
116+
overflow: hidden;
117+
}
118+
119+
.section-content.collapsed {
120+
max-height: 0;
121+
opacity: 0;
122+
padding-top: 0;
123+
padding-bottom: 0;
124+
margin-top: 0;
125+
margin-bottom: 0;
126+
}
127+
128+
.section-content:not(.collapsed) {
129+
max-height: none;
130+
opacity: 1;
131+
padding-top: 0;
132+
padding-bottom: 0;
133+
}
134+
57135
.metadata-content {
58136
padding: 20px;
59137
}
@@ -157,6 +235,21 @@ section h2 {
157235
border-radius: 8px;
158236
color: white;
159237
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
238+
transition: transform 0.2s ease, box-shadow 0.2s ease;
239+
}
240+
241+
.summary-card.clickable {
242+
cursor: pointer;
243+
}
244+
245+
.summary-card.clickable:hover {
246+
transform: translateY(-2px);
247+
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
248+
}
249+
250+
.summary-card.clickable:active {
251+
transform: translateY(0);
252+
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
160253
}
161254

162255
.summary-card.added {

0 commit comments

Comments
 (0)