From dc7a0f4de13ee1b9cbf44e020c9da42446367adf Mon Sep 17 00:00:00 2001 From: "Vyctor H. Brzezowski" Date: Mon, 3 Aug 2026 13:51:44 -0300 Subject: [PATCH] fix(home): contain app category scroller on mobile (#3281) Constrain the mobile app-category strip to the available content width while preserving internal horizontal scrolling. Scope the CSS regression contract to the exact nested mobile rule. --- src/__tests__/ui-design-contract.test.ts | 31 ++++++++++++++++++++++-- src/styles.css | 2 ++ 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/src/__tests__/ui-design-contract.test.ts b/src/__tests__/ui-design-contract.test.ts index 411c7930..3f9d710d 100644 --- a/src/__tests__/ui-design-contract.test.ts +++ b/src/__tests__/ui-design-contract.test.ts @@ -19,8 +19,7 @@ function cssRule(css: string, selector: string) { function cssMediaContaining(css: string, query: string, required: readonly string[]) { let start = css.indexOf(`@media ${query}`); while (start >= 0) { - const nextMedia = css.indexOf("@media ", start + 1); - const block = css.slice(start, nextMedia === -1 ? undefined : nextMedia); + const block = balancedCssBlock(css, start, `@media ${query}`); if (required.every((snippet) => block.includes(snippet))) return block; start = css.indexOf(`@media ${query}`, start + 1); } @@ -36,6 +35,22 @@ function cssBlock(css: string, selector: string) { return css.slice(start, end + 2); } +function balancedCssBlock(css: string, start: number, label: string) { + const open = css.indexOf("{", start); + expect(open, `Missing opening brace for ${label}`).toBeGreaterThanOrEqual(0); + + let depth = 0; + for (let index = open; index < css.length; index += 1) { + if (css[index] === "{") depth += 1; + if (css[index] !== "}") continue; + + depth -= 1; + if (depth === 0) return css.slice(start, index + 1); + } + + throw new Error(`Unclosed CSS block for ${label}`); +} + function tokenValue(css: string, selector: string, token: string) { const block = cssBlock(css, selector); const match = block.match(new RegExp(`${token}:\\s*(#[0-9a-fA-F]{6})`)); @@ -385,6 +400,18 @@ describe("restored UI design contract", () => { expect(cssRule(css, ".home-v2-listing-row::before")).toContain("border-radius: 0"); }); + it("keeps the mobile app-category scroller within the page width", () => { + const mobileBlock = cssMediaContaining(styles(), "(max-width: 760px)", [ + ".home-v2-apps-categories {", + ]); + const scroller = mobileBlock.match(/\.home-v2-apps-categories \{([^}]*)\}/)?.[1]; + + expect(scroller, "Missing .home-v2-apps-categories rule").toBeTruthy(); + expect(scroller).toMatch(/width:\s*100%/); + expect(scroller).toMatch(/min-width:\s*0/); + expect(scroller).toMatch(/overflow-x:\s*auto/); + }); + it("requires the restored footer columns and mobile section toggles", () => { const footerSource = footer(); const navSource = navItems(); diff --git a/src/styles.css b/src/styles.css index 440e5b86..b29a20b3 100644 --- a/src/styles.css +++ b/src/styles.css @@ -27851,6 +27851,8 @@ a.home-v2-apps-see-all:focus-visible svg { } .home-v2-apps-categories { + width: 100%; + min-width: 0; flex-wrap: nowrap; overflow-x: auto; overflow-y: hidden;