Skip to content

Commit ce8d00c

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 ce8d00c

File tree

8 files changed

+215
-88
lines changed

8 files changed

+215
-88
lines changed

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: 69 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,18 @@ interface SortEntry {
9595

9696
const sortState: Record<string, SortEntry> = {};
9797

98-
function getSortValue(td: Element): number | string {
98+
function getVisibleChild(row: Element, index: number): Element | null {
99+
let count = 0;
100+
for (let i = 0; i < row.children.length; i++) {
101+
if ((row.children[i] as HTMLElement).style.display === 'none') continue;
102+
if (count === index) return row.children[i];
103+
count++;
104+
}
105+
return null;
106+
}
107+
108+
function getSortValue(td: Element | null): number | string {
109+
if (!td) return '';
99110
const order = td.getAttribute('data-order');
100111
if (order !== null) return parseFloat(order);
101112
const text = (td.textContent || '').trim();
@@ -115,8 +126,8 @@ function sortTable(table: Element, colIndex: number): void {
115126
const rows = Array.from(tbody.querySelectorAll('tr.t-file'));
116127

117128
rows.sort((a, b) => {
118-
const aVal = getSortValue(a.children[colIndex]);
119-
const bVal = getSortValue(b.children[colIndex]);
129+
const aVal = getSortValue(getVisibleChild(a, colIndex));
130+
const bVal = getSortValue(getVisibleChild(b, colIndex));
120131
let cmp: number;
121132
if (typeof aVal === 'number' && typeof bVal === 'number') {
122133
cmp = aVal - bVal;
@@ -129,11 +140,14 @@ function sortTable(table: Element, colIndex: number): void {
129140
rows.forEach(row => tbody.appendChild(row));
130141

131142
// Update sort indicators
132-
let idx = 0;
143+
let tdPos = 0;
133144
$$('thead tr:first-child th', table).forEach((th) => {
145+
const span = parseInt(th.getAttribute('colspan') || '1', 10);
134146
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);
147+
// colIndex falls within this th if it's >= tdPos and < tdPos + span
148+
const isActive = colIndex >= tdPos && colIndex < tdPos + span;
149+
th.classList.add(isActive ? (dir === 'asc' ? 'sorting_asc' : 'sorting_desc') : 'sorting');
150+
tdPos += span;
137151
});
138152
}
139153

@@ -212,6 +226,7 @@ function filterTable(container: Element): void {
212226
});
213227

214228
updateTotalsRow(container);
229+
equalizeBarWidths();
215230
}
216231

217232
function updateFilterOptions(input: HTMLInputElement): void {
@@ -252,22 +267,16 @@ function updateTotalsRow(container: Element): void {
252267
const relevantLines = sumData('relevantLines');
253268
updateCoverageCells(container, '.t-totals__line', coveredLines, relevantLines);
254269

255-
const numberCells = $$('.totals-row .cell--number', container);
256-
if (numberCells[0]) numberCells[0].textContent = relevantLines ? fmtNum(relevantLines) : '';
257-
258270
if ($('.t-totals__branch-pct', container)) {
259271
const coveredBranches = sumData('coveredBranches');
260272
const totalBranches = sumData('totalBranches');
261273
updateCoverageCells(container, '.t-totals__branch', coveredBranches, totalBranches);
262-
if (numberCells[1]) numberCells[1].textContent = totalBranches ? fmtNum(totalBranches) : '';
263274
}
264275

265276
if ($('.t-totals__method-pct', container)) {
266277
const coveredMethods = sumData('coveredMethods');
267278
const totalMethods = sumData('totalMethods');
268279
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) : '';
271280
}
272281
}
273282

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

293302
// --- Bar width equalization ------------------------------------
294303

295-
function setBarWidth(bars: Element[], headers: Element[], px: number): void {
304+
function setBarWidth(bars: Element[], table: Element, px: number): void {
296305
const w = px + 'px';
297-
headers.forEach(h => h.setAttribute('colspan', '4'));
306+
$$('th.cell--coverage', table).forEach(h => h.setAttribute('colspan', '2'));
298307
bars.forEach(b => {
299308
const s = (b as HTMLElement).style;
300309
s.display = '';
301310
s.width = w; s.minWidth = w; s.maxWidth = w;
302311
});
303312
}
304313

305-
function hideBars(bars: Element[], headers: Element[]): void {
306-
headers.forEach(h => h.setAttribute('colspan', '3'));
314+
function hideBars(bars: Element[], table: Element): void {
315+
$$('th.cell--coverage', table).forEach(h => h.setAttribute('colspan', '1'));
307316
bars.forEach(b => {
308317
const s = (b as HTMLElement).style;
309318
s.display = 'none'; s.width = ''; s.minWidth = ''; s.maxWidth = '';
@@ -318,33 +327,43 @@ function equalizeBarWidths(): void {
318327
const table = $('table.file_list', container) as HTMLTableElement | null;
319328
if (!table) return;
320329
const bars = $$('td.cell--bar', table);
321-
const headers = $$('th[colspan]', table);
322330
if (bars.length === 0) return;
323331

324332
const wrapper = table.closest('.file_list--responsive') as HTMLElement | null;
325333
if (!wrapper) return;
326334

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
335+
// Hide during measurement to prevent flicker
336+
wrapper.style.visibility = 'hidden';
337+
338+
// Test whether bars at a given width fit without overflow
339+
const fitsAt = (px: number): boolean => {
340+
setBarWidth(bars, table, px);
341+
table.style.width = 'auto';
342+
void table.offsetWidth;
343+
const fits = table.scrollWidth <= wrapper.clientWidth;
344+
table.style.width = '';
345+
return fits;
346+
};
347+
348+
let barWidth = 240;
349+
if (!fitsAt(240)) {
350+
if (!fitsAt(80)) {
351+
hideBars(bars, table);
352+
wrapper.style.visibility = '';
353+
return;
354+
}
355+
// Binary search for the widest bars that fit
356+
let lo = 80, hi = 239;
357+
while (lo < hi) {
358+
const mid = Math.ceil((lo + hi) / 2);
359+
if (fitsAt(mid)) lo = mid;
360+
else hi = mid - 1;
361+
}
362+
barWidth = lo;
363+
}
346364

347-
setBarWidth(bars, headers, Math.min(perBar, 200));
365+
setBarWidth(bars, table, barWidth);
366+
wrapper.style.visibility = '';
348367
});
349368
}
350369

@@ -472,15 +491,22 @@ document.addEventListener('DOMContentLoaded', function () {
472491
});
473492
})();
474493

475-
// Table sorting — compute td index from th colspan
494+
// Table sorting — compute td index dynamically at click time
495+
function thToTdIndex(table: Element, clickedTh: Element): number {
496+
let idx = 0;
497+
for (const th of $$('thead tr:first-child th', table)) {
498+
const span = parseInt(th.getAttribute('colspan') || '1', 10);
499+
if (th === clickedTh) return idx + span - 1;
500+
idx += span;
501+
}
502+
return idx;
503+
}
504+
476505
$$('table.file_list').forEach(table => {
477-
let tdIndex = 0;
478506
$$('thead tr:first-child th', table).forEach((th) => {
479-
const myTdIndex = tdIndex;
480507
th.classList.add('sorting');
481508
(th as HTMLElement).style.cursor = 'pointer';
482-
th.addEventListener('click', () => sortTable(table, myTdIndex));
483-
tdIndex += parseInt(th.getAttribute('colspan') || '1', 10);
509+
th.addEventListener('click', () => sortTable(table, thToTdIndex(table, th)));
484510
});
485511
});
486512

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

597623
// Equalize bar widths now that wrapper is visible
598624
equalizeBarWidths();
625+
599626
});

0 commit comments

Comments
 (0)