diff --git a/CHANGELOG.md b/CHANGELOG.md
index c3436d5..75b2be1 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -29,6 +29,7 @@
- Shadow edges at page boundaries for depth effect
- Dynamic page snapping (editor always shows full pages)
- Print-ready @media print styles with @page A4 sizing
+- add bottom toolbar to show page number
### Not yet implemented
- ODT import / open existing file
diff --git a/src/App.svelte b/src/App.svelte
index 6c0c0dc..a979245 100644
--- a/src/App.svelte
+++ b/src/App.svelte
@@ -8,6 +8,8 @@
let editor: Editor | null = $state(null);
let tick: number = $state(0);
+ let currentPage: number = $state(1);
+ let numPages: number = $state(1);
let themeMode: ThemeMode = $state(loadTheme());
let themeOpen = $state(false);
@@ -111,7 +113,10 @@
{#if toolbarExpanded}
{/if}
-
+
+
diff --git a/src/lib/editor/Editor.svelte b/src/lib/editor/Editor.svelte
index 6ef98ed..0d1d47f 100644
--- a/src/lib/editor/Editor.svelte
+++ b/src/lib/editor/Editor.svelte
@@ -5,9 +5,46 @@
import { saveDocument, loadDocument } from '../storage/autosave';
import '../../styles/editor.css';
- let { editor = $bindable(), tick = $bindable(0) }: { editor: Editor | null; tick: number } = $props();
+ let { editor = $bindable(), tick = $bindable(0), currentPage = $bindable(1), numPages = $bindable(1) }: {
+ editor: Editor | null; tick: number; currentPage: number; numPages: number;
+ } = $props();
let element: HTMLDivElement;
+ let editorContainer: HTMLDivElement;
+
+ const CYCLE = 1143; // PAGE_HEIGHT + PAGE_GAP, must match pageBreaks.ts
+
+ function updateCurrentPage() {
+ const tiptap = element?.querySelector('.tiptap') as HTMLElement | null;
+ if (!tiptap || !editorContainer) return;
+
+ const editorRect = editorContainer.getBoundingClientRect();
+ const tiptapRect = tiptap.getBoundingClientRect();
+
+ // 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));
+ return;
+ }
+ } catch {
+ // coordsAtPos can fail during editor teardown — fall through
+ }
+ }
+
+ // 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));
+ }
+
+ function onPageCount(e: Event) {
+ numPages = (e as CustomEvent<{ numPages: number }>).detail.numPages;
+ updateCurrentPage();
+ }
onMount(() => {
const saved = loadDocument();
@@ -19,17 +56,34 @@
onTransaction: () => {
tick++;
},
+ onSelectionUpdate: ({ editor: e }) => {
+ // Use the editor instance passed by TipTap directly — avoids any Svelte
+ // prop-reactivity timing issues. TipTap always auto-scrolls the cursor
+ // into view before firing this, so no visibility check is needed here.
+ const tiptap = element?.querySelector('.tiptap') as HTMLElement | null;
+ 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));
+ } catch { /* ignore */ }
+ },
onUpdate: ({ editor: e }) => {
saveDocument(e.getJSON());
},
});
+
+ element.addEventListener('pm-pagecount', onPageCount);
+ editorContainer.addEventListener('scroll', updateCurrentPage);
});
onDestroy(() => {
editor?.destroy();
+ element?.removeEventListener('pm-pagecount', onPageCount);
+ editorContainer?.removeEventListener('scroll', updateCurrentPage);
});
-
+
diff --git a/src/lib/editor/pageBreaks.ts b/src/lib/editor/pageBreaks.ts
index 8507f55..8438e7f 100644
--- a/src/lib/editor/pageBreaks.ts
+++ b/src/lib/editor/pageBreaks.ts
@@ -141,6 +141,9 @@ export const PageBreaks = Extension.create({
const targetHeight = numPages * CYCLE - PAGE_GAP; // = N*PAGE_HEIGHT + (N-1)*PAGE_GAP
dom.style.minHeight = `${targetHeight}px`;
+ // Notify the Svelte layer of the current page count
+ dom.dispatchEvent(new CustomEvent('pm-pagecount', { bubbles: true, detail: { numPages } }));
+
isUpdating = false;
}
diff --git a/src/styles/editor.css b/src/styles/editor.css
index de35844..bc0af72 100644
--- a/src/styles/editor.css
+++ b/src/styles/editor.css
@@ -2,7 +2,7 @@
flex: 1;
display: flex;
justify-content: center;
- padding: 2rem 1rem;
+ padding: 2rem 1rem 3rem; /* extra bottom padding clears the fixed status bar */
overflow-y: auto;
}