diff --git a/src/components/ai-edition/v4/EditorShellV4.module.css b/src/components/ai-edition/v4/EditorShellV4.module.css index cae243f28..ee3d11fab 100644 --- a/src/components/ai-edition/v4/EditorShellV4.module.css +++ b/src/components/ai-edition/v4/EditorShellV4.module.css @@ -143,7 +143,13 @@ position: absolute; top: calc(100% + 6px); left: 0; + /* Sized to its widest row. Left to the default shrink-to-fit, an absolute box measures its + available width against the anchor, which is only as wide as the wordmark, so it settled + on min-width and long locales wrapped: "Changer de langue" on two lines, the version split + as "2.0.0-" / "rc.8" (#969). The max-width only keeps it inside the window. */ + width: max-content; min-width: 232px; + max-width: calc(100vw - 24px); /* The trigger sits 13px from the top of a 58px bar, so the menu's first 7px overlap the titlebar's drag region. Only `.topbar button` is opted out of dragging, and that strip is the menu's padding, not a row — without this a mousedown on it drags the window out from @@ -170,6 +176,8 @@ cursor: pointer; text-align: left; font: 500 12.5px var(--font-body); + /* One line per row, in every locale: the menu grows to fit instead (see .appMenu). */ + white-space: nowrap; transition: background var(--motion-fast) var(--ease), color var(--motion-fast) var(--ease); } .appMenuRow:hover, @@ -188,6 +196,9 @@ /* Pushes the version to the right edge of the row it shares with "About". */ .appMenuVersion { margin-left: auto; + /* Never squeezed or broken at the hyphen of a pre-release tag like "2.0.0-rc.8". */ + flex-shrink: 0; + white-space: nowrap; color: var(--muted); font-variant-numeric: tabular-nums; font-size: 11.5px; diff --git a/src/components/ai-edition/v4/EditorTopBar.test.tsx b/src/components/ai-edition/v4/EditorTopBar.test.tsx index f5239a21a..f357f1121 100644 --- a/src/components/ai-edition/v4/EditorTopBar.test.tsx +++ b/src/components/ai-edition/v4/EditorTopBar.test.tsx @@ -1,5 +1,7 @@ // @vitest-environment jsdom import "@testing-library/jest-dom"; +import { readFileSync } from "node:fs"; +import path from "node:path"; import { act, fireEvent, render, screen, waitFor } from "@testing-library/react"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { TooltipProvider } from "@/components/ui/tooltip"; @@ -447,3 +449,29 @@ describe("EditorTopBar responsive affordances and tooltips", () => { expect(screen.queryByRole("button", { name: "topbar.changeLanguage" })).not.toBeInTheDocument(); }); }); + +// jsdom does not lay anything out, so it cannot show a row wrapping. What it can pin is the +// sizing contract that stops it: in French the menu used to settle on its min-width, wrapping +// "Changer de langue" and splitting the version as "2.0.0-" / "rc.8" (#969). +describe("AppMenu sizing (issue #969)", () => { + const css = readFileSync(path.join(__dirname, "EditorShellV4.module.css"), "utf8"); + const rule = (selector: string) => { + const body = css.match(new RegExp(`\\n\\.${selector}\\s*\\{([^}]*)\\}`))?.[1]; + expect(body, selector).toBeDefined(); + return body ?? ""; + }; + + it("sizes the menu to its widest row", () => { + expect(rule("appMenu")).toMatch(/^\s*width:\s*max-content;/m); + }); + + it("keeps every row on one line", () => { + expect(rule("appMenuRow")).toMatch(/white-space:\s*nowrap/); + }); + + it("never squeezes or breaks the trailing value", () => { + const version = rule("appMenuVersion"); + expect(version).toMatch(/white-space:\s*nowrap/); + expect(version).toMatch(/flex-shrink:\s*0/); + }); +});