diff --git a/src/App.svelte b/src/App.svelte
index a979245..dae3f5b 100644
--- a/src/App.svelte
+++ b/src/App.svelte
@@ -14,6 +14,12 @@
let themeMode: ThemeMode = $state(loadTheme());
let themeOpen = $state(false);
let toolbarExpanded = $state(loadToolbarExpanded());
+ let zoom = $state(Math.max(50, Math.min(200, parseInt(localStorage.getItem('odf-editor-zoom') ?? '100', 10))));
+
+ function setZoom(value: number) {
+ zoom = Math.max(50, Math.min(200, value));
+ localStorage.setItem('odf-editor-zoom', String(zoom));
+ }
function selectTheme(m: ThemeMode) {
themeMode = m;
@@ -113,9 +119,24 @@
{#if toolbarExpanded}
{/if}
-
+
@@ -265,4 +286,99 @@
user-select: none;
z-index: 50;
}
+
+ .zoom-controls {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ margin-left: auto;
+ }
+
+ .zoom-btn {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 16px;
+ height: 16px;
+ padding: 0;
+ border: none;
+ background: transparent;
+ color: var(--color-text-muted);
+ font-size: 14px;
+ line-height: 1;
+ cursor: pointer;
+ border-radius: 2px;
+ transition: color 0.1s, background 0.1s;
+ }
+
+ .zoom-btn:hover:not(:disabled) {
+ color: var(--color-text);
+ background: var(--color-btn-hover);
+ }
+
+ .zoom-btn:disabled {
+ opacity: 0.35;
+ cursor: not-allowed;
+ }
+
+ .zoom-slider {
+ -webkit-appearance: none;
+ appearance: none;
+ width: 80px;
+ height: 3px;
+ border-radius: 2px;
+ background: var(--color-border);
+ outline: none;
+ cursor: pointer;
+ }
+
+ .zoom-slider::-webkit-slider-thumb {
+ -webkit-appearance: none;
+ appearance: none;
+ width: 10px;
+ height: 10px;
+ border-radius: 50%;
+ background: var(--color-text-muted);
+ cursor: pointer;
+ transition: background 0.1s;
+ }
+
+ .zoom-slider:hover::-webkit-slider-thumb,
+ .zoom-slider:focus::-webkit-slider-thumb {
+ background: var(--color-primary);
+ }
+
+ .zoom-slider::-moz-range-thumb {
+ width: 10px;
+ height: 10px;
+ border: none;
+ border-radius: 50%;
+ background: var(--color-text-muted);
+ cursor: pointer;
+ transition: background 0.1s;
+ }
+
+ .zoom-slider:hover::-moz-range-thumb,
+ .zoom-slider:focus::-moz-range-thumb {
+ background: var(--color-primary);
+ }
+
+ .zoom-pct {
+ min-width: 36px;
+ padding: 0 3px;
+ border: none;
+ background: transparent;
+ color: var(--color-text-muted);
+ font-family: var(--font-sans);
+ font-size: 0.75rem;
+ text-align: right;
+ cursor: pointer;
+ border-radius: 2px;
+ transition: color 0.1s, background 0.1s;
+ }
+
+ .zoom-pct:hover {
+ color: var(--color-text);
+ background: var(--color-btn-hover);
+ }
diff --git a/src/lib/editor/Editor.svelte b/src/lib/editor/Editor.svelte
index 0d1d47f..d9e847f 100644
--- a/src/lib/editor/Editor.svelte
+++ b/src/lib/editor/Editor.svelte
@@ -5,8 +5,8 @@
import { saveDocument, loadDocument } from '../storage/autosave';
import '../../styles/editor.css';
- let { editor = $bindable(), tick = $bindable(0), currentPage = $bindable(1), numPages = $bindable(1) }: {
- editor: Editor | null; tick: number; currentPage: number; numPages: number;
+ let { editor = $bindable(), tick = $bindable(0), currentPage = $bindable(1), numPages = $bindable(1), zoom = 100 }: {
+ editor: Editor | null; tick: number; currentPage: number; numPages: number; zoom: number;
} = $props();
let element: HTMLDivElement;
@@ -20,15 +20,17 @@
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.
+ const zoomFactor = zoom / 100;
- // If the cursor is visible in the editor viewport, show its page
if (editor) {
try {
const coords = editor.view.coordsAtPos(editor.state.selection.head);
const cursorMidY = (coords.top + coords.bottom) / 2;
if (cursorMidY >= editorRect.top && cursorMidY <= editorRect.bottom) {
- const cursorInTiptap = cursorMidY - tiptapRect.top;
- currentPage = Math.max(1, Math.min(numPages, Math.floor(Math.max(0, cursorInTiptap) / CYCLE) + 1));
+ const cursorInDoc = (cursorMidY - tiptapRect.top) / zoomFactor;
+ currentPage = Math.max(1, Math.min(numPages, Math.floor(Math.max(0, cursorInDoc) / CYCLE) + 1));
return;
}
} catch {
@@ -36,9 +38,8 @@
}
}
- // Cursor not visible — use the top of the visible scroll area
- const visibleTopInTiptap = editorRect.top - tiptapRect.top;
- currentPage = Math.max(1, Math.min(numPages, Math.floor(Math.max(0, visibleTopInTiptap) / CYCLE) + 1));
+ const visibleTopInDoc = (editorRect.top - tiptapRect.top) / zoomFactor;
+ currentPage = Math.max(1, Math.min(numPages, Math.floor(Math.max(0, visibleTopInDoc) / CYCLE) + 1));
}
function onPageCount(e: Event) {
@@ -64,8 +65,8 @@
if (!tiptap) return;
try {
const coords = e.view.coordsAtPos(e.state.selection.head);
- const cursorInTiptap = ((coords.top + coords.bottom) / 2) - tiptap.getBoundingClientRect().top;
- currentPage = Math.max(1, Math.min(numPages, Math.floor(Math.max(0, cursorInTiptap) / CYCLE) + 1));
+ const cursorInDoc = ((coords.top + coords.bottom) / 2 - tiptap.getBoundingClientRect().top) / (zoom / 100);
+ currentPage = Math.max(1, Math.min(numPages, Math.floor(Math.max(0, cursorInDoc) / CYCLE) + 1));
} catch { /* ignore */ }
},
onUpdate: ({ editor: e }) => {
@@ -85,5 +86,5 @@