feat: page orientation
This commit is contained in:
1 parent
2088423b59
commit
61400d1eed
8 files changed
+176
-46
No files matched your search
@@ -33,6 +33,7 @@
|
||||
- text alignment
|
||||
- show formatting marks
|
||||
- margins
|
||||
- page orientation (odt-kit bug still open => export not working)
|
||||
|
||||
### Not yet implemented
|
||||
- ODT import / open existing file
|
||||
|
||||
+9
-3
@@ -8,6 +8,7 @@
|
||||
import { getColorDebug } from './lib/editor/colorDebug';
|
||||
import { loadTheme, saveTheme, applyTheme, loadToolbarExpanded, saveToolbarExpanded, loadFormattingMarks, saveFormattingMarks, type ThemeMode } from './lib/storage/theme';
|
||||
import { loadPageMargins, savePageMargins, type PageMargins } from './lib/storage/pageMargins';
|
||||
import { loadOrientation, saveOrientation, type Orientation } from './lib/storage/pageOrientation';
|
||||
|
||||
let editor: Editor | null = $state(null);
|
||||
let tick: number = $state(0);
|
||||
@@ -20,6 +21,7 @@
|
||||
let showFormattingMarks = $state(loadFormattingMarks());
|
||||
let zoom = $state(Math.max(20, Math.min(300, parseInt(localStorage.getItem('odf-editor-zoom') ?? '100', 10))));
|
||||
let pageMargins: PageMargins = $state(loadPageMargins());
|
||||
let pageOrientation: Orientation = $state(loadOrientation());
|
||||
|
||||
$effect(() => {
|
||||
saveFormattingMarks(showFormattingMarks);
|
||||
@@ -29,6 +31,10 @@
|
||||
savePageMargins(pageMargins);
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
saveOrientation(pageOrientation);
|
||||
});
|
||||
|
||||
function setZoom(value: number) {
|
||||
zoom = Math.max(20, Math.min(300, value));
|
||||
localStorage.setItem('odf-editor-zoom', String(zoom));
|
||||
@@ -55,7 +61,7 @@
|
||||
}
|
||||
|
||||
async function handleExport() {
|
||||
if (editor) await exportToOdt(editor, pageMargins);
|
||||
if (editor) await exportToOdt(editor, pageMargins, pageOrientation);
|
||||
}
|
||||
|
||||
function handleDebugDump() {
|
||||
@@ -167,9 +173,9 @@
|
||||
</div>
|
||||
</header>
|
||||
{#if toolbarExpanded}
|
||||
<ToolbarExpanded {editor} {tick} bind:showFormattingMarks bind:pageMargins />
|
||||
<ToolbarExpanded {editor} {tick} bind:showFormattingMarks bind:pageMargins bind:pageOrientation />
|
||||
{/if}
|
||||
<EditorComponent bind:editor bind:tick bind:currentPage bind:numPages {zoom} {showFormattingMarks} {pageMargins} />
|
||||
<EditorComponent bind:editor bind:tick bind:currentPage bind:numPages {zoom} {showFormattingMarks} {pageMargins} orientation={pageOrientation} />
|
||||
<footer class="statusbar">
|
||||
<span>Page {currentPage} of {numPages}</span>
|
||||
<div class="zoom-controls">
|
||||
|
||||
@@ -6,20 +6,24 @@
|
||||
import { extensions } from './extensions';
|
||||
import { saveDocument, loadDocument } from '../storage/autosave';
|
||||
import { applyMarginVars, DEFAULT_MARGINS, type PageMargins } from '../storage/pageMargins';
|
||||
import { applyOrientationVars, type Orientation } from '../storage/pageOrientation';
|
||||
import { FORCE_PAGE_RECALC } from './pageBreaks';
|
||||
import '../../styles/editor.css';
|
||||
|
||||
const DEFAULT_EDITOR_FONT = 'Georgia'; // must match ToolbarExpanded.svelte
|
||||
|
||||
let { editor = $bindable(), tick = $bindable(0), currentPage = $bindable(1), numPages = $bindable(1), zoom = 100, showFormattingMarks = false, pageMargins = DEFAULT_MARGINS }: {
|
||||
editor: Editor | null; tick: number; currentPage: number; numPages: number; zoom: number; showFormattingMarks?: boolean; pageMargins?: PageMargins;
|
||||
let { editor = $bindable(), tick = $bindable(0), currentPage = $bindable(1), numPages = $bindable(1), zoom = 100, showFormattingMarks = false, pageMargins = DEFAULT_MARGINS, orientation = 'portrait' }: {
|
||||
editor: Editor | null; tick: number; currentPage: number; numPages: number; zoom: number; showFormattingMarks?: boolean; pageMargins?: PageMargins; orientation?: Orientation;
|
||||
} = $props();
|
||||
|
||||
// Apply the page margins to the :root CSS vars (visual padding + pagination
|
||||
// both read these). This only touches the DOM, so it's safe inside an effect.
|
||||
// Apply the page margins + orientation to the :root CSS vars (visual padding,
|
||||
// page dimensions, and pagination all read these). DOM-only, safe in effects.
|
||||
$effect(() => {
|
||||
applyMarginVars(pageMargins);
|
||||
});
|
||||
$effect(() => {
|
||||
applyOrientationVars(orientation);
|
||||
});
|
||||
|
||||
// Nudge the pageBreaks plugin to recompute with the new content area. The
|
||||
// dispatch bumps the `tick`/`numPages` bindings (via onTransaction/pm-pagecount),
|
||||
@@ -28,8 +32,9 @@
|
||||
// Frame defers it past the flush; the empty transaction changes no document content.
|
||||
let marginRecalcRaf = 0;
|
||||
$effect(() => {
|
||||
// track each margin so the effect re-runs on any change
|
||||
// track each margin + orientation so the effect re-runs on any change
|
||||
void (pageMargins.top + pageMargins.bottom + pageMargins.left + pageMargins.right);
|
||||
void orientation;
|
||||
const ed = editor;
|
||||
if (!ed) return;
|
||||
cancelAnimationFrame(marginRecalcRaf);
|
||||
@@ -43,7 +48,13 @@
|
||||
let element: HTMLDivElement;
|
||||
let editorContainer: HTMLDivElement;
|
||||
|
||||
const CYCLE = 1143; // PAGE_HEIGHT + PAGE_GAP, must match pageBreaks.ts
|
||||
// Page cycle (page height + 20px gap) in document px. Orientation-dependent, so
|
||||
// read live from --user-page-height (set by applyOrientationVars). Must match
|
||||
// pageBreaks.ts. Fallback = A4 portrait (1123 + 20).
|
||||
function getCycle(): number {
|
||||
const ph = parseFloat(getComputedStyle(document.documentElement).getPropertyValue('--user-page-height'));
|
||||
return (Number.isFinite(ph) ? ph : 1123) + 20;
|
||||
}
|
||||
|
||||
// Throttle the value actually written to the DOM to one update per animation frame,
|
||||
// so rapid slider events don't trigger 50+ layout/paint cycles per second.
|
||||
@@ -93,8 +104,9 @@
|
||||
const editorRect = editorContainer.getBoundingClientRect();
|
||||
const tiptapRect = tiptap.getBoundingClientRect();
|
||||
// getBoundingClientRect and coordsAtPos return zoomed viewport pixels;
|
||||
// divide by zoom factor to convert to document coordinates before comparing with CYCLE.
|
||||
// divide by zoom factor to convert to document coordinates before comparing with the cycle.
|
||||
const zoomFactor = appliedZoom / 100;
|
||||
const cycle = getCycle();
|
||||
|
||||
if (editor) {
|
||||
try {
|
||||
@@ -102,7 +114,7 @@
|
||||
const cursorMidY = (coords.top + coords.bottom) / 2;
|
||||
if (cursorMidY >= editorRect.top && cursorMidY <= editorRect.bottom) {
|
||||
const cursorInDoc = (cursorMidY - tiptapRect.top) / zoomFactor;
|
||||
currentPage = Math.max(1, Math.min(numPages, Math.floor(Math.max(0, cursorInDoc) / CYCLE) + 1));
|
||||
currentPage = Math.max(1, Math.min(numPages, Math.floor(Math.max(0, cursorInDoc) / cycle) + 1));
|
||||
return;
|
||||
}
|
||||
} catch {
|
||||
@@ -111,7 +123,7 @@
|
||||
}
|
||||
|
||||
const visibleTopInDoc = (editorRect.top - tiptapRect.top) / zoomFactor;
|
||||
currentPage = Math.max(1, Math.min(numPages, Math.floor(Math.max(0, visibleTopInDoc) / CYCLE) + 1));
|
||||
currentPage = Math.max(1, Math.min(numPages, Math.floor(Math.max(0, visibleTopInDoc) / cycle) + 1));
|
||||
}
|
||||
|
||||
function onPageCount(e: Event) {
|
||||
@@ -167,7 +179,7 @@
|
||||
try {
|
||||
const coords = e.view.coordsAtPos(e.state.selection.head);
|
||||
const cursorInDoc = ((coords.top + coords.bottom) / 2 - tiptap.getBoundingClientRect().top) / (appliedZoom / 100);
|
||||
currentPage = Math.max(1, Math.min(numPages, Math.floor(Math.max(0, cursorInDoc) / CYCLE) + 1));
|
||||
currentPage = Math.max(1, Math.min(numPages, Math.floor(Math.max(0, cursorInDoc) / getCycle()) + 1));
|
||||
} catch { /* ignore */ }
|
||||
},
|
||||
onUpdate: ({ editor: e }) => {
|
||||
|
||||
@@ -8,9 +8,10 @@
|
||||
supportsLocalFontAccess,
|
||||
} from './fontDetect';
|
||||
import { DEFAULT_MARGINS, type PageMargins } from '../storage/pageMargins';
|
||||
import type { Orientation } from '../storage/pageOrientation';
|
||||
|
||||
let { editor, tick, showFormattingMarks = $bindable(), pageMargins = $bindable(DEFAULT_MARGINS) }:
|
||||
{ editor: Editor | null; tick: number; showFormattingMarks: boolean; pageMargins?: PageMargins } = $props();
|
||||
let { editor, tick, showFormattingMarks = $bindable(), pageMargins = $bindable(DEFAULT_MARGINS), pageOrientation = $bindable<Orientation>('portrait') }:
|
||||
{ editor: Editor | null; tick: number; showFormattingMarks: boolean; pageMargins?: PageMargins; pageOrientation?: Orientation } = $props();
|
||||
|
||||
type AlignValue = 'left' | 'center' | 'right' | 'justify';
|
||||
|
||||
@@ -1286,6 +1287,33 @@
|
||||
</button>
|
||||
{#if layoutOpen}
|
||||
<div class="layout-dropdown">
|
||||
<div class="lh-section-label">Orientation</div>
|
||||
<div class="orientation-row">
|
||||
<button
|
||||
class="orientation-btn"
|
||||
class:active={pageOrientation === 'portrait'}
|
||||
aria-pressed={pageOrientation === 'portrait'}
|
||||
onclick={() => (pageOrientation = 'portrait')}
|
||||
title="Portrait"
|
||||
>
|
||||
<svg width="14" height="16" viewBox="0 0 14 16" fill="none" aria-hidden="true">
|
||||
<rect x="2.5" y="1.5" width="9" height="13" rx="1" stroke="currentColor" stroke-width="1.3"/>
|
||||
</svg>
|
||||
<span>Portrait</span>
|
||||
</button>
|
||||
<button
|
||||
class="orientation-btn"
|
||||
class:active={pageOrientation === 'landscape'}
|
||||
aria-pressed={pageOrientation === 'landscape'}
|
||||
onclick={() => (pageOrientation = 'landscape')}
|
||||
title="Landscape"
|
||||
>
|
||||
<svg width="16" height="14" viewBox="0 0 16 14" fill="none" aria-hidden="true">
|
||||
<rect x="1.5" y="2.5" width="13" height="9" rx="1" stroke="currentColor" stroke-width="1.3"/>
|
||||
</svg>
|
||||
<span>Landscape</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="lh-section-label">Page margins (cm)</div>
|
||||
<div class="margin-grid">
|
||||
{#each MARGIN_FIELDS as f}
|
||||
@@ -1708,6 +1736,39 @@
|
||||
padding: 2px 2px 6px;
|
||||
}
|
||||
|
||||
.orientation-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 8px;
|
||||
padding: 4px 6px 2px;
|
||||
}
|
||||
|
||||
.orientation-btn {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 4px;
|
||||
padding: 8px 4px;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius);
|
||||
background: transparent;
|
||||
color: var(--color-text);
|
||||
font-family: var(--font-sans);
|
||||
font-size: 0.72rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.orientation-btn:hover {
|
||||
background: var(--color-btn-hover);
|
||||
}
|
||||
|
||||
.orientation-btn.active {
|
||||
border-color: var(--color-primary);
|
||||
background: var(--color-primary);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.margin-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
|
||||
@@ -48,20 +48,27 @@ export function getPageBreakDebug(view: EditorView): PageBreakDebugSnapshot | nu
|
||||
return debugAccessors.get(view)?.() ?? null;
|
||||
}
|
||||
|
||||
// A4 page layout constants (px at 96 dpi). The page box (height/gap/cycle) is
|
||||
// fixed; only the content-area inset (top/bottom margin) is user-adjustable, so
|
||||
// it's read per layout pass from the live padding instead of being a constant.
|
||||
// A4 page layout constants (px at 96 dpi). The page height (and thus cycle) is
|
||||
// orientation-dependent and the content-area inset (top/bottom margin) is
|
||||
// user-adjustable, so both are read per layout pass from live CSS vars instead
|
||||
// of being constants — the values below are only portrait fallbacks.
|
||||
const PAGE_HEIGHT = 1123;
|
||||
const PAGE_GAP = 20;
|
||||
const DEFAULT_MARGIN_TOP = 96;
|
||||
const DEFAULT_MARGIN_BOTTOM = 96;
|
||||
const CYCLE = PAGE_HEIGHT + PAGE_GAP; // 1143
|
||||
|
||||
type VMargins = { top: number; bottom: number; contentHeight: number };
|
||||
type VMargins = {
|
||||
top: number;
|
||||
bottom: number;
|
||||
contentHeight: number;
|
||||
pageHeight: number;
|
||||
cycle: number;
|
||||
};
|
||||
|
||||
// Reads the vertical page margins (px) from the --user-margin-* custom props the
|
||||
// Layout panel writes via applyMarginVars (pageMargins.ts). These are plain px
|
||||
// strings in document coordinates — unaffected by the ancestor CSS `zoom`, unlike
|
||||
// Reads the vertical page margins + page height (px) from the --user-* custom
|
||||
// props the Layout panel writes via applyMarginVars (pageMargins.ts) and
|
||||
// applyOrientationVars (pageOrientation.ts). These are plain px strings in
|
||||
// document coordinates — unaffected by the ancestor CSS `zoom`, unlike
|
||||
// getComputedStyle padding — so they pair directly with the unscaled offsetTop
|
||||
// measurements below. Side margins don't affect pagination (they change line
|
||||
// wrapping, which we already measure from the live DOM), so we ignore them.
|
||||
@@ -69,21 +76,29 @@ function readVerticalMargins(dom: HTMLElement): VMargins {
|
||||
const cs = getComputedStyle(dom);
|
||||
const top = parseFloat(cs.getPropertyValue('--user-margin-top'));
|
||||
const bottom = parseFloat(cs.getPropertyValue('--user-margin-bottom'));
|
||||
const ph = parseFloat(cs.getPropertyValue('--user-page-height'));
|
||||
const mt = Number.isFinite(top) ? top : DEFAULT_MARGIN_TOP;
|
||||
const mb = Number.isFinite(bottom) ? bottom : DEFAULT_MARGIN_BOTTOM;
|
||||
return { top: mt, bottom: mb, contentHeight: PAGE_HEIGHT - mt - mb };
|
||||
const pageHeight = Number.isFinite(ph) ? ph : PAGE_HEIGHT;
|
||||
return {
|
||||
top: mt,
|
||||
bottom: mb,
|
||||
contentHeight: pageHeight - mt - mb,
|
||||
pageHeight,
|
||||
cycle: pageHeight + PAGE_GAP,
|
||||
};
|
||||
}
|
||||
|
||||
function pageContentStart(page: number, marginTop: number): number {
|
||||
return (page - 1) * CYCLE + marginTop;
|
||||
function pageContentStart(page: number, marginTop: number, cycle: number): number {
|
||||
return (page - 1) * cycle + marginTop;
|
||||
}
|
||||
|
||||
function pageContentEnd(page: number, marginTop: number, contentHeight: number): number {
|
||||
return (page - 1) * CYCLE + marginTop + contentHeight;
|
||||
function pageContentEnd(page: number, marginTop: number, contentHeight: number, cycle: number): number {
|
||||
return (page - 1) * cycle + marginTop + contentHeight;
|
||||
}
|
||||
|
||||
function getPageForY(y: number): number {
|
||||
return Math.floor(y / CYCLE) + 1;
|
||||
function getPageForY(y: number, cycle: number): number {
|
||||
return Math.floor(y / cycle) + 1;
|
||||
}
|
||||
|
||||
const pageBreakKey = new PluginKey<number>('pageBreaks');
|
||||
@@ -409,6 +424,7 @@ export const PageBreaks = Extension.create({
|
||||
|
||||
const vm = readVerticalMargins(dom);
|
||||
const CONTENT_HEIGHT = vm.contentHeight;
|
||||
const CYCLE_PX = vm.cycle;
|
||||
|
||||
const zoom = getZoomFactor();
|
||||
const leaves = collectLeaves(zoom);
|
||||
@@ -422,9 +438,9 @@ export const PageBreaks = Extension.create({
|
||||
const leaf = leaves[i];
|
||||
const effectiveTop = leaf.naturalTop + cumulativeShift;
|
||||
const effectiveBottom = effectiveTop + leaf.naturalHeight;
|
||||
const page = getPageForY(effectiveTop);
|
||||
const contentStart = pageContentStart(page, vm.top);
|
||||
const contentEnd = pageContentEnd(page, vm.top, CONTENT_HEIGHT);
|
||||
const page = getPageForY(effectiveTop, CYCLE_PX);
|
||||
const contentStart = pageContentStart(page, vm.top, CYCLE_PX);
|
||||
const contentEnd = pageContentEnd(page, vm.top, CONTENT_HEIGHT, CYCLE_PX);
|
||||
|
||||
let spacerHeight = 0;
|
||||
let spacerDocPos: number | null = null;
|
||||
@@ -435,13 +451,13 @@ export const PageBreaks = Extension.create({
|
||||
spacerDocPos = preLeafDocPos(leaf.el);
|
||||
reason = 'pre-leaf-push-to-content-start';
|
||||
} else if (effectiveTop >= contentEnd) {
|
||||
spacerHeight = pageContentStart(page + 1, vm.top) - effectiveTop;
|
||||
spacerHeight = pageContentStart(page + 1, vm.top, CYCLE_PX) - effectiveTop;
|
||||
spacerDocPos = preLeafDocPos(leaf.el);
|
||||
reason = 'leaf-jump-to-next-page';
|
||||
} else if (effectiveBottom > contentEnd) {
|
||||
if (leaf.kind === 'atomic') {
|
||||
if (leaf.naturalHeight <= CONTENT_HEIGHT) {
|
||||
spacerHeight = pageContentStart(page + 1, vm.top) - effectiveTop;
|
||||
spacerHeight = pageContentStart(page + 1, vm.top, CYCLE_PX) - effectiveTop;
|
||||
spacerDocPos = preLeafDocPos(leaf.el);
|
||||
reason = 'atomic-push-to-next-page';
|
||||
} else {
|
||||
@@ -451,14 +467,14 @@ export const PageBreaks = Extension.create({
|
||||
const split = findLineSplit(leaf.el, contentEnd - effectiveTop, zoom);
|
||||
if (split === null) {
|
||||
if (leaf.naturalHeight <= CONTENT_HEIGHT) {
|
||||
spacerHeight = pageContentStart(page + 1, vm.top) - effectiveTop;
|
||||
spacerHeight = pageContentStart(page + 1, vm.top, CYCLE_PX) - effectiveTop;
|
||||
spacerDocPos = preLeafDocPos(leaf.el);
|
||||
reason = 'split-fallback-push-whole-leaf';
|
||||
} else {
|
||||
reason = 'splittable-too-tall-no-push';
|
||||
}
|
||||
} else {
|
||||
spacerHeight = pageContentStart(page + 1, vm.top) - (effectiveTop + split.naturalLineTop);
|
||||
spacerHeight = pageContentStart(page + 1, vm.top, CYCLE_PX) - (effectiveTop + split.naturalLineTop);
|
||||
spacerDocPos = split.docPos;
|
||||
reason = 'line-split';
|
||||
}
|
||||
@@ -478,7 +494,7 @@ export const PageBreaks = Extension.create({
|
||||
effectiveTop,
|
||||
effectiveBottom,
|
||||
pageOfTop: page,
|
||||
pageOfBottom: getPageForY(effectiveBottom),
|
||||
pageOfBottom: getPageForY(effectiveBottom, CYCLE_PX),
|
||||
contentStart,
|
||||
contentEnd,
|
||||
overflowsPageEnd: effectiveBottom > contentEnd,
|
||||
@@ -531,9 +547,9 @@ export const PageBreaks = Extension.create({
|
||||
if (leaves.length > 0) {
|
||||
const lastLeaf = leaves[leaves.length - 1];
|
||||
const effectiveBottom = lastLeaf.naturalTop + cumulativeShift + lastLeaf.naturalHeight;
|
||||
numPages = Math.max(1, getPageForY(effectiveBottom));
|
||||
numPages = Math.max(1, getPageForY(effectiveBottom, CYCLE_PX));
|
||||
}
|
||||
const targetHeight = numPages * CYCLE - PAGE_GAP;
|
||||
const targetHeight = numPages * CYCLE_PX - PAGE_GAP;
|
||||
dom.style.minHeight = `${targetHeight}px`;
|
||||
|
||||
dom.dispatchEvent(new CustomEvent('pm-pagecount', { bubbles: true, detail: { numPages } }));
|
||||
@@ -541,12 +557,12 @@ export const PageBreaks = Extension.create({
|
||||
lastSnapshot = {
|
||||
timestamp: new Date().toISOString(),
|
||||
layout: {
|
||||
PAGE_HEIGHT,
|
||||
PAGE_HEIGHT: vm.pageHeight,
|
||||
PAGE_GAP,
|
||||
PAGE_MARGIN_TOP: vm.top,
|
||||
PAGE_MARGIN_BOTTOM: vm.bottom,
|
||||
CONTENT_HEIGHT,
|
||||
CYCLE,
|
||||
CYCLE: CYCLE_PX,
|
||||
},
|
||||
numPages,
|
||||
leaves: leavesDebug,
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { Editor } from '@tiptap/core';
|
||||
import { tiptapToOdt, type TiptapNode, type TextFormatting, type OdtDocument, type ParagraphBuilder } from 'odf-kit';
|
||||
import { unzipSync, zipSync, strFromU8, strToU8 } from 'fflate';
|
||||
import { DEFAULT_MARGINS, type PageMargins } from '../storage/pageMargins';
|
||||
import type { Orientation } from '../storage/pageOrientation';
|
||||
|
||||
type AlignValue = 'left' | 'center' | 'right' | 'justify';
|
||||
|
||||
@@ -283,11 +284,14 @@ function applyRuns(p: ParagraphBuilder, content: TiptapNode[] = []) {
|
||||
}
|
||||
}
|
||||
|
||||
export async function exportToOdt(editor: Editor, margins: PageMargins = DEFAULT_MARGINS): Promise<void> {
|
||||
export async function exportToOdt(editor: Editor, margins: PageMargins = DEFAULT_MARGINS, orientation: Orientation = 'portrait'): Promise<void> {
|
||||
const raw = editor.getJSON() as TiptapNode;
|
||||
const json = injectCustomTypes(raw);
|
||||
|
||||
const odt = await tiptapToOdt(json, {
|
||||
// Orientation comes from the Layout panel; odf-kit swaps the A4 dimensions
|
||||
// automatically (29.7×21cm) and writes style:print-orientation accordingly.
|
||||
orientation,
|
||||
// Margins (cm) come from the Layout panel via App state. They match the
|
||||
// editor's on-screen padding so exported line wrapping / page flow is identical.
|
||||
marginTop: `${margins.top}cm`,
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
export type Orientation = 'portrait' | 'landscape';
|
||||
|
||||
const KEY = 'odf-editor-page-orientation';
|
||||
|
||||
// A4 page box in px @96dpi. Portrait is the editor's historical fixed layout;
|
||||
// landscape swaps the two dimensions (matches odf-kit's automatic A4 swap).
|
||||
export const PAGE_W_PORTRAIT = 794; // 210mm
|
||||
export const PAGE_H_PORTRAIT = 1123; // 297mm
|
||||
|
||||
export function loadOrientation(): Orientation {
|
||||
return localStorage.getItem(KEY) === 'landscape' ? 'landscape' : 'portrait';
|
||||
}
|
||||
|
||||
export function saveOrientation(o: Orientation): void {
|
||||
localStorage.setItem(KEY, o);
|
||||
}
|
||||
|
||||
// Sets --user-page-{width,height} (in px) on the document root, where they
|
||||
// inherit down to .paper / .tiptap (see editor.css) and are read per layout
|
||||
// pass by pageBreaks.ts. Landscape swaps the portrait dimensions.
|
||||
export function applyOrientationVars(o: Orientation): void {
|
||||
const landscape = o === 'landscape';
|
||||
const w = landscape ? PAGE_H_PORTRAIT : PAGE_W_PORTRAIT;
|
||||
const h = landscape ? PAGE_W_PORTRAIT : PAGE_H_PORTRAIT;
|
||||
const root = document.documentElement.style;
|
||||
root.setProperty('--user-page-width', `${w}px`);
|
||||
root.setProperty('--user-page-height', `${h}px`);
|
||||
}
|
||||
@@ -4,14 +4,16 @@
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
/* Fixed A4 paper container (794px = 210mm at 96dpi) */
|
||||
/* A4 paper container (794px = 210mm at 96dpi by default; --user-page-width is
|
||||
set from the Layout panel and swaps to 1123px in landscape — pageOrientation.ts) */
|
||||
.paper {
|
||||
width: 794px;
|
||||
width: var(--user-page-width, 794px);
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.paper .tiptap {
|
||||
--page-height: 1123px;
|
||||
/* Driven by --user-page-height (pageOrientation.ts); fallback = A4 portrait. */
|
||||
--page-height: var(--user-page-height, 1123px);
|
||||
--page-gap: 20px;
|
||||
/* Driven by --user-margin-* set on :root from the Layout panel (pageMargins.ts).
|
||||
Fallbacks preserve the original hardcoded layout when unset. */
|
||||
|
||||
Reference in new issue
Block a user