feat: add bottom toolbar to show page number

This commit is contained in:
Steffen Becker committed 2026-05-25 20:50:08 +02:00
1 parent 166cadde86
commit 87be83efd3
5 files changed
+85 -4

No files matched your search

+1
View File
@@ -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
+24 -1
View File
@@ -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}
<ToolbarExpanded {editor} {tick} />
{/if}
<EditorComponent bind:editor bind:tick />
<EditorComponent bind:editor bind:tick bind:currentPage bind:numPages />
<footer class="statusbar">
<span>Page {currentPage} of {numPages}</span>
</footer>
</main>
<style>
@@ -242,4 +247,22 @@
opacity: 0.5;
cursor: not-allowed;
}
.statusbar {
position: fixed;
bottom: 0;
left: 0;
right: 0;
height: 26px;
display: flex;
align-items: center;
padding: 0 1rem;
background: var(--color-toolbar-bg);
border-top: 1px solid var(--color-border);
font-family: var(--font-sans);
font-size: 0.75rem;
color: var(--color-text-muted);
user-select: none;
z-index: 50;
}
</style>
+56 -2
View File
@@ -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);
});
</script>
<div class="editor">
<div class="editor" bind:this={editorContainer}>
<div bind:this={element} class="paper"></div>
</div>
+3
View File
@@ -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;
}
+1 -1
View File
@@ -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;
}