diff --git a/e2e/reader.spec.js b/e2e/reader.spec.js index 9ea0654..7ad28f8 100644 --- a/e2e/reader.spec.js +++ b/e2e/reader.spec.js @@ -120,6 +120,31 @@ test('a highlight is kept, and shows in the margin', async ({ page }) => { await expect(page.locator('#reader-margin-markers .margin-note')).toHaveCount(1); }); +test('an open margin keeps the text column in the middle when there is room', async ({ page }) => { + const column = () => page.evaluate(() => { + const c = document.getElementById('reader-content'); + const p = c.querySelector('p'); + const r = p.getBoundingClientRect(); + const m = document.getElementById('reader-margin').getBoundingClientRect(); + return {left: r.left, right: r.right, width: r.width, marginRight: m.right, window: window.innerWidth}; + }); + await page.setViewportSize({ width: 1600, height: 900 }); + await openTestBook(page); + const closed = await column(); + await page.evaluate(() => { if (!readerAnnotationsMarginOpen) toggleAnnotationsMargin(); }); + await page.waitForTimeout(400); // the margin and the padding slide together + const open = await column(); + expect(Math.abs(open.left - closed.left)).toBeLessThanOrEqual(2); + expect(open.width).toBeCloseTo(closed.width, 0); + + // Not enough room: the column moves aside, but keeps its width and clears the margin. + await page.setViewportSize({ width: 1000, height: 900 }); + await page.waitForTimeout(400); + const narrow = await column(); + expect(narrow.left).toBeGreaterThanOrEqual(narrow.marginRight); + await page.evaluate(() => { toggleAnnotationsMargin(); }); +}); + test('the theme switch turns the page and the reader light, and stays', async ({ page }) => { await page.goto('/'); await expect(page.locator('html')).toHaveAttribute('data-theme', 'dark'); @@ -135,6 +160,8 @@ test('the theme switch turns the page and the reader light, and stays', async ({ test('every tab opens without errors', async ({ page }) => { await page.goto('/'); + await page.waitForLoadState('networkidle'); + await page.waitForFunction(() => typeof showTab === 'function' && typeof loadBookList === 'function'); for (const tab of ['home', 'radio', 'podcasts', 'books', 'feeds', 'focus', 'ambient']) { await page.evaluate(t => showTab(t), tab); await expect(page.locator(`#tab-${tab}`)).toBeVisible(); diff --git a/playwright.config.js b/playwright.config.js index 2c6bd11..bd285ac 100644 --- a/playwright.config.js +++ b/playwright.config.js @@ -18,6 +18,9 @@ module.exports = defineConfig({ baseURL: BASE_URL, trace: 'retain-on-failure', screenshot: 'only-on-failure', + // A new service worker taking over reloads the page (28-serviceworker.js), + // mid-test. It isn't what these tests are about. + serviceWorkers: 'block', }, projects: [ { diff --git a/static/css/app.css b/static/css/app.css index 350c0b4..4fd1024 100644 --- a/static/css/app.css +++ b/static/css/app.css @@ -1893,12 +1893,15 @@ html.nui-tts-on body.reader-immersive .reader-overlay { bottom: var(--bar-h); } is purely positional, only ever visible here in the margin. */ .reader-body-row { display:flex; flex:1; min-height:0; } .reader-margin { + --reader-margin-w:220px; /* read by app.js _centerReaderColumn */ width:0; flex-shrink:0; overflow:hidden; display:flex; flex-direction:column; border-right:1px solid var(--border); background:var(--bg); transition:width 0.25s ease; } -.reader-margin.open { width:220px; } +.reader-margin.open { width:var(--reader-margin-w); } +/* The text column keeps its place while the margin opens (app.js _centerReaderColumn). */ +.reader-content { transition: padding-right 0.25s ease; } .reader-margin-header { display:none; align-items:center; justify-content:space-between; padding:6px 8px; border-bottom:1px solid var(--border); white-space:nowrap; flex-shrink:0; @@ -1973,7 +1976,7 @@ html.nui-tts-on body.reader-immersive .reader-overlay { bottom: var(--bar-h); } .note-bottom-sheet-actions { display:flex; justify-content:flex-end; gap:8px; } @media (max-width: 600px) { - .reader-margin.open { width:150px; } + .reader-margin { --reader-margin-w:150px; } .reader-margin-title { display:none; } } diff --git a/static/js/app/62-reader.js b/static/js/app/62-reader.js index a78aecd..268e896 100644 --- a/static/js/app/62-reader.js +++ b/static/js/app/62-reader.js @@ -520,6 +520,7 @@ function closeReader() { readerAnnotationsMarginOpen = false; const marginEl = $('reader-margin'); if (marginEl) marginEl.classList.remove('open'); + _centerReaderColumn(); const marginMarkersEl = $('reader-margin-markers'); if (marginMarkersEl) marginMarkersEl.innerHTML = ''; diff --git a/static/js/app/64-reader-settings.js b/static/js/app/64-reader-settings.js index f9c604e..fe94dae 100644 --- a/static/js/app/64-reader-settings.js +++ b/static/js/app/64-reader-settings.js @@ -96,6 +96,8 @@ function applyReaderSettings(isPdf) { if (isPdf && readerAnnotationsMarginOpen) toggleAnnotationsMargin(); document.querySelectorAll('.marker-mode-btn').forEach(b => { b.style.display = isPdf ? 'none' : ''; }); if (isPdf && markerModeActive) toggleMarkerMode(); + // Line length or font changed: the column's place next to an open margin too. + if (readerAnnotationsMarginOpen) _centerReaderColumn(); } // Font weight slider: the variable Source Serif 4 takes any weight, the diff --git a/static/js/app/74-margin.js b/static/js/app/74-margin.js index 60f0c6b..d929fd8 100644 --- a/static/js/app/74-margin.js +++ b/static/js/app/74-margin.js @@ -13,6 +13,7 @@ function toggleAnnotationsMargin() { if (!el || !markersEl || currentPdfDoc) return; readerAnnotationsMarginOpen = !readerAnnotationsMarginOpen; el.classList.toggle('open', readerAnnotationsMarginOpen); + _centerReaderColumn(); if (readerAnnotationsMarginOpen) { // Reposition once now (pre-transition) and once more after the width // transition settles, since content reflow can shift line heights. @@ -23,6 +24,32 @@ function toggleAnnotationsMargin() { } } +// The margin takes its width from the left, which pushes the text column off +// the middle of the window. Where there's room, the same width added on the +// right puts it back; where there isn't, as much as fits (the column never +// gets narrower for it). Both widths change together, so the text stays put. +function _centerReaderColumn() { + const contentEl = $('reader-content'); + const marginEl = $('reader-margin'); + const rowEl = $('reader-body-row'); + if (!contentEl) return; + if (!readerAnnotationsMarginOpen || !marginEl || !rowEl || currentPdfDoc) { + contentEl.style.paddingRight = ''; + return; + } + const margin = parseFloat(getComputedStyle(marginEl).getPropertyValue('--reader-margin-w')) || 0; + const side = parseFloat(getComputedStyle(contentEl).paddingLeft) || 0; + // The column's width: --reader-max-width, measured in the reader's font. + const probe = document.createElement('div'); + probe.style.cssText = 'position:absolute;visibility:hidden;height:0;width:var(--reader-max-width,65ch)'; + contentEl.appendChild(probe); + const column = probe.getBoundingClientRect().width; + probe.remove(); + const room = rowEl.clientWidth - margin - 2 * side - column; + contentEl.style.paddingRight = (side + Math.max(0, Math.min(margin, room))) + 'px'; +} +window.addEventListener('resize', () => { if (readerAnnotationsMarginOpen) _centerReaderColumn(); }); + // Renders each highlight/note at its text position in the margin. Entries // with note text become a readable "post-it" card (the whole point of the // margin — you can read notes there, not just see that one exists); a plain diff --git a/static/js/sw.js b/static/js/sw.js index cd485c4..24d713e 100644 --- a/static/js/sw.js +++ b/static/js/sw.js @@ -2,7 +2,7 @@ * diora service worker — caches the app shell for offline use. */ -const CACHE = 'diora-v106'; +const CACHE = 'diora-v107'; const SHELL = [ '/static/css/app.css', '/static/js/app/01-core.js',