Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
124 changes: 120 additions & 4 deletions app/Views/frontend/catalog-grid.php
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,13 @@
line-height: 1.4;
margin-bottom: 0.5rem;
color: var(--text-primary);
min-height: 2.8em;
/* #298: cap at two lines; the exact height is equalised per grid row by the
script below, so a row of short titles stays compact while a row that
needs two lines lines up. */
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}

.book-title a {
Expand All @@ -182,6 +188,9 @@
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
/* #298: the subtitle is rendered only when the book has one, so it takes
space conditionally — no reserved gap on the (majority) of cards without
a subtitle. */
}

.book-title a:hover {
Expand All @@ -192,15 +201,28 @@
font-size: 0.875rem;
color: var(--text-secondary);
margin-bottom: 0.5rem;
min-height: 1.2em;
/* #298: fixed single-line box so a long author name can't push the row
below out of alignment with adjacent cards. line-height matches the
height so the single line isn't clipped by overflow:hidden. */
height: 1.3em;
line-height: 1.3;
display: -webkit-box;
-webkit-line-clamp: 1;
-webkit-box-orient: vertical;
overflow: hidden;
}

.book-meta {
font-size: 0.75rem;
color: var(--text-muted);
line-height: 1.5;
margin-bottom: auto;
min-height: 1.5em;
/* #298: fixed single-line box (see .book-author). */
height: 1.5em;
display: -webkit-box;
-webkit-line-clamp: 1;
-webkit-box-orient: vertical;
overflow: hidden;
}

.book-actions {
Expand Down Expand Up @@ -275,7 +297,6 @@

.book-title {
font-size: 1rem;
min-height: 2.4em;
}
}

Expand All @@ -302,3 +323,98 @@
font-size: 0.75rem;
}
</style>
<script>
/* #298: equalise title AND subtitle height PER GRID ROW. A row where no card
has a subtitle stays compact; a row where at least one card has a subtitle
gets the same reserved subtitle height on every card of that row — including
a hidden placeholder on the cards that have none — so title, author, publisher
and "Details" line up. Pure layout — no theme colours. */
(function () {
function rowsOf(cards) {
var rows = [];
cards.forEach(function (c) {
var top = Math.round(c.getBoundingClientRect().top);
var row = null;
for (var i = 0; i < rows.length; i++) { if (Math.abs(rows[i].top - top) < 8) { row = rows[i]; break; } }
if (!row) { row = { top: top, cards: [] }; rows.push(row); }
row.cards.push(c);
});
return rows;
}
function maxHeight(els) {
var max = 0;
els.forEach(function (e) { if (e.offsetHeight > max) max = e.offsetHeight; });
return max;
}
function alignGrid(grid) {
var cards = Array.prototype.slice.call(grid.querySelectorAll('.book-card'));
if (!cards.length) return;
// 1) RESET (writes only): undo previous adjustments so the READ phase below
// measures natural heights.
cards.forEach(function (c) {
var t = c.querySelector('.book-title'); if (t) t.style.height = '';
var s = c.querySelector('.book-subtitle:not(.subtitle-ph)'); if (s) s.style.height = '';
var ph = c.querySelector('.subtitle-ph'); if (ph) ph.parentNode.removeChild(ph);
});
// 2) READ (measurements only): batch every getBoundingClientRect/offsetHeight
// read here so the WRITE phase can't interleave reads and force repeated
// synchronous reflows (#302 review, layout-thrashing fix).
var plan = rowsOf(cards).map(function (row) {
var titles = row.cards.map(function (c) { return c.querySelector('.book-title'); }).filter(Boolean);
var realSubs = row.cards.map(function (c) { return c.querySelector('.book-subtitle'); }).filter(Boolean);
return { row: row, titles: titles, realSubs: realSubs, maxT: maxHeight(titles), maxS: maxHeight(realSubs) };
});
// 3) WRITE (mutations only): apply heights and inject placeholders. No reads
// here, so the browser reflows at most once after this batch.
plan.forEach(function (p) {
// maxT === 0 means the grid is not laid out (e.g. an ancestor is
// display:none) — skip rather than collapse every title to 0px.
if (!p.maxT) return;
p.titles.forEach(function (e) { e.style.height = p.maxT + 'px'; });
if (!p.realSubs.length) return; // row has no subtitle → stays compact
p.realSubs.forEach(function (e) { e.style.height = p.maxS + 'px'; });
p.row.cards.forEach(function (c) {
if (c.querySelector('.book-subtitle')) return; // already has one
var t = c.querySelector('.book-title'); if (!t) return;
// a hidden .book-subtitle placeholder inherits the same margins, so the
// author below lines up exactly with the subtitled cards.
var ph = document.createElement('p');
ph.className = 'book-subtitle subtitle-ph';
ph.setAttribute('aria-hidden', 'true');
ph.style.visibility = 'hidden';
ph.style.height = p.maxS + 'px';
ph.innerHTML = '&nbsp;';
t.insertAdjacentElement('afterend', ph);
});
});
}
var frame = null;
function align() {
// Coalesce bursts of triggers (DOMContentLoaded+load, rapid resize/AJAX)
// into a single alignment per animation frame (#302 review, thrashing fix).
if (frame) return;
frame = (window.requestAnimationFrame || window.setTimeout)(function () {
frame = null;
// Scope per grid: two .books-grid on the same page must not have their rows
// merged just because cards happen to share a vertical position.
Array.prototype.slice.call(document.querySelectorAll('.books-grid')).forEach(alignGrid);
}, 16);
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
var timer;
function schedule() { clearTimeout(timer); timer = setTimeout(align, 120); }
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', align);
else align();
window.addEventListener('load', align);
window.addEventListener('resize', schedule);
// Re-align once web fonts settle: a late font swap can reflow a title from one
// line to two after the initial pass, leaving the row misaligned until a resize
// (#302 review). Harmless where fonts are already loaded.
if (document.fonts && document.fonts.ready && typeof document.fonts.ready.then === 'function') {
document.fonts.ready.then(align);
}
// The catalogue replaces this partial through AJAX after filters, sorting and
// pagination. Scripts inserted through innerHTML do not execute, so the page
// explicitly emits this event once the replacement cards are in the DOM.
document.addEventListener('pinakes:catalog-grid-updated', align);
})();
</script>
1 change: 1 addition & 0 deletions app/Views/frontend/catalog.php
Original file line number Diff line number Diff line change
Expand Up @@ -1919,6 +1919,7 @@ function loadBooks() {
} else {
container.style.display = 'grid';
container.innerHTML = data.html;
container.dispatchEvent(new Event('pinakes:catalog-grid-updated', { bubbles: true }));
container.classList.add('fade-in');
}

Expand Down
Loading
Loading