diff --git a/CHANGELOG.md b/CHANGELOG.md index 51aa9b6..8db0d83 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -99,6 +99,7 @@ - Page margins per section: a section's own `w:pgMar` / ODF page layout is read, rendered and written back, and an ODF page style that hands over to another (`style:next-style-name`, the title-page idiom) gives its layout to the section's first page and the successor's to the rest. The ruler and the header/footer layer still show the document's own pair - Headers & footers: page-number / page-count fields, configurable edge distances, and variants — different first page and different odd & even pages (edit a page's zone directly; blank first/even zones supported). Press Enter to add blank lines that grow a zone into the page while body text reflows so the page break sits above the footer / below the header, never overlapping. Inline images/logos can be inserted in a zone. Round-trips to ODF (`style:header-first`/`-left`, `draw:frame`) and DOCX (`w:titlePg`/`w:evenAndOddHeaders`, `ImageRun`) - Zoom (20–300 %) +- A section's own page setup: paper size and orientation per section (Layout ▸ Size / Orientation ▸ This section), beside the document's own. Pages of different sizes are each **centred** in the scroll, as both word processors draw them — sheet, text, header/footer, page border and line numbers alike - Horizontal ruler above the page: click to place a tab stop of the selected type, drag to move it, drag it off to remove it; three markers set the paragraph's first-line, left and right indent. Toggle in the extended toolbar - Show formatting marks (spaces, tabs, paragraph marks) @@ -153,7 +154,6 @@ The gap against Word/LibreOffice, most valuable first. Reviewed 2026-08-14. **Missing while writing** - Hyphenation's zone and ladder count (`fo:hyphenation-ladder-count`, `w:hyphenationZone`) — CSS exposes neither - A bibliography's citation style: the list is set the one way ("key: author, title, year"), where both word processors offer APA, MLA, Chicago and the rest, and numbered citations (`[1]`) instead of the source's short name -- A section's own **page format** is set only by the file it came from — the ribbon offers the orientation per section, the paper size document-wide. Pages of different widths are left-aligned in the scroll, where both word processors centre each - A sort dialog's second and third key, and its choice of sort type over the detected one; a formula's own number format (LibreOffice's Number Format dialog per cell — the general format is the only one here), and a formula reaching into another table - A list level's own hanging indent: the marker sits at the 0.635 cm both exports write (`LIST_HANGING_CM`), so a wider *left*-set marker overflows where Word moves the text to the next list tab (a **right**-set one — `w:lvlJc`, which is what the built-in Roman numberings use — grows into the margin and is fine). Reading the value back naively also moves the markers of our own ODT exports — LibreOffice draws its own flat hanging at exactly the value odf-kit writes for level 2; see `tests/render-parity/README.md` before building on it - Linked / chained text frames diff --git a/docs/architecture/pagination.md b/docs/architecture/pagination.md index ba2ec1b..701b59c 100644 --- a/docs/architecture/pagination.md +++ b/docs/architecture/pagination.md @@ -87,9 +87,10 @@ becomes known, so a pass never measures its own last answer; it reports the runs `Editor.svelte` publishes them as `--pb-page-runs` for every other consumer (the TOC, a cross-reference, a page-anchored frame) plus `--pb-section-page` for the next pass. `.paper` reserves the **widest** section's paper (`--pb-paper-width`) or a landscape page -would be cut at the sheet edge, and a narrower section takes the difference as an extra -right inset on top of its own margins. The sheets are left-aligned in that box, where -both word processors centre each page. +would be cut at the sheet edge, and a narrower section splits the difference between its +two insets, on top of its own margins — so each page is **centred** in that box, as both +word processors draw one. Every per-page layer positions from the box's own `left` +(`PageSheetLayer`, `PageDecorLayer`, `LineNumberLayer`, `HeaderFooterLayer`). **Tables across page breaks:** when a single continuous table box crosses a page boundary, the plugin reports `TableBreakBand`s (doc-px geometry). `Editor.svelte` renders an overlay (`.band-layer` inside `.paper`) that masks the table borders bleeding through the page margins and paints the dark page gap as one seam-free stripe. diff --git a/src/lib/components/Editor.svelte b/src/lib/components/Editor.svelte index 7069807..c0eba03 100644 --- a/src/lib/components/Editor.svelte +++ b/src/lib/components/Editor.svelte @@ -204,13 +204,15 @@ import { EMPTY_PAGE_DECOR, type PageDecor } from '../storage/pageDecor'; // Which section each page belongs to (sectionStartPages holds the first page of every // section past the first), and from it the box every per-page layer paints. let pageBoxes = $derived.by(() => { - const out: { top: number; height: number; width: number; section: number }[] = []; + const out: { top: number; left: number; height: number; width: number; section: number }[] = []; let top = 0; let section = 0; for (let p = 1; p <= Math.max(1, numPages); p++) { while (section < sectionStartPages.length && sectionStartPages[section] <= p) section++; const paper = sectionPaper[Math.min(section, sectionPaper.length - 1)]; - out.push({ top, height: paper.h, width: paper.w, section }); + // A page narrower than the reserved sheet is centred in it, as both word + // processors centre each page of a document whose sections differ. + out.push({ top, left: Math.round((paperWidth - paper.w) / 2), height: paper.h, width: paper.w, section }); top += paper.h + PAGE_GAP; } return out; @@ -219,19 +221,22 @@ import { EMPTY_PAGE_DECOR, type PageDecor } from '../storage/pageDecor'; // The same per section for the side margins, as a delta against the document's own: // .tiptap's padding draws those for every page, so a section that wants others has // its blocks inset by the difference ("leftFirst|rightFirst|leftRest|rightRest"). - // A section narrower than the sheet (.paper reserves the widest paper) takes the whole - // difference on its right, so its text still ends at its own page's right margin. - let sectionInset = $derived([ - [0, paperWidth - sectionPaper[0].w, 0, paperWidth - sectionPaper[0].w], - ...extraHfSections.map((s, i) => { - const rest = s.margins ?? null; - const first = s.marginsFirst ?? rest; - const d = (m: PageMargins | null, side: 'left' | 'right') => - m ? Math.round(cmToPx(m[side]) - cmToPx(pageMargins[side])) : 0; - const narrow = paperWidth - (sectionPaper[i + 1]?.w ?? paperWidth); - return [d(first, 'left'), d(first, 'right') + narrow, d(rest, 'left'), d(rest, 'right') + narrow]; - }), - ].map((g) => g.join('|')).join(',')); + // A section narrower than the sheet (.paper reserves the widest paper) splits the + // difference between its sides, so its text sits on the centred page it belongs to. + let sectionInset = $derived.by(() => { + const half = (i: number) => Math.round((paperWidth - (sectionPaper[i]?.w ?? paperWidth)) / 2); + return [ + [half(0), half(0), half(0), half(0)], + ...extraHfSections.map((s, i) => { + const rest = s.margins ?? null; + const first = s.marginsFirst ?? rest; + const d = (m: PageMargins | null, side: 'left' | 'right') => + m ? Math.round(cmToPx(m[side]) - cmToPx(pageMargins[side])) : 0; + const narrow = half(i + 1); + return [d(first, 'left') + narrow, d(first, 'right') + narrow, d(rest, 'left') + narrow, d(rest, 'right') + narrow]; + }), + ].map((g) => g.join('|')).join(','); + }); $effect(() => { const s = document.documentElement.style; diff --git a/src/lib/components/HeaderFooterLayer.svelte b/src/lib/components/HeaderFooterLayer.svelte index 47b5c96..7cce47a 100644 --- a/src/lib/components/HeaderFooterLayer.svelte +++ b/src/lib/components/HeaderFooterLayer.svelte @@ -44,7 +44,7 @@ differentOddEven?: boolean; numPages: number; /** One box per page (Editor.svelte): a section on its own paper differs in size. */ - pageBoxes: { top: number; height: number; width: number }[]; + pageBoxes: { top: number; left: number; height: number; width: number }[]; currentPage: number; /** How the page-number field counts (format + start value). */ pageNumbering?: PageNumbering; @@ -74,7 +74,7 @@ // A section on its own paper makes the pages differ, so every box comes from the // grid Editor.svelte publishes; the document's own is the fallback. const boxOf = (page: number) => pageBoxes[page - 1] - ?? { top: (page - 1) * (pageHeightPx + PAGE_GAP), height: pageHeightPx, width: pageWidthPx }; + ?? { top: (page - 1) * (pageHeightPx + PAGE_GAP), left: 0, height: pageHeightPx, width: pageWidthPx }; let mTop = $derived(cmToPx(pageMargins.top)); let mBottom = $derived(cmToPx(pageMargins.bottom)); let mLeft = $derived(cmToPx(pageMargins.left)); @@ -89,9 +89,10 @@ function zoneBox(zone: HfZone, page: number) { // Mirrored margins: an even page is the left-hand one, so the pair is swapped. - const left = pageMargins.mirrored && page % 2 === 0 ? mRight : mLeft; - const width = contentWidthOf(page); const box = boxOf(page); + // From the page's own left edge — a section on narrower paper is centred. + const left = box.left + (pageMargins.mirrored && page % 2 === 0 ? mRight : mLeft); + const width = contentWidthOf(page); if (zone === 'header') { const top = box.top + headerDistPx; return { top, left, width, height: Math.max(MIN_ZONE_PX, mTop - headerDistPx) }; @@ -387,7 +388,7 @@ class="hf-page-bg" src={bg.src} alt="" - style="top: {boxOf(p).top + bg.y}px; left: {bg.x}px; width: {bg.width}px; height: {bg.height}px;" + style="top: {boxOf(p).top + bg.y}px; left: {boxOf(p).left + bg.x}px; width: {bg.width}px; height: {bg.height}px;" /> {/each} {/each} diff --git a/src/lib/components/LineNumberLayer.svelte b/src/lib/components/LineNumberLayer.svelte index f8bee28..40c1547 100644 --- a/src/lib/components/LineNumberLayer.svelte +++ b/src/lib/components/LineNumberLayer.svelte @@ -15,7 +15,7 @@ tick: number; lineNumbering: LineNumbering; /** One box per page (Editor.svelte): a section on its own paper differs in size. */ - pageBoxes: { top: number; height: number; width: number }[]; + pageBoxes: { top: number; left: number; height: number; width: number }[]; pageMargins: PageMargins; } = $props(); @@ -24,7 +24,9 @@ for (let i = pageBoxes.length - 1; i >= 0; i--) if (top >= pageBoxes[i].top) return i + 1; return 1; }; - let left = $derived(cmToPx(pageMargins.left) - cmToPx(lineNumbering.distanceCm)); + // Measured from the page's own left edge: a narrower section's page is centred. + const leftOf = (top: number) => (pageBoxes[pageAt(top) - 1]?.left ?? 0) + + cmToPx(pageMargins.left) - cmToPx(lineNumbering.distanceCm); let marks = $state<{ top: number; label: string }[]>([]); let host = $state(null); @@ -101,7 +103,7 @@ diff --git a/src/lib/components/PageDecorLayer.svelte b/src/lib/components/PageDecorLayer.svelte index 7e0cfe5..dfb0ca9 100644 --- a/src/lib/components/PageDecorLayer.svelte +++ b/src/lib/components/PageDecorLayer.svelte @@ -8,7 +8,7 @@ let { decor, pageBoxes, pageMargins }: { decor: PageDecor; /** One box per page (Editor.svelte): a section on its own paper differs in size. */ - pageBoxes: { top: number; height: number; width: number }[]; + pageBoxes: { top: number; left: number; height: number; width: number }[]; pageMargins: PageMargins; } = $props(); @@ -38,14 +38,14 @@ {@const ring = inset(p)}
{/if} {#if decor.watermark} -
+
diff --git a/src/lib/components/PageSheetLayer.svelte b/src/lib/components/PageSheetLayer.svelte index a045a48..115e0c8 100644 --- a/src/lib/components/PageSheetLayer.svelte +++ b/src/lib/components/PageSheetLayer.svelte @@ -3,13 +3,13 @@ // page was the same size; a section on its own paper makes the pages differ in height // and width, and only a box per page can say that. let { pageBoxes }: { - pageBoxes: { top: number; height: number; width: number }[]; + pageBoxes: { top: number; left: number; height: number; width: number }[]; } = $props(); @@ -25,7 +25,6 @@ .page-sheet { position: absolute; - left: 0; background: var(--color-page-bg); border: 1px solid var(--color-page-border); /* The border rings the sheet without growing it, so the page box stays the paper. */ diff --git a/src/lib/components/ribbon/tabs/LayoutTab.svelte b/src/lib/components/ribbon/tabs/LayoutTab.svelte index 2183ebd..2a82961 100644 --- a/src/lib/components/ribbon/tabs/LayoutTab.svelte +++ b/src/lib/components/ribbon/tabs/LayoutTab.svelte @@ -57,6 +57,10 @@ }); return n; }); + // Its own paper, where it has one — else the document's. + let sectionFormat = $derived( + currentSection > 0 ? (extraHfSections[currentSection - 1]?.format ?? pageFormat) : pageFormat, + ); // Its own orientation, where it has one — else the document's. let sectionOrientation = $derived( currentSection > 0 ? (extraHfSections[currentSection - 1]?.orientation ?? pageOrientation) : pageOrientation, @@ -187,6 +191,15 @@ {fmtCm(PAGE_FORMAT_CM[f].w)} × {fmtCm(PAGE_FORMAT_CM[f].h)} cm {/each} + {#if currentSection > 0} +
{t().ribbon.thisSection}
+ {#each FORMATS as f} + + {/each} + + {/if}
{/if}