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} - + +
+ Page {currentPage} of {numPages} +
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; }