Skip to content

Commit 41905a9

Browse files
authored
Let long subject tags wrap instead of shoving the book page sideways (#1171)
Addresses #1170. The read-only tag pill was `white-space: nowrap` with no `max-width`, so a Library-of-Congress subject heading rendered wider than the wrapping flex row holding it and pushed the book detail page past the viewport. Readers without the edit role — guests and viewer accounts — got a page that scrolled sideways on a phone. The editor branch uses removable chips that already wrap, and every mobile spec logs in as admin, which is why the gate never covered it. Verified live on cwn-local as a guest: 12px of overflow at 390px, 82px at 320px, 323px with one unbroken token, all 0 after. Keeping nowrap and adding max-width alone leaves 82px, which pins nowrap as the cause rather than a missing cap. Regression test measures the overflow the long tags add rather than the page's absolute overflow, so it holds on any baseline: red at 323px pre-fix against a redeployed pre-fix build, green after. book-detail.spec.ts is 13/13 on desktop and mobile, and CI E2E went 267 to 269 passed with the same three pre-existing failures untouched.
1 parent 63d0b3b commit 41905a9

7 files changed

Lines changed: 140 additions & 11 deletions

File tree

CHANGELOG.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,16 @@ is for things you can see or feel when running the app.
1616

1717
## [Unreleased]
1818

19+
### Fixed
20+
21+
- Book pages no longer scroll sideways on a phone when a book carries long
22+
subject tags. Libraries with Library-of-Congress headings (for example
23+
"France -- History -- Revolution, 1789-1799 -- Fiction") pushed the tag row
24+
off the edge of the screen for anyone reading without an editor account,
25+
including guests. Long tags now wrap instead. (#1170)
26+
27+
## [v4.1.22] - 2026-07-27
28+
1929
### Changed
2030

2131
- **The "read" mark on a book cover is now a green "Read" label at the bottom-left, instead of a small tick in the corner.** It was a 22-pixel checkmark circle in the top-right with no wording, which was easy to miss at a glance — the reporter, who reads in the light theme because of their eyesight, pointed out that the classic view's labelled green badge was far easier to spot. It now says what it means, sits where the classic one sits, and gets larger again on phones and tablets. The wording follows your language, so a German library reads "Gelesen" exactly as the classic view does. Reported by [@uschi1](https://github.com/new-usemame/Calibre-Web-NextGen/issues/351) ([#1117](https://github.com/new-usemame/Calibre-Web-NextGen/issues/1117)).
@@ -38,6 +48,8 @@ is for things you can see or feel when running the app.
3848

3949
- **Opening your library no longer runs the same search twice.** Every visit to the library asked the server for the first page of books, then immediately asked for it again at a different size and threw the first answer away. Nothing looked wrong — the wasted answer never reached the screen — but on a large library that first page is the slowest query on it, and it was being run twice on every load, for every reader. The grid now waits until it knows how many columns it has before asking, so the page is fetched once ([#1144](https://github.com/new-usemame/Calibre-Web-NextGen/issues/1144)).
4050

51+
- **The series line on a book cover is dark enough to read again.** The small series text under a title on each cover was styled with the muted grey the rest of the interface uses, and then faded a second time on top of that. The two together took it to a contrast of about 4:1 against the card, below the 4.5:1 that small text needs to stay legible, which made it hard to read for anyone whose eyesight or screen is less than ideal. The second fade is gone, so the line is the muted grey it was meant to be ([#1135](https://github.com/new-usemame/Calibre-Web-NextGen/issues/1135)).
52+
4153
## [v4.1.21] - 2026-07-25
4254

4355
### Added

CHANGES-vs-upstream.md

Lines changed: 10 additions & 7 deletions
Large diffs are not rendered by default.

cps/spa_strings.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -337,6 +337,7 @@ def _(message): # noqa: E743 - intentional gettext extraction marker, not the b
337337
_("Create an app password")
338338
_("Browse series")
339339
_("Browse authors")
340+
_("Browse languages")
340341
_("Open Table view")
341342
_("Open Admin")
342343
_("Open account settings")

frontend/e2e/book-detail.spec.ts

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { test, expect, Page } from '@playwright/test';
2-
import { collectPageErrors, assertNoPageErrors, assertNoHorizontalOverflow } from './utils';
2+
import { collectPageErrors, assertNoPageErrors, assertNoHorizontalOverflow, pageOverflow } from './utils';
33

44
/*
55
* Book-detail completeness pass:
@@ -182,3 +182,59 @@ test('book detail with a "More by" strip has no horizontal overflow on mobile',
182182
await expect(page.locator('section[aria-label^="More by"]')).toBeVisible({ timeout: 10_000 });
183183
await assertNoHorizontalOverflow(page);
184184
});
185+
186+
// A reader without the edit role sees the read-only tag pills, and those pills
187+
// were `white-space: nowrap` with no max-width. Real libraries carry
188+
// Library-of-Congress subject headings ("France -- History -- Revolution,
189+
// 1789-1799 -- Fiction") that are wider than a phone, so one tag dragged the
190+
// whole detail page into horizontal scroll. The admin-role branch renders
191+
// removable chips instead and wraps fine, which is why the existing mobile
192+
// specs — all of which log in as admin — never saw this.
193+
//
194+
// Deterministic on any seed: the role and the tag list are both stubbed.
195+
//
196+
// Measures the DELTA the long tags add, not absolute overflow. The CI fixture's
197+
// detail page carries ~35px of horizontal overflow from an unrelated cause
198+
// (notes/e2e-mobile-overflow-ci-triage.md), and an absolute assertion here would
199+
// fail on that instead of on the tag row, reporting nothing about the pills. The
200+
// delta is what this fix owns: pre-fix it was ~300px, post-fix it is 0.
201+
test('long tag names add no horizontal overflow to the read-only detail page', async ({ page }) => {
202+
await page.setViewportSize({ width: 390, height: 844 });
203+
await page.goto('/app');
204+
const bookId = await firstBookId(page);
205+
test.skip(bookId == null, 'seed has no books');
206+
207+
// Drop the edit role so the page renders the read-only Pill branch that a
208+
// guest or viewer account gets.
209+
await page.route('**/api/v1/auth/me', async (route) => {
210+
const res = await route.fetch();
211+
const me = await res.json();
212+
if (me?.role) me.role.edit = false;
213+
await route.fulfill({ response: res, json: me });
214+
});
215+
216+
await page.goto(`/app/book/${bookId}`, { waitUntil: 'domcontentloaded' });
217+
await expect(page.locator('main h1')).toBeVisible({ timeout: 10_000 });
218+
const baseline = await pageOverflow(page);
219+
220+
// A real LoC heading plus a single unbroken token wider than the viewport.
221+
const longTag = 'France -- History -- Revolution, 1789-1799 -- Fiction';
222+
await page.route(`**/api/v1/books/${bookId}`, async (route) => {
223+
const res = await route.fetch();
224+
const book = await res.json();
225+
book.tags = [
226+
{ id: 990001, name: longTag },
227+
{ id: 990002, name: 'Bildungsroman'.repeat(8) },
228+
];
229+
await route.fulfill({ response: res, json: book });
230+
});
231+
232+
await page.reload({ waitUntil: 'domcontentloaded' });
233+
await expect(page.getByText(longTag)).toBeVisible({ timeout: 10_000 });
234+
const withLongTags = await pageOverflow(page);
235+
236+
expect(
237+
withLongTags - baseline,
238+
`long tags widened the page by ${withLongTags - baseline}px (baseline ${baseline}px)`,
239+
).toBeLessThanOrEqual(1);
240+
});

frontend/e2e/utils.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,17 @@ export function assertNoPageErrors(errors: string[]) {
2626
/** No horizontal body overflow — the signature of the mobile-reflow regressions
2727
* (#288 banner, #576 drawer, edit-cover at 375px). */
2828
export async function assertNoHorizontalOverflow(page: Page) {
29-
const overflow = await page.evaluate(() => {
29+
const overflow = await pageOverflow(page);
30+
expect(overflow, 'page scrolls horizontally (mobile reflow regression)').toBeLessThanOrEqual(1);
31+
}
32+
33+
/** Horizontal overflow in px, for specs that need to compare one render against
34+
* another rather than assert the absolute property. Asserting "no overflow at
35+
* all" makes a spec fail for any unrelated overflow the page happens to carry,
36+
* which tells you nothing about the element under test. */
37+
export async function pageOverflow(page: Page): Promise<number> {
38+
return page.evaluate(() => {
3039
const el = document.documentElement;
3140
return el.scrollWidth - el.clientWidth;
3241
});
33-
expect(overflow, 'page scrolls horizontally (mobile reflow regression)').toBeLessThanOrEqual(1);
3442
}

frontend/src/components/Pill.module.css

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,11 @@
77
border-radius: var(--r-full);
88
padding: var(--sp-1) var(--sp-3);
99
border: 1px solid var(--border);
10-
white-space: nowrap;
10+
/* No nowrap: real Calibre libraries carry Library-of-Congress subject
11+
headings ("France -- History -- Revolution, 1789-1799 -- Fiction") that are
12+
wider than a phone, and an unbreakable pill pushed the whole book detail
13+
page into horizontal scroll (#1130). Normal wrapping still keeps every
14+
pill that fits on one line; anywhere breaks a single oversized token. */
15+
max-width: 100%;
16+
overflow-wrap: anywhere;
1117
}

frontend/src/data/whatsNew.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,49 @@ export interface WhatsNewRelease {
5555

5656
/** Newest release first. The `whats-new-populate` skill prepends here. */
5757
export const WHATS_NEW: WhatsNewRelease[] = [
58+
{
59+
version: 'v4.1.22',
60+
date: '2026-07-27',
61+
items: [
62+
{
63+
title: 'A book that cannot be converted still lands in your library',
64+
body: 'If an incoming book could not be converted to your chosen format — a large PDF that ran out of time, a file Calibre chokes on, a format needing a plugin you do not have — it was dropped altogether: no entry in the library and the file gone from the ingest folder, while the log still reported success. The original file is now imported whenever a conversion fails, and a conversion that runs long ends as an ordinary failure instead of killing the import outright. Raising Ingest Timeout in CWA Settings raises the conversion limit along with it.',
65+
category: 'Library',
66+
},
67+
{
68+
title: 'The "read" mark on a cover now says Read',
69+
body: 'A finished book was marked with a small unlabelled tick in the corner of its cover, which was easy to miss at a glance — particularly in the light theme. It is now a green "Read" label at the bottom-left, the same place and wording the classic view uses, and it stays large enough to see on a phone or tablet. The label follows the language you read the interface in.',
70+
category: 'Library',
71+
link: { to: '/', label: 'Open your library' },
72+
},
73+
{
74+
title: 'Books recorded as "ger", "fre" or "dut" show their real language',
75+
body: 'A language can be written two ways in book metadata, and only the form Calibre normally writes was recognised — so a book carrying the other form, common in library-catalogue records and some EPUB files, showed "Unknown" as its language, and importing one could be refused outright. Both forms are now accepted for all twenty languages where they differ. Whether a language counts as valid also no longer depends on the language you happen to read the interface in.',
76+
category: 'Library',
77+
link: { to: '/languages', label: 'Browse languages' },
78+
},
79+
{
80+
title: 'Pages no longer fail at random when the server is busy',
81+
body: 'The server kept one working copy of your library and shared it across everything it was doing at once, so a page being built could lose its data the moment another page finished — or the moment a background job such as a duplicate scan, thumbnail run or metadata backup completed. It surfaced as a different-looking error every time, and grew likelier the busier the server was, which hit big libraries and shared instances hardest. Every page load and every background job now works from its own copy.',
82+
category: 'Under the hood',
83+
},
84+
{
85+
title: 'Your library keeps working when its extra fields cannot be read',
86+
body: 'Book pages, the table view and both search screens ask your library for the extra fields you can add to a book in Calibre. If that lookup failed — the library mid-write, its folder moved, the definitions not there yet — those pages returned an error rather than simply leaving the extra fields out, even though the rest of the library was perfectly readable. They now load without them and note the reason in the log.',
87+
category: 'Under the hood',
88+
},
89+
{
90+
title: 'Opening your library runs one search instead of two',
91+
body: 'Every visit asked the server for the first page of books, then immediately asked again at a different size and threw the first answer away. Nothing looked wrong, but on a large library that first page is the slowest query there is, and it was running twice on every load for every reader. The grid now waits until it knows how many columns it has before asking.',
92+
category: 'Under the hood',
93+
},
94+
{
95+
title: 'The series line under a cover is easier to read',
96+
body: 'The small series text on a book cover was given the muted grey the rest of the interface uses and then faded a second time on top of that, which took it below the contrast small text needs to stay legible. The extra fade is gone, so the line is the muted grey it was meant to be.',
97+
category: 'Under the hood',
98+
},
99+
],
100+
},
58101
{
59102
version: 'v4.1.21',
60103
date: '2026-07-25',

0 commit comments

Comments
 (0)