diff --git a/src/lib/components/CLAUDE.md b/src/lib/components/CLAUDE.md index b79b55e..c0b603a 100644 --- a/src/lib/components/CLAUDE.md +++ b/src/lib/components/CLAUDE.md @@ -132,7 +132,7 @@ Both anchors differ: a wheel zoom holds the **point under the cursor** fixed (`p One header and one footer (`HfDoc` = a single-paragraph TipTap doc per zone, persisted to `edentext-header`/`-footer`), repeated on every page. Double-clicking a page margin — or the Layout-panel "Edit header/footer" buttons — opens the zone for editing; clicking back into the body (`onFocus`) or Escape ends it. The edge→zone distance (header from top, footer from bottom) is user-configurable in cm via the Layout panel (`HfDistances`, persisted to `edentext-hf-distances`, default `HF_DISTANCE_CM` 1.27cm, clamped 0–10 and below the body margin). A **section** can have its own (`HfSet.distances`, `distancesFirst` where its page style hands over): ODF's page-layout margin *is* that distance, Word's is `w:pgMar w:header/w:footer`, and an index whose header starts 4cm down is not the body's 1.25 — `zoneBox` and `sectionReach` both read the section's. - **Fields:** the bar beside the active zone inserts the `pageField.ts` atoms — page number, page count, chapter (the running head, level 1). Each is patched per page, so the same zone shows a different value on every page. -- **Rendering:** the zone's own font is the document's **default paragraph style** (`styleCss` gives `.paper .hf-layer .hf-zone` its text half): both formats base the Header/Footer style on it, so a file whose body is Arial 10pt has an Arial footer, not the editor's serif. `HeaderFooterLayer.svelte` mounts inside the scaled `.paper` (like `.band-layer`), positioning per-page zone boxes in unscaled doc px. A zone's out-of-flow frame (`wrap` ≠ inline) is hidden in the box and painted per page by `.hf-bg-layer` at `z-index: -1` — hence `.paper`, not `.tiptap`, paints the page surface, so the image lands between the sheet and the body text. Inactive zones render as static HTML (`generateHTML` + `hfExtensions`); the active zone hosts the single live TipTap editor. `App.svelte`'s `activeEditor`/`activeTick` route the top toolbars to `hfEditor` while a zone is active, so all body formatting works on the header/footer with no toolbar changes. +- **Rendering:** the zone's own font is the document's **default paragraph style** (`styleCss` gives `.paper .hf-layer .hf-zone` its text half): both formats base the Header/Footer style on it, so a file whose body is Arial 10pt has an Arial footer, not the editor's serif. `HeaderFooterLayer.svelte` mounts inside the scaled `.paper` (like `.band-layer`), positioning per-page zone boxes in unscaled doc px. A zone's out-of-flow frame (`wrap` ≠ inline) is hidden in the box and painted per page by `.hf-bg-layer` at `z-index: -1` — hence `.paper`, not `.tiptap`, paints the page surface, so the image lands between the sheet and the body text. Inactive zones render as static HTML (`generateHTML` + `hfExtensions`); the active zone hosts the single live TipTap editor. `App.svelte`'s `activeEditor`/`activeTick` route the top toolbars to `hfEditor` while a zone is active, so all body formatting works on the header/footer with no toolbar changes. Only a window of pages around `currentPage` carries its zones (`WINDOW_PAGES`/`WINDOW_STEP`): a 460-page document is a thousand absolutely-placed boxes otherwise, which the browser lays out again on every pagination pass — worth ~0.4 s of the load. The window shifts a step at a time, so scrolling does not rebuild it on every tick, and `pageWindow.svelte.ts`'s `allPagesDrawn` opens it to the whole document for anything that captures the live DOM: `renderPaperToCanvas` (`export/pdf.ts`) before it clones, and `beforeprint`/`afterprint` for a browser printout. ## Comments (`CommentsPane.svelte`) diff --git a/src/lib/components/HeaderFooterLayer.svelte b/src/lib/components/HeaderFooterLayer.svelte index 2216d7a..aa0de6b 100644 --- a/src/lib/components/HeaderFooterLayer.svelte +++ b/src/lib/components/HeaderFooterLayer.svelte @@ -11,6 +11,7 @@ import { formatOrdinal } from '../utils/orderedListTypes'; import { chapterOn, type ChapterStart } from '../utils/chapterField'; import { t } from '../i18n/i18n.svelte'; + import { allPagesDrawn } from './pageWindow.svelte'; let { headerDoc = $bindable(), @@ -110,7 +111,18 @@ : { header: headerDistPx, footer: footerDistPx }; } - let pages = $derived(Array.from({ length: Math.max(1, numPages) }, (_, i) => i + 1)); + // Only the pages around the one being read carry their zones: a long document's layer + // is a thousand absolutely-placed boxes otherwise, laid out again on every pass. The + // window shifts a step at a time, so a scroll does not rebuild it on every tick. + const WINDOW_STEP = 25; + const WINDOW_PAGES = 75; + let pages = $derived.by(() => { + const total = Math.max(1, numPages); + if (allPagesDrawn.on || total <= WINDOW_PAGES) return Array.from({ length: total }, (_, i) => i + 1); + const step = Math.floor((currentPage - 1) / WINDOW_STEP) * WINDOW_STEP - WINDOW_STEP + 1; + const start = Math.max(1, Math.min(total - WINDOW_PAGES + 1, step)); + return Array.from({ length: WINDOW_PAGES }, (_, i) => start + i); + }); function zoneBox(zone: HfZone, page: number) { // Mirrored margins: an even page is the left-hand one, so the pair is swapped. diff --git a/src/lib/components/pageWindow.svelte.ts b/src/lib/components/pageWindow.svelte.ts new file mode 100644 index 0000000..67ea9d2 --- /dev/null +++ b/src/lib/components/pageWindow.svelte.ts @@ -0,0 +1,15 @@ +import { flushSync } from 'svelte'; + +// The header/footer layer draws only the pages around the one being read. A capture or a +// printout takes the live DOM, so both open the window to the whole document first. +export const allPagesDrawn = $state({ on: false }); + +if (typeof window !== 'undefined') { + window.addEventListener('beforeprint', () => { + allPagesDrawn.on = true; + flushSync(); + }); + window.addEventListener('afterprint', () => { + allPagesDrawn.on = false; + }); +} diff --git a/src/lib/export/pdf.ts b/src/lib/export/pdf.ts index a627d3e..d924bf4 100644 --- a/src/lib/export/pdf.ts +++ b/src/lib/export/pdf.ts @@ -2,7 +2,9 @@ // print-time re-render drifts text (vector ≠ hinted metrics) and broke the band masks, // so we raster the on-screen render and overlay an invisible, positioned text layer. +import { tick } from 'svelte'; import { generateHTML } from '@tiptap/core'; +import { allPagesDrawn } from '../components/pageWindow.svelte'; import { type Orientation } from '../storage/pageOrientation'; import { DEFAULT_MARGINS, PX_PER_CM, type PageMargins } from '../storage/pageMargins'; import { pageDimsCm, type PageFormat } from '../storage/pageFormat'; @@ -228,7 +230,11 @@ export async function renderPaperToCanvas(opts: PdfOptions, scale = 2): Promise< const { pageW, pageH } = pageDims(opts.pageFormat ?? 'A4', orientation); const cycle = pageH + PAGE_GAP; + // The header/footer layer draws a window of pages; the capture takes them all. + allPagesDrawn.on = true; + await tick(); const { holder, clone, style } = buildClone(paper, pageW, opts.printMarkup !== false); + allPagesDrawn.on = false; document.head.appendChild(style); document.body.appendChild(holder); const cleanup = () => { holder.remove(); style.remove(); };