Skip to content

Commit bdda8da

Browse files
committed
Split bar and percentage into aligned columns, tighten table padding
* Bar and percentage in separate td columns under colspan=2 header so bars align vertically regardless of percentage text width * Colspan reduces to 1 when bars are hidden on narrow viewports * Reduce all table cell padding from 12px to 8px to reclaim space
1 parent c921e18 commit bdda8da

8 files changed

Lines changed: 148 additions & 85 deletions

File tree

Gemfile.lock

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ GEM
5757
racc (~> 1.4)
5858
nokogiri (1.19.2-x86_64-linux-gnu)
5959
racc (~> 1.4)
60-
parallel (1.27.0)
60+
parallel (1.28.0)
6161
parser (3.3.11.1)
6262
ast (~> 2.4.1)
6363
racc
@@ -109,7 +109,7 @@ GEM
109109
rubocop (>= 1.72.1)
110110
ruby-progressbar (1.13.0)
111111
simplecov_json_formatter (0.1.4)
112-
sorbet-runtime (0.6.13073)
112+
sorbet-runtime (0.6.13080)
113113
stringio (3.2.0)
114114
tsort (0.2.0)
115115
unicode-display_width (3.2.0)

assets/stylesheets/screen.css

Lines changed: 45 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -351,6 +351,11 @@ abbr.timeago {
351351
gap: var(--sp-2);
352352
}
353353

354+
table.file_list th.cell--coverage .th-with-filter {
355+
white-space: nowrap;
356+
justify-content: flex-end;
357+
}
358+
354359
.th-with-filter .th-label {
355360
white-space: nowrap;
356361
}
@@ -405,26 +410,29 @@ abbr.timeago {
405410
/* --- File list table --------------------------------------- */
406411

407412
.file_list--responsive {
408-
overflow-x: auto;
409413
}
410414

411415
table.file_list {
412416
width: 100%;
413417
font-size: 18px;
414418
}
415419

420+
table.file_list {
421+
border-collapse: separate;
422+
border-spacing: 0;
423+
}
424+
425+
416426
table.file_list thead th {
417427
font-size: 14px;
418428
font-weight: 600;
419429
text-transform: uppercase;
420430
letter-spacing: 0.04em;
421431
color: var(--text-tertiary);
422432
background: var(--surface);
423-
padding: var(--sp-2) var(--sp-3);
433+
padding: var(--sp-2) var(--sp-2);
424434
border-bottom: 2px solid var(--border-strong);
425435
white-space: nowrap;
426-
position: sticky;
427-
top: 0;
428436
}
429437

430438
table.file_list tbody tr {
@@ -441,7 +449,7 @@ table.file_list tbody tr:hover {
441449
}
442450

443451
table.file_list tbody td {
444-
padding: var(--sp-2) var(--sp-3);
452+
padding: var(--sp-2) var(--sp-2);
445453
border-bottom: 1px solid var(--border);
446454
color: var(--text);
447455
}
@@ -461,6 +469,16 @@ table.file_list th.cell--number {
461469
text-align: right;
462470
}
463471

472+
table.file_list th.cell--numerator {
473+
text-align: right;
474+
padding-right: 0;
475+
}
476+
477+
table.file_list th.cell--denominator {
478+
text-align: left;
479+
padding-left: 0;
480+
}
481+
464482
table.file_list td.strong {
465483
font-weight: 600;
466484
color: var(--text);
@@ -478,23 +496,35 @@ a.src_link {
478496

479497
/* --- Coverage columns -------------------------------------- */
480498

481-
.cell--bar {
482-
padding-right: 0;
499+
table.file_list td.cell--bar {
500+
padding-right: 4px;
501+
padding-left: 0;
502+
}
503+
504+
table.file_list td.cell--bar .coverage-bar {
505+
margin-left: auto;
483506
}
484507

485-
.cell--pct {
508+
table.file_list td.cell--pct {
486509
text-align: right;
487510
font-variant-numeric: tabular-nums;
488511
white-space: nowrap;
489-
padding-left: var(--sp-2);
490-
padding-right: var(--sp-1);
512+
width: 1%;
513+
padding-left: 4px;
514+
}
515+
516+
table.file_list .totals-row td.cell--bar {
517+
padding-right: 4px;
518+
padding-left: 0;
519+
}
520+
521+
table.file_list .totals-row td.cell--pct {
522+
padding-left: 4px;
491523
}
492524

493525
table.file_list td.cell--numerator {
494526
text-align: right;
495527
font-variant-numeric: tabular-nums;
496-
color: var(--text-secondary);
497-
font-size: 14px;
498528
white-space: nowrap;
499529
padding-left: var(--sp-1);
500530
padding-right: 0;
@@ -503,8 +533,6 @@ table.file_list td.cell--numerator {
503533
table.file_list td.cell--denominator {
504534
text-align: left;
505535
font-variant-numeric: tabular-nums;
506-
color: var(--text-secondary);
507-
font-size: 14px;
508536
white-space: nowrap;
509537
padding-left: 0;
510538
padding-right: var(--sp-1);
@@ -663,7 +691,7 @@ dialog.source-dialog[open] {
663691
}
664692

665693
table.file_list .totals-row td {
666-
padding: var(--sp-3) var(--sp-3);
694+
padding: var(--sp-2) var(--sp-2);
667695
font-weight: 600;
668696
border-bottom: 2px solid var(--border-strong);
669697
background: var(--surface-alt);
@@ -839,14 +867,14 @@ table.file_list thead th.sorting_asc,
839867
table.file_list thead th.sorting_desc {
840868
cursor: pointer;
841869
position: relative;
842-
padding-right: 20px;
870+
padding-right: 12px;
843871
}
844872

845873
table.file_list thead th.sorting::after,
846874
table.file_list thead th.sorting_asc::after,
847875
table.file_list thead th.sorting_desc::after {
848876
position: absolute;
849-
right: 4px;
877+
right: 2px;
850878
top: 50%;
851879
transform: translateY(-50%);
852880
font-size: 14px;

public/application.css

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

public/application.js

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/app.ts

Lines changed: 54 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -129,11 +129,14 @@ function sortTable(table: Element, colIndex: number): void {
129129
rows.forEach(row => tbody.appendChild(row));
130130

131131
// Update sort indicators
132-
let idx = 0;
132+
let tdPos = 0;
133133
$$('thead tr:first-child th', table).forEach((th) => {
134+
const span = parseInt(th.getAttribute('colspan') || '1', 10);
134135
th.classList.remove('sorting_asc', 'sorting_desc', 'sorting');
135-
th.classList.add(idx === colIndex ? (dir === 'asc' ? 'sorting_asc' : 'sorting_desc') : 'sorting');
136-
idx += parseInt(th.getAttribute('colspan') || '1', 10);
136+
// colIndex falls within this th if it's >= tdPos and < tdPos + span
137+
const isActive = colIndex >= tdPos && colIndex < tdPos + span;
138+
th.classList.add(isActive ? (dir === 'asc' ? 'sorting_asc' : 'sorting_desc') : 'sorting');
139+
tdPos += span;
137140
});
138141
}
139142

@@ -212,6 +215,7 @@ function filterTable(container: Element): void {
212215
});
213216

214217
updateTotalsRow(container);
218+
equalizeBarWidths();
215219
}
216220

217221
function updateFilterOptions(input: HTMLInputElement): void {
@@ -252,22 +256,16 @@ function updateTotalsRow(container: Element): void {
252256
const relevantLines = sumData('relevantLines');
253257
updateCoverageCells(container, '.t-totals__line', coveredLines, relevantLines);
254258

255-
const numberCells = $$('.totals-row .cell--number', container);
256-
if (numberCells[0]) numberCells[0].textContent = relevantLines ? fmtNum(relevantLines) : '';
257-
258259
if ($('.t-totals__branch-pct', container)) {
259260
const coveredBranches = sumData('coveredBranches');
260261
const totalBranches = sumData('totalBranches');
261262
updateCoverageCells(container, '.t-totals__branch', coveredBranches, totalBranches);
262-
if (numberCells[1]) numberCells[1].textContent = totalBranches ? fmtNum(totalBranches) : '';
263263
}
264264

265265
if ($('.t-totals__method-pct', container)) {
266266
const coveredMethods = sumData('coveredMethods');
267267
const totalMethods = sumData('totalMethods');
268268
updateCoverageCells(container, '.t-totals__method', coveredMethods, totalMethods);
269-
const numIdx = $('.t-totals__branch-pct', container) ? 2 : 1;
270-
if (numberCells[numIdx]) numberCells[numIdx].textContent = totalMethods ? fmtNum(totalMethods) : '';
271269
}
272270
}
273271

@@ -292,18 +290,18 @@ function materializeSourceFile(sourceFileId: string): HTMLElement | null {
292290

293291
// --- Bar width equalization ------------------------------------
294292

295-
function setBarWidth(bars: Element[], headers: Element[], px: number): void {
293+
function setBarWidth(bars: Element[], table: Element, px: number): void {
296294
const w = px + 'px';
297-
headers.forEach(h => h.setAttribute('colspan', '4'));
295+
$$('th.cell--coverage', table).forEach(h => h.setAttribute('colspan', '2'));
298296
bars.forEach(b => {
299297
const s = (b as HTMLElement).style;
300298
s.display = '';
301299
s.width = w; s.minWidth = w; s.maxWidth = w;
302300
});
303301
}
304302

305-
function hideBars(bars: Element[], headers: Element[]): void {
306-
headers.forEach(h => h.setAttribute('colspan', '3'));
303+
function hideBars(bars: Element[], table: Element): void {
304+
$$('th.cell--coverage', table).forEach(h => h.setAttribute('colspan', '1'));
307305
bars.forEach(b => {
308306
const s = (b as HTMLElement).style;
309307
s.display = 'none'; s.width = ''; s.minWidth = ''; s.maxWidth = '';
@@ -318,33 +316,43 @@ function equalizeBarWidths(): void {
318316
const table = $('table.file_list', container) as HTMLTableElement | null;
319317
if (!table) return;
320318
const bars = $$('td.cell--bar', table);
321-
const headers = $$('th[colspan]', table);
322319
if (bars.length === 0) return;
323320

324321
const wrapper = table.closest('.file_list--responsive') as HTMLElement | null;
325322
if (!wrapper) return;
326323

327-
const firstDataRow = $('tbody tr', table) || $('thead tr.totals-row', table);
328-
const barsPerRow = firstDataRow ? $$('td.cell--bar', firstDataRow).length : 1;
329-
330-
// Step 1: Hide bars, measure content width with table at auto
331-
hideBars(bars, headers);
332-
table.style.width = 'auto';
333-
void table.offsetWidth;
334-
const contentWidth = table.scrollWidth;
335-
336-
// Step 2: Restore table width, measure available space
337-
table.style.width = '';
338-
void table.offsetWidth;
339-
const availableWidth = wrapper.clientWidth;
340-
341-
// Step 3: Calculate bar width
342-
const totalBarSpace = availableWidth - contentWidth;
343-
const perBar = Math.floor(totalBarSpace / barsPerRow);
344-
345-
if (perBar < 100) return; // keep bars hidden
324+
// Hide during measurement to prevent flicker
325+
wrapper.style.visibility = 'hidden';
326+
327+
// Test whether bars at a given width fit without overflow
328+
const fitsAt = (px: number): boolean => {
329+
setBarWidth(bars, table, px);
330+
table.style.width = 'auto';
331+
void table.offsetWidth;
332+
const fits = table.scrollWidth <= wrapper.clientWidth;
333+
table.style.width = '';
334+
return fits;
335+
};
336+
337+
let barWidth = 240;
338+
if (!fitsAt(240)) {
339+
if (!fitsAt(80)) {
340+
hideBars(bars, table);
341+
wrapper.style.visibility = '';
342+
return;
343+
}
344+
// Binary search for the widest bars that fit
345+
let lo = 80, hi = 239;
346+
while (lo < hi) {
347+
const mid = Math.ceil((lo + hi) / 2);
348+
if (fitsAt(mid)) lo = mid;
349+
else hi = mid - 1;
350+
}
351+
barWidth = lo;
352+
}
346353

347-
setBarWidth(bars, headers, Math.min(perBar, 200));
354+
setBarWidth(bars, table, barWidth);
355+
wrapper.style.visibility = '';
348356
});
349357
}
350358

@@ -472,15 +480,21 @@ document.addEventListener('DOMContentLoaded', function () {
472480
});
473481
})();
474482

475-
// Table sorting — compute td index from th colspan
483+
// Table sorting — compute td index dynamically at click time
484+
function thToTdIndex(table: Element, clickedTh: Element): number {
485+
let idx = 0;
486+
for (const th of $$('thead tr:first-child th', table)) {
487+
if (th === clickedTh) return idx;
488+
idx += parseInt(th.getAttribute('colspan') || '1', 10);
489+
}
490+
return idx;
491+
}
492+
476493
$$('table.file_list').forEach(table => {
477-
let tdIndex = 0;
478494
$$('thead tr:first-child th', table).forEach((th) => {
479-
const myTdIndex = tdIndex;
480495
th.classList.add('sorting');
481496
(th as HTMLElement).style.cursor = 'pointer';
482-
th.addEventListener('click', () => sortTable(table, myTdIndex));
483-
tdIndex += parseInt(th.getAttribute('colspan') || '1', 10);
497+
th.addEventListener('click', () => sortTable(table, thToTdIndex(table, th)));
484498
});
485499
});
486500

@@ -596,4 +610,5 @@ document.addEventListener('DOMContentLoaded', function () {
596610

597611
// Equalize bar widths now that wrapper is visible
598612
equalizeBarWidths();
613+
599614
});

test/test_simple_cov-html.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ def test_output_header_coverage
4343

4444
def test_output_line_coverages
4545
html_doc = format_results(CoverageFixtures::ALL_FIXTURES)
46-
pcts = html_doc.css("div#AllFiles table.file_list tr.t-file td.cell--line-pct")
46+
pcts = html_doc.css("div#AllFiles table.file_list tr.t-file .cell--line-pct")
4747
table = pcts.map { |m| m.content.strip }
4848

4949
assert_equal EXPECTED_LINE_COVERAGES, table.sort_by(&:to_f)
@@ -58,7 +58,7 @@ def test_output_branch_coverages
5858

5959
assert branch_pct, "Expected branch coverage totals row"
6060

61-
pcts = html_doc.css("div#AllFiles table.file_list tr.t-file td.cell--branch-pct")
61+
pcts = html_doc.css("div#AllFiles table.file_list tr.t-file .cell--branch-pct")
6262
table = pcts.map { |m| m.content.strip }
6363

6464
assert_equal EXPECTED_BRANCH_COVERAGES, table.sort_by(&:to_f)

test/test_view_helpers.rb

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,29 @@ def test_to_id_returns_empty_for_all_invalid
130130
assert_equal "", formatter.send(:to_id, "!@#")
131131
end
132132

133+
cover "SimpleCov::Formatter::HTMLFormatter::ViewHelpers#fmt" if respond_to?(:cover)
134+
135+
def test_fmt_small_numbers
136+
assert_equal "0", formatter.send(:fmt, 0)
137+
assert_equal "1", formatter.send(:fmt, 1)
138+
assert_equal "999", formatter.send(:fmt, 999)
139+
end
140+
141+
def test_fmt_thousands
142+
assert_equal "1,000", formatter.send(:fmt, 1000)
143+
assert_equal "1,234", formatter.send(:fmt, 1234)
144+
assert_equal "9,999", formatter.send(:fmt, 9999)
145+
end
146+
147+
def test_fmt_millions
148+
assert_equal "1,000,000", formatter.send(:fmt, 1_000_000)
149+
assert_equal "1,234,567", formatter.send(:fmt, 1_234_567)
150+
end
151+
152+
def test_fmt_preserves_string_input
153+
assert_equal "12,345", formatter.send(:fmt, "12345")
154+
end
155+
133156
cover "SimpleCov::Formatter::HTMLFormatter::ViewHelpers#build_stats" if respond_to?(:cover)
134157

135158
def test_build_stats_basic

0 commit comments

Comments
 (0)