diff --git a/config/architecture-inventory.json b/config/architecture-inventory.json index 96315d0..436e91e 100644 --- a/config/architecture-inventory.json +++ b/config/architecture-inventory.json @@ -372,8 +372,8 @@ "css_layers": [ "/shared/tokens.css?v=20260820-2", "/shared/base.css?v=20260806-1", - "/shared/shell.css?v=20260820-5", - "/shared/auth.css?v=20260820-4", + "/shared/shell.css?v=20260820-6", + "/shared/auth.css?v=20260820-5", "/shared/components/controls.css?v=20260820-2", "/shared/components/navigation.css?v=20260820-1", "/shared/components/cards.css?v=20260820-1", @@ -451,8 +451,8 @@ }, { "path": "frontend/shared/shell.css", - "bytes": 63204, - "lines": 3761 + "bytes": 63245, + "lines": 3762 }, { "path": "backend/features/heaven/engine.py", diff --git a/frontend/index.html b/frontend/index.html index 1576c9b..1e9cee1 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -19,8 +19,8 @@ - - + + diff --git a/frontend/shared/auth.css b/frontend/shared/auth.css index 602d3ac..424d982 100644 --- a/frontend/shared/auth.css +++ b/frontend/shared/auth.css @@ -472,11 +472,11 @@ } .account-button > span { - min-width: 0px; + flex: 0 0 auto; - overflow: hidden; + min-width: max-content; - text-overflow: ellipsis; + overflow: visible; white-space: nowrap; } @@ -1028,11 +1028,15 @@ button.account-role-badge:focus-visible { display: grid; - grid-template-columns: auto minmax(0px, 1fr); + grid-template-columns: minmax(0px, 1fr); } .account-menu-shell .account-button { + width: 100%; + min-width: 0px; + + max-width: none; } .account-dropdown { @@ -1179,7 +1183,7 @@ button.account-role-badge:focus-visible { gap: 3px; } -@media (min-width: 1280px) { +@media (min-width: 1280px) and (max-width: 1439px) { .app-header .header-command-group .account-role-badge span { display: none; } @@ -1191,17 +1195,25 @@ button.account-role-badge:focus-visible { padding-inline: 6px; } +} +@media (min-width: 1280px) { .header-command-group .account-button { + flex: 0 0 auto; + width: auto; - max-width: 108px; + max-width: none; + + overflow: visible; padding: 0px 8px; } .header-command-group .account-button > span { display: inline; + + overflow: visible; } } @@ -1264,11 +1276,11 @@ button.account-role-badge:focus-visible { } .account-button { - overflow: hidden; + flex: 0 0 auto; - text-overflow: ellipsis; + overflow: visible; - max-width: 108px; + max-width: none; min-height: 28px; diff --git a/frontend/shared/shell.css b/frontend/shared/shell.css index 3250d92..8038db0 100644 --- a/frontend/shared/shell.css +++ b/frontend/shared/shell.css @@ -2396,7 +2396,7 @@ body.sidebar-collapsed .status-bar { overflow: hidden; } - body:is([data-active-view="sentimentCycleView"], [data-active-view="yesterdayView"]) .overview-strip { + body:is([data-active-view="sentimentCycleView"], [data-active-view="yesterdayView"]) .app-main .overview-strip { flex: 0 0 auto; } @@ -2412,7 +2412,7 @@ body.sidebar-collapsed .status-bar { overflow: hidden; } - body:is([data-active-view="auctionView"], [data-active-view="themeLibraryView"], [data-active-view="popularityView"], [data-active-view="mentorView"], [data-active-view="rotationView"]) .overview-strip { + body:is([data-active-view="auctionView"], [data-active-view="themeLibraryView"], [data-active-view="popularityView"], [data-active-view="mentorView"], [data-active-view="rotationView"]) .app-main .overview-strip { flex: 0 0 auto; } @@ -3009,6 +3009,7 @@ body.sidebar-collapsed .status-bar { body.mobile-shell .header-command-group > .command-button, body.mobile-shell .header-command-group .account-button { width: 100%; + max-width: none; min-height: var(--mobile-touch-size); justify-content: flex-start; } diff --git a/tests/e2e/app-shell.spec.js b/tests/e2e/app-shell.spec.js index ed42c9b..9fc9bbe 100644 --- a/tests/e2e/app-shell.spec.js +++ b/tests/e2e/app-shell.spec.js @@ -517,6 +517,49 @@ async function openHeaderCommandMenu(page) { await expect(menu).toBeVisible(); } +function namedSession(username, role = "admin", subscribed = true) { + const auth = session(role, subscribed); + auth.user.username = username; + return auth; +} + +async function setColorTheme(page, theme) { + const wanted = theme === "night" ? "dark" : "light"; + const current = await page.locator("html").getAttribute("data-theme"); + if (current === wanted) return; + await page.locator("#themeToggle").click(); + await expect(page.locator("html")).toHaveAttribute("data-theme", wanted); +} + +function measureAccountName() { + const name = document.querySelector("#accountName"); + const button = document.querySelector("#accountButton"); + const badges = document.querySelector("#accountRoleBadges"); + const adminLabel = document.querySelector("#accountAdminBadge > span"); + const vipLabel = document.querySelector("#accountVipLabel"); + const box = (node) => node ? node.getBoundingClientRect() : null; + const visible = (node) => { + if (!node || node.hidden) return false; + const style = getComputedStyle(node); + if (style.display === "none" || style.visibility === "hidden") return false; + const rect = node.getBoundingClientRect(); + return rect.width > 0 && rect.height > 0; + }; + const buttonBox = box(button); + const badgeBox = box(badges); + return { + text: name ? String(name.textContent || "") : "", + clientWidth: name ? name.clientWidth : 0, + scrollWidth: name ? name.scrollWidth : 0, + fits: name ? name.clientWidth >= name.scrollWidth : false, + buttonFits: button ? button.clientWidth >= button.scrollWidth : false, + stacked: Boolean(badgeBox && buttonBox && buttonBox.top >= badgeBox.bottom - 1), + adminLabelVisible: visible(adminLabel), + vipLabelVisible: visible(vipLabel), + documentOverflow: document.documentElement.scrollWidth - window.innerWidth, + }; +} + test("admin shell opens every primary workspace and global search", async ({ page }) => { await mockApplication(page); await page.goto("/index.html"); @@ -3418,6 +3461,7 @@ test("desktop header keeps refresh, admin commands and account identity visible" refreshVisible: visible(refresh), accountVisible: visible(account), nameVisible: visible(name), + nameFits: name ? name.clientWidth >= name.scrollWidth : false, }; }); @@ -3441,6 +3485,7 @@ test("desktop header keeps refresh, admin commands and account identity visible" expect(geometry.documentOverflow, `${viewport.width} document overflow`).toBeLessThanOrEqual(1); expect(geometry.overlap, `${viewport.width} tape overlap`).toBe(0); expect(geometry.tapeHeight, `${viewport.width} tape height`).toBeGreaterThan(20); + expect(geometry.nameFits, `${viewport.width} account name truncated`).toBe(true); await page.locator("#refreshButton").click(); await expect(page.locator("#loadingOverlay")).toBeHidden(); await page.locator("#settingsButton").click(); @@ -3477,6 +3522,9 @@ test("desktop header keeps refresh, admin commands and account identity visible" await expect(page.locator("#accountVipBadge")).toBeVisible(); await expect(page.locator("#accountButton")).toBeVisible(); await expect(page.locator('[data-mobile-command-target="themeToggle"]')).toBeVisible(); + const mobileName = await page.evaluate(measureAccountName); + expect(mobileName.fits, "390 account name truncated").toBe(true); + expect(mobileName.stacked, "390 identity and account should stack").toBe(true); await shotPage("390-menu-open"); await page.keyboard.press("Escape"); await expect(page.locator("#headerCommandGroup")).toBeHidden(); @@ -3493,3 +3541,72 @@ test("desktop header keeps refresh, admin commands and account identity visible" await shot("1600-user-day"); expect(pageErrors).toEqual([]); }); + +test("header username stays untruncated on desktop and in the mobile menu", async ({ page }) => { + test.setTimeout(120_000); + const fs = require("node:fs"); + const path = require("node:path"); + const shotDir = path.join(__dirname, "../../runtime/b217-shots"); + fs.mkdirSync(shotDir, { recursive: true }); + const measurements = []; + const viewports = [ + { width: 1600, height: 1000 }, + { width: 1440, height: 900 }, + { width: 1280, height: 800 }, + { width: 390, height: 844 }, + ]; + const accounts = [ + { label: "admin", username: "review_admin", auth: namedSession("review_admin", "admin", true) }, + { label: "user", username: "normal_user", auth: namedSession("normal_user", "user", false) }, + ]; + + for (const account of accounts) { + await mockApplication(page, account.auth); + await page.goto("/index.html"); + await expect(page.locator("#accountName")).toHaveText(account.username); + + for (const theme of ["day", "night"]) { + await page.setViewportSize({ width: 1600, height: 1000 }); + await setColorTheme(page, theme); + for (const viewport of viewports) { + await page.setViewportSize(viewport); + if (viewport.width === 390) { + await page.locator("#headerMenuButton").click(); + await expect(page.locator("#headerCommandGroup")).toBeVisible(); + } else { + await expect(page.locator("#headerMenuButton")).toBeHidden(); + await expect(page.locator("#accountName")).toBeVisible(); + } + const geometry = await page.evaluate(measureAccountName); + const label = `${account.label}-${viewport.width}-${theme}`; + expect(geometry.text, `${label} username`).toBe(account.username); + expect(geometry.fits, `${label} accountName truncated ${geometry.clientWidth}<${geometry.scrollWidth}`).toBe(true); + expect(geometry.documentOverflow, `${label} document overflow`).toBeLessThanOrEqual(1); + if (viewport.width === 390) { + expect(geometry.stacked, `${label} identity/account not stacked`).toBe(true); + } + if (viewport.width >= 1440 && account.label === "admin") { + expect(geometry.adminLabelVisible, `${label} admin badge text hidden`).toBe(true); + expect(geometry.vipLabelVisible, `${label} vip badge text hidden`).toBe(true); + } + measurements.push({ + account: account.username, + theme, + width: viewport.width, + height: viewport.height, + ...geometry, + }); + const shotName = `${account.label}-${viewport.width}-${theme}.png`; + if (viewport.width === 390) { + await page.screenshot({ path: path.join(shotDir, shotName) }); + await page.keyboard.press("Escape"); + await expect(page.locator("#headerCommandGroup")).toBeHidden(); + } else { + await page.locator(".app-header").screenshot({ path: path.join(shotDir, shotName) }); + } + } + } + } + + fs.writeFileSync(path.join(shotDir, "measurements.json"), `${JSON.stringify(measurements, null, 2)}\n`); +});