Phone reader: back and the item's actions float over the text (#456)

On a phone they sat above the title and scrolled away with it, so
marking, pinning or sharing a long post meant scrolling back to its
top. Now back sits alone on the left and the actions in a capsule on
the right, sticky at the top of the reader, as Gmail's iOS reader does.
Each theme's own panel, frosted, so no theme changes colour; Glass gets
its sheen and a denser fill, since the text scrolls right under them,
and Classic its toolbar metal. Read and pin redraw the reader, which
now keeps its scroll position, since they are pressed mid-post. The
desktop reader is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RqkrVA8TdTxe556JB3sR1n
This commit is contained in:
2026-11-13 17:43:45 +00:00
parent 757ce5fbb3
commit 47e093e313
4 changed files with 74 additions and 10 deletions

View File

@@ -2129,6 +2129,36 @@ test.describe('an item with no files, on a phone', () => {
await expect(page.locator('#detail .encbox')).toHaveCount(0);
await expect(page.locator('#detail')).not.toContainText('No files');
});
test('back and the item\'s actions stay at hand anywhere in a long post (#456)', async ({ page }) => {
await page.locator('#burger').click();
await page.locator('#feedlist .place', { hasText: 'All Subscriptions' }).click();
await page.locator('.tabs button', { hasText: 'All' }).first().click();
await expect(page.locator('.ep').first()).toBeVisible({ timeout: 20_000 });
await page.locator('.ep').first().click();
await expect(page.locator('#detail .dt')).toBeVisible();
// A long post, and a link, so the whole set of actions is there.
await page.evaluate(() => {
const e = S.entries.find(x => entryKey(x) === S.sel)!;
showDetail({ ...e, link: 'https://example.com/post', description: '<p>Words.</p>'.repeat(400) });
});
const bar = page.locator('#detail .dfloat');
await expect(bar.locator('#dback')).toBeVisible();
await expect(bar.locator('.dacts [data-a]')).toHaveCount(3);
await page.locator('#detail').evaluate(el => { el.scrollTop = 3000; });
// Still at the top of the screen, not scrolled away with the title.
for (const q of ['#dback', '.dacts']) expect((await bar.locator(q).boundingBox())!.y).toBeLessThan(80);
// Pressed down there, the reader keeps its place.
const flag = bar.locator('[data-a="flag"]');
const was = await flag.getAttribute('aria-pressed');
await flag.click();
await expect(bar.locator('[data-a="flag"]')).not.toHaveAttribute('aria-pressed', was!);
expect(await page.locator('#detail').evaluate(el => el.scrollTop)).toBeGreaterThan(2500);
await bar.locator('[data-a="flag"]').click();
await expect(bar.locator('[data-a="flag"]')).toHaveAttribute('aria-pressed', was!);
await bar.locator('#dback').click();
await expect(page.locator('#detail')).toBeHidden();
});
});
test('a pinned feed, even one from inside a folder, goes to the top of the list', async ({ page }) => {