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`);
+});