diff --git a/backend/app/routes/messages.py b/backend/app/routes/messages.py index d0e9a7b0..28270603 100644 --- a/backend/app/routes/messages.py +++ b/backend/app/routes/messages.py @@ -97,8 +97,9 @@ def _system_partner() -> dict: # The welcome's opening line in every language — how a stored copy is recognised. It was rendered # once, in the language of the day it was created, so an account that later switched language kept # reading it in the old one (E31 S1: the owner's, in English). Recognised here, it is served in the -# reader's language instead. Derived from WELCOME itself, so only a change to an OPENING line would -# orphan the copies already stored. +# reader's language instead. Derived from WELCOME itself, so a change to an OPENING line, or a +# language taken out of WELCOME, orphans the copies already stored: they are served as stored. The +# one retired language (German, 2026-07) left none on prod (checked 2026-10-02). _WELCOME_OPENINGS = {text.partition("\n")[0] for text in WELCOME.values()} diff --git a/frontend/e2e/channel.spec.ts b/frontend/e2e/channel.spec.ts index b3b46db9..2b9a3c23 100644 --- a/frontend/e2e/channel.spec.ts +++ b/frontend/e2e/channel.spec.ts @@ -106,28 +106,34 @@ test.describe("channel view", () => { await expectContentType(page, "includeLive", false); }); - // Platform S4b: the whole channel chrome (banner, identity, tabs, toolbar) is fixed — it stays - // put while only the video grid scrolls. - test("the channel chrome stays fixed while the video grid scrolls", async ({ page }) => { + // Platform S4b + E31 S4c: the channel chrome sits outside the scroller. Scrolling into the videos + // folds the banner and identity once (the name moves into the tab row); from then on the folded + // chrome stays put while only the video grid scrolls. + test("the channel chrome folds once, then stays fixed while the grid scrolls", async ({ + page, + }) => { await openApp(page); await page.getByTestId("video-card-channel").first().click(); const count = page.getByTestId("item-count"); - const title = page.getByTestId("channel-title"); await expect(count).toBeVisible(); await expect(page.getByTestId("video-card").first()).toBeVisible(); - const titleTop = await title.evaluate((el) => el.getBoundingClientRect().top); - const countTop = await count.evaluate((el) => el.getBoundingClientRect().top); + await expect(page.getByTestId("channel-folded")).toHaveCount(0); // Keep nudging until the grid is tall enough to actually scroll (it may still be filling in). await expect .poll(async () => { - await page.getByRole("main").evaluate((el) => el.scrollBy(0, 1500)); + await page.getByRole("main").evaluate((el) => el.scrollBy(0, 600)); return page.getByRole("main").evaluate((el) => el.scrollTop); }) .toBeGreaterThan(400); + const folded = page.getByTestId("channel-folded"); + await expect(folded).toBeVisible(); + await expect(folded.getByTestId("channel-title")).toBeVisible(); + const titleTop = await folded.evaluate((el) => el.getBoundingClientRect().top); + const countTop = await count.evaluate((el) => el.getBoundingClientRect().top); - // The chrome didn't move — banner title and toolbar are in exactly the same place. - expect(await title.evaluate((el) => el.getBoundingClientRect().top)).toBe(titleTop); + await page.getByRole("main").evaluate((el) => el.scrollBy(0, 400)); + expect(await folded.evaluate((el) => el.getBoundingClientRect().top)).toBe(titleTop); expect(await count.evaluate((el) => el.getBoundingClientRect().top)).toBe(countTop); }); }); diff --git a/frontend/src/components/ui/README.md b/frontend/src/components/ui/README.md index 370e65de..5e2b5e7d 100644 --- a/frontend/src/components/ui/README.md +++ b/frontend/src/components/ui/README.md @@ -40,8 +40,8 @@ domain-free. | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | overlays & anchoring | `AnchoredPopover` + `anchoredPopoverHooks` (the popover engine: arrows, typeahead, Escape, anchor-flip), `ActionMenu`, `ContextMenu` + `useContextMenu` (the SAME menu, opened by right-click / long-press / Shift+F10 / a row's `MoreButton` "…" via `buttonProps`; `anchor` for a panel a row opens; `PageMenu` for a page's background menu), `Modal` (+ `ModalFooter`; `initialFocus` by purpose, C4), `Overlay`, `SidePanel`, `Tooltip`, `Toaster` (+ `toastLevels`, `overlayOrder`; `lib/toastDock` docks one control under the stack, in every host), `CommandPalette`, `KeyboardHelp` | | actions & inputs | `Button`, `IconButton`, `Pressable` (`selectable` when its text is a title or a name), `Chip` + `chipStyles`, `DragHandle` + `OrderButtons` / `useOrderItems` (the non-drag reorder, WCAG 2.5.7) (one per button family, docs/DESIGN.md; a raw `