fix(magazine): top-align the page in the import preview

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
mARTin-B78 2026-09-22 01:14:58 +02:00
parent 1edc941dbf
commit 6384530dc6
6 changed files with 20 additions and 12 deletions

View File

@ -7,6 +7,11 @@ Follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) · versioned wi
## [Unreleased] ## [Unreleased]
## [1.42.1] — 2026-09-22
### Fixed
- **Magazine import preview: the page sat at the bottom of its pane.** The page image (and the region boxes drawn over it) is now aligned to the top of the preview, so it stays in view next to a long recognised-text column.
## [1.42.0] — 2026-09-22 ## [1.42.0] — 2026-09-22
### Added ### Added

View File

@ -1 +1 @@
1.42.0 1.42.1

View File

@ -27,12 +27,12 @@
<!-- ── Core styles (local — no CDN dependency for first paint) ────────── --> <!-- ── Core styles (local — no CDN dependency for first paint) ────────── -->
<link rel="stylesheet" href="/static/vendor/mdi/materialdesignicons.min.css"> <link rel="stylesheet" href="/static/vendor/mdi/materialdesignicons.min.css">
<link rel="stylesheet" href="/static/style.css?v=1.42.0"> <link rel="stylesheet" href="/static/style.css?v=1.42.1">
<link rel="stylesheet" href="/static/css/resource-diagnostics.css?v=1.42.0"> <link rel="stylesheet" href="/static/css/resource-diagnostics.css?v=1.42.1">
<link rel="stylesheet" href="/static/css/background-jobs.css?v=1.42.0"> <link rel="stylesheet" href="/static/css/background-jobs.css?v=1.42.1">
<link rel="stylesheet" href="/static/css/voice-design.css?v=1.42.0"> <link rel="stylesheet" href="/static/css/voice-design.css?v=1.42.1">
<link rel="stylesheet" href="/static/css/rehearser.css?v=1.42.0"> <link rel="stylesheet" href="/static/css/rehearser.css?v=1.42.1">
<link rel="stylesheet" href="/static/css/audiobook.css?v=1.42.0"> <link rel="stylesheet" href="/static/css/audiobook.css?v=1.42.1">
<!-- ── Flag icons — non-blocking (loaded async, icons appear after JS) ── --> <!-- ── Flag icons — non-blocking (loaded async, icons appear after JS) ── -->

View File

@ -471,14 +471,15 @@ const MAG_PREVIEW_TYPES = {
let _magPreviewSeen = { page: null, keys: new Set() }; let _magPreviewSeen = { page: null, keys: new Set() };
// Fit the box layer to the part of the frame the page image actually covers ("contain" scaling // Fit the box layer to the part of the frame the page image actually covers ("contain" scaling
// leaves bars around it), so a box drawn at 50% sits at 50% of the page, not of the frame. // leaves bars beside or below it -- the page is top-aligned), so a box drawn at 50% sits at 50% of
// the page, not of the frame.
function magFitPreviewLayer() { function magFitPreviewLayer() {
const img = $('mag-ingest-preview-img'), layer = $('mag-ingest-preview-boxes'); const img = $('mag-ingest-preview-img'), layer = $('mag-ingest-preview-boxes');
if (!img || !layer || !img.naturalWidth || !img.clientWidth) return; if (!img || !layer || !img.naturalWidth || !img.clientWidth) return;
const scale = Math.min(img.clientWidth / img.naturalWidth, img.clientHeight / img.naturalHeight); const scale = Math.min(img.clientWidth / img.naturalWidth, img.clientHeight / img.naturalHeight);
const w = img.naturalWidth * scale, h = img.naturalHeight * scale; const w = img.naturalWidth * scale, h = img.naturalHeight * scale;
layer.style.left = (img.offsetLeft + (img.clientWidth - w) / 2) + 'px'; layer.style.left = (img.offsetLeft + (img.clientWidth - w) / 2) + 'px';
layer.style.top = (img.offsetTop + (img.clientHeight - h) / 2) + 'px'; layer.style.top = img.offsetTop + 'px'; // the page is drawn at the top of its frame (object-position: top)
layer.style.width = w + 'px'; layer.style.width = w + 'px';
layer.style.height = h + 'px'; layer.style.height = h + 'px';
} }

View File

@ -5192,7 +5192,8 @@ input[list="lang-codes"] {
/* Live region boxes on the page being read (magazine ingestion preview) */ /* Live region boxes on the page being read (magazine ingestion preview) */
.mag-ingest-preview-canvas { position: relative; flex: 1 1 auto; min-height: 0; display: flex; } .mag-ingest-preview-canvas { position: relative; flex: 1 1 auto; min-height: 0; display: flex; }
.mag-ingest-preview-canvas img { width: 100%; height: 100%; object-fit: contain; } .mag-ingest-preview-canvas { align-items: flex-start; }
.mag-ingest-preview-canvas img { width: 100%; height: 100%; object-fit: contain; object-position: center top; }
.mag-ingest-preview-boxes { position: absolute; pointer-events: none; } .mag-ingest-preview-boxes { position: absolute; pointer-events: none; }
.mag-preview-box { position: absolute; border: 2px solid var(--c, #3b82f6); border-radius: 2px; .mag-preview-box { position: absolute; border: 2px solid var(--c, #3b82f6); border-radius: 2px;
background: color-mix(in srgb, var(--c, #3b82f6) 14%, transparent); } background: color-mix(in srgb, var(--c, #3b82f6) 14%, transparent); }

View File

@ -656,7 +656,7 @@ test('the live preview draws the page regions over the page image as they arrive
preview.style.width = '800px'; preview.style.width = '800px';
const canvas = document.createElement('canvas'); canvas.width = 100; canvas.height = 200; const canvas = document.createElement('canvas'); canvas.width = 100; canvas.height = 200;
const img = document.getElementById('mag-ingest-preview-img'); const img = document.getElementById('mag-ingest-preview-img');
img.style.height = '400px'; img.style.height = '1400px';
await new Promise(r => { img.onload = r; img.src = canvas.toDataURL('image/png'); }); await new Promise(r => { img.onload = r; img.src = canvas.toDataURL('image/png'); });
}); });
await page.evaluate(() => window.magRenderPreviewBoxes(5, [ await page.evaluate(() => window.magRenderPreviewBoxes(5, [
@ -669,9 +669,10 @@ test('the live preview draws the page regions over the page image as they arrive
await expect(page.locator('#mag-ingest-preview-legend')).toContainText('2 regions'); await expect(page.locator('#mag-ingest-preview-legend')).toContainText('2 regions');
const [imgBox, layerBox] = await Promise.all([ const [imgBox, layerBox] = await Promise.all([
page.locator('#mag-ingest-preview-img').boundingBox(), page.locator('#mag-ingest-preview-boxes').boundingBox()]); page.locator('#mag-ingest-preview-img').boundingBox(), page.locator('#mag-ingest-preview-boxes').boundingBox()]);
// The layer has the page's 1:2 shape and is centred in the wider frame. // The layer has the page's 1:2 shape, is centred across the wider frame and sits at its top.
expect(Math.abs(layerBox.height - 2 * layerBox.width)).toBeLessThan(2); expect(Math.abs(layerBox.height - 2 * layerBox.width)).toBeLessThan(2);
expect(Math.abs((layerBox.x + layerBox.width / 2) - (imgBox.x + imgBox.width / 2))).toBeLessThan(2); expect(Math.abs((layerBox.x + layerBox.width / 2) - (imgBox.x + imgBox.width / 2))).toBeLessThan(2);
expect(Math.abs(layerBox.y - imgBox.y)).toBeLessThan(2);
// A region that appears on the next poll is marked new; the ones already shown are not. // A region that appears on the next poll is marked new; the ones already shown are not.
await page.evaluate(() => window.magRenderPreviewBoxes(5, [ await page.evaluate(() => window.magRenderPreviewBoxes(5, [
{ x: 0, y: 0, w: 100, h: 10, type: 'title', n: 1 }, { x: 0, y: 0, w: 100, h: 10, type: 'title', n: 1 },