Merge branch 'feat/desktop-reader-brightness' into chore/reconcile-ui-review

This commit is contained in:
MechaCat02
2026-06-25 21:13:55 +02:00
2 changed files with 104 additions and 0 deletions

View File

@@ -66,6 +66,15 @@ async function mockReaderApis(page: Page) {
body: JSON.stringify({ error: { code: 'unauthenticated', message: '' } })
})
);
// Anonymous reader: read-progress reads/writes are unauthenticated. Without
// this the GET/PUT fall through to the (absent) backend and stall the load.
await page.route('**/api/v1/me/read-progress/**', (route) =>
route.fulfill({
status: 401,
contentType: 'application/json',
body: JSON.stringify({ error: { code: 'unauthenticated', message: '' } })
})
);
await page.route(`**/api/v1/mangas/${mangaId}`, (route) =>
route.fulfill({
status: 200,
@@ -126,6 +135,7 @@ test.beforeEach(async ({ context }) => {
try {
localStorage.removeItem('mangalord-reader-mode');
localStorage.removeItem('mangalord-reader-gap');
localStorage.removeItem('mangalord-reader-brightness');
} catch {
// ignore — about:blank doesn't expose localStorage yet.
}
@@ -181,6 +191,64 @@ test('gap select updates the inline gap on the continuous container', async ({ p
await expect(container).toHaveAttribute('style', /gap:\s*64px/);
});
test('desktop reader nav exposes a brightness control that dims and undims the reader', async ({
page
}) => {
// The brightness slider used to live only in the mobile settings sheet,
// so desktop users couldn't dim — and could be stuck dimmed by a value a
// phone had persisted. The desktop nav now carries its own slider.
await mockReaderApis(page);
await page.goto(`/manga/${mangaId}/chapter/${chapterId}`);
const slider = page.getByTestId('reader-brightness-desktop');
await expect(slider).toBeVisible();
const dim = () =>
page.evaluate(() =>
document.documentElement.style.getPropertyValue('--reader-dim')
);
// Default brightness is full → no dim overlay.
await expect.poll(async () => Number(await dim())).toBe(0);
// Dimming raises the overlay opacity…
await slider.fill('0.3');
await expect.poll(async () => Number(await dim())).toBeGreaterThan(0);
// …and restoring it clears the dim, so a desktop user is never stuck.
await slider.fill('1');
await expect.poll(async () => Number(await dim())).toBe(0);
});
test('a brightness value persisted by a phone is recoverable on desktop', async ({
page,
context
}) => {
// Seed a dimmed value as if set on mobile, then open the reader on a
// desktop viewport: the dim applies and the desktop control reflects it.
await context.addInitScript(() => {
try {
localStorage.setItem('mangalord-reader-brightness', '0.3');
} catch {
// about:blank — ignore
}
});
await mockReaderApis(page);
await page.goto(`/manga/${mangaId}/chapter/${chapterId}`);
const slider = page.getByTestId('reader-brightness-desktop');
await expect(slider).toHaveValue('0.3');
await expect
.poll(async () =>
Number(
await page.evaluate(() =>
document.documentElement.style.getPropertyValue('--reader-dim')
)
)
)
.toBeGreaterThan(0);
});
test('reader-mode preference set on one page is honored when the reader opens', async ({
page,
context

View File

@@ -1094,6 +1094,28 @@
{/if}
<!-- Desktop brightness. On mobile this control lives in the reader
settings sheet; desktop has no sheet, so it sits inline here.
Shares the `brightness` state and the same `--reader-dim`
effect, so a value set on either form factor stays in sync. -->
<label class="brightness-field desktop-control">
<Sun size={16} aria-hidden="true" />
<span class="visually-hidden">Brightness</span>
<input
type="range"
class="brightness-slider"
min="0.3"
max="1"
step="0.05"
value={brightness}
oninput={(e) => {
brightness = Number((e.currentTarget as HTMLInputElement).value);
}}
aria-label="Brightness"
data-testid="reader-brightness-desktop"
/>
</label>
<button
type="button"
class="reader-settings-btn mobile-control"
@@ -2181,6 +2203,20 @@
width: 100%;
}
/* Inline desktop brightness control in the reader nav. The slider is
full-width inside the mobile settings sheet; here it's capped so it
sits neatly beside the mode toggle. */
.brightness-field {
display: inline-flex;
align-items: center;
gap: var(--space-2);
color: var(--text-muted);
}
.brightness-field .brightness-slider {
width: 120px;
}
@media (max-width: 640px) {
/* Reader-nav sits at the viewport top — the global mobile
chrome is hidden on this route so there's no app bar above