perf: the zone's own box carries the margin-band hit area

A second element per zone doubled the header/footer layer — 150 absolutely placed
boxes in the 75-page window, laid out again on every pass. The zone's ::after grows
out to the page edges instead, which keeps the hover hint and the double-click on
the element that already handles them.

Claude-Session: https://claude.ai/code/session_01Gpw1ctsR8FrdNMrpqePtik
This commit is contained in:
Steffen Becker committed 2026-09-11 16:28:21 +02:00
1 parent 6d53e4f5c4
commit 01501398ad
1 file changed
+18 -21
+18 -21
View File
@@ -155,18 +155,14 @@
if (zone === 'footer') return { ...b, top: b.top + b.height - height, height };
return { ...b, height };
}
// Double-click target: the whole margin band the zone sits in, edge to edge, not just
// the zone box — the band above the body is the header's, as it is in LibreOffice.
function hitBox(zone: HfZone, page: number) {
// Double-click target: the zone's ::after grows the box out to the page edges, so the
// whole margin band opens the zone (as in LibreOffice) without a box of its own — a
// second element per zone is a hundred more to lay out on every pass.
function hitVars(zone: HfZone, page: number, z: { top: number; left: number; width: number; height: number }) {
const box = boxOf(page);
const z = zoneBox(zone, page);
const m = marginsOf(page);
if (zone === 'header') {
const bottom = Math.max(box.top + cmToPx(m.top), z.top + z.height);
return { top: box.top, left: box.left, width: box.width, height: bottom - box.top };
}
const top = Math.min(box.top + box.height - cmToPx(m.bottom), z.top);
return { top, left: box.left, width: box.width, height: box.top + box.height - top };
const edge = zone === 'header' ? z.top - box.top : box.top + box.height - z.top - z.height;
return ` --hit-${zone === 'header' ? 'top' : 'bottom'}: ${-edge}px;` +
` --hit-left: ${box.left - z.left}px; --hit-right: ${box.left + box.width - z.left - z.width}px;`;
}
const boxStyle = (b: { top: number; left: number; width: number; height: number }) =>
`top: ${b.top}px; left: ${b.left}px; width: ${b.width}px; height: ${b.height}px;`;
@@ -488,18 +484,16 @@
<div class="hf-layer">
{#each pages as p}
{#each ['header', 'footer'] as const as zone}
{#if interactive}
<div class="hf-hit" style={boxStyle(hitBox(zone, p))} ondblclick={() => startEdit(zone, p)} role="button" tabindex="-1"></div>
{/if}
{#if !(interactive && hfActive === zone && editingPage === p)}
{@const html = zoneHtml(zone, p)}
{@const total = html.includes('data-page-field="count"') ? numPages : 0}
{@const chapters = html.includes('data-page-field="chapter"') ? chapterStarts : null}
{@const zb = zoneBox(zone, p)}
<div
class="hf-zone hf-{zone}"
class:hf-empty={!html}
data-hf-label={zone === 'header' ? t().hf.addHeaderHint : t().hf.addFooterHint}
style={boxStyle(zoneBox(zone, p))}
style={boxStyle(zb) + (interactive ? hitVars(zone, p, zb) : '')}
ondblclick={() => interactive && startEdit(zone, p)}
role="button"
tabindex="-1"
@@ -602,11 +596,15 @@
justify-content: flex-end;
}
/* The margin band around a zone, double-clickable like the zone itself. Painted
before it, so the zone keeps the events inside its own box. */
.hf-hit {
/* The margin band around the zone, double-clickable like the zone itself. Not on the
edited zone, where it would swallow the clicks into the live editor. */
.hf-zone:not(.hf-active)::after {
content: '';
position: absolute;
pointer-events: auto;
top: var(--hit-top, 0);
bottom: var(--hit-bottom, 0);
left: var(--hit-left, 0);
right: var(--hit-right, 0);
}
/* Empty zone: invisible until hovered, then show a faint double-click hint. */
@@ -621,8 +619,7 @@
.hf-footer.hf-empty::before {
margin-top: auto;
}
.hf-empty:hover::before,
.hf-hit:hover + .hf-zone.hf-empty::before {
.hf-empty:hover::before {
opacity: 0.6;
}