diff --git a/src/lib/editor/Editor.svelte b/src/lib/editor/Editor.svelte index dd7ac16..672e46e 100644 --- a/src/lib/editor/Editor.svelte +++ b/src/lib/editor/Editor.svelte @@ -27,11 +27,9 @@ applyOrientationVars(orientation); }); - // Nudge the pageBreaks plugin to recompute with the new content area. The - // dispatch bumps the `tick`/`numPages` bindings (via onTransaction/pm-pagecount), - // so it must run OUTSIDE the Svelte effect flush — otherwise the child↔parent - // binding updates re-enter and trip effect_update_depth_exceeded. requestAnimation- - // Frame defers it past the flush; the empty transaction changes no document content. + // Nudge the pageBreaks plugin to recompute with the new content area. The dispatch + // bumps the tick/numPages bindings, so requestAnimationFrame defers it past the + // Svelte effect flush (re-entering it would trip effect_update_depth_exceeded). let marginRecalcRaf = 0; $effect(() => { // track each margin + orientation so the effect re-runs on any change @@ -109,21 +107,17 @@ return; } // Position in the editor container's content space: viewport delta + scroll. - // getBoundingClientRect and scrollTop share the same (zoom-affected) scale, - // so this stays aligned across zoom; the toolbar itself is a non-zoomed - // sibling of .paper, so it renders at a constant size. + // getBoundingClientRect and scrollTop share the same zoom scale, so this stays + // aligned across zoom; the toolbar is a non-zoomed sibling, rendered constant-size. const tRect = dom.getBoundingClientRect(); const cRect = editorContainer.getBoundingClientRect(); const left = tRect.left - cRect.left + editorContainer.scrollLeft; // Default: anchor just above the table's top-left corner. let top = tRect.top - cRect.top + editorContainer.scrollTop; - // When a table spans page breaks, keep the toolbar on the page the cursor is - // actually on, so the user isn't forced to scroll back to the table's first - // page to reach it. Compare the cursor's page to the table's starting page - // (both in document px, via the same cycle math as the page indicator); if - // they differ, re-anchor to the active page's content-top — i.e. the table's - // resumed top on that page — so the toolbar floats in that page's top margin. + // When a table spans page breaks, keep the toolbar on the cursor's page rather + // than the table's first page. If the cursor's page differs from the table's start + // page, re-anchor to that page's content-top so the toolbar floats in its margin. const tiptap = element?.querySelector('.tiptap') as HTMLElement | null; if (tiptap) { try { @@ -148,18 +142,9 @@ tableUi = { visible: true, top, left }; } - // --- Table page-break overlay --- - // pageBreaks.ts reports (via pm-pagecount) where a single continuous table box crosses - // a page boundary (a too-tall cell's content flowing across pages, or a between-rows - // push), each as one band in document px relative to .tiptap's top. We render the - // overlay in a .band-layer inside .paper - // (in document px) so the same `transform: scale()` scales it identically to the - // .tiptap page background. Two pieces per break: - // • band — a solid page-coloured mask (content width) hiding the table's borders - // bleeding through the page margins, plus the close/open lines. - // • stripe — the dark page gap, drawn as ONE full-page-width element on top. Using - // a single rectangle for the whole gap avoids a left/right edge where two - // separately-rasterised gradients disagree by a sub-pixel (the seam). + // Table page-break overlay: pageBreaks.ts reports (via pm-pagecount) where a continuous + // table box crosses a page boundary, each as a band in doc px. Rendered in .band-layer + // inside the scaled .paper — a mask hides borders in the margins, a stripe is the gap. type BandStyle = { top: number; left: number; width: number; height: number }; type GapStripeStyle = { top: number; width: number; height: number; background: string }; let tableBandsDoc = $state([]); @@ -178,26 +163,14 @@ // lives inside the scaled .paper, so the transform applies the scaling — no `* z`. const border = 1; // 1px page-edge line, like the CSS .tiptap background bandStyles = tableBandsDoc.map((b) => { - // Use pure page geometry: the band spans exactly the inter-page region — from the - // closing page's content-bottom (closeY) down through margin/gap/margin to the next - // page's content-top. This is precisely where the table must show no vertical - // borders, and pagination guarantees no real content ever sits there, so a solid - // mask here can never eat content. (Anchoring the band to the break's spacers - // instead under-covered atomic-push / leaf-jump breaks, where the spacer renders - // below content-bottom — after the empty gap left by the pushed block — leaving the - // top of the page's bottom margin unmasked and the borders bleeding through it.) - // Match the band exactly to the table's content box (b.left / b.width): the black - // close (top) + open (bottom) lines are this element's top/bottom borders, so they - // must line up with the table's own start/end borders, which span the content width. - // The 1px overhang to also mask the outer L/R borders' half-pixel slivers is done by - // a horizontal-only box-shadow (.table-break-band CSS) so the mask widens without - // lengthening those close/open lines. + // The band spans the inter-page region (closeY through margin/gap/margin to the + // next content-top), where pagination guarantees no content, so the mask can't eat + // content. Matched to the table's content box (b.left/b.width) so the lines align. return { top: b.closeY, left: b.left, width: b.width, height: b.height }; }); - // Full-page-width gap stripe: the dark page gap + its two page-edge lines, at the - // true surface bottom (closeY + marginBottom = N·cycle − gap). Covers the entire - // natural gap (margins + table) with ONE element → no seam. Painted on top of the - // bands (later in DOM) so it overrides their white fill in the gap region. + // Full-page-width gap stripe: the dark page gap + its two edge lines at the surface + // bottom (closeY + marginBottom). One element covers the whole gap → no seam; painted + // after the bands so it overrides their fill in the gap region. gapStripeStyles = tableBandsDoc.map((b) => { const gapStart = b.closeY + b.marginBottom; return { @@ -431,13 +404,9 @@