diff --git a/config/architecture-inventory.json b/config/architecture-inventory.json
index 436e91e..eb229b1 100644
--- a/config/architecture-inventory.json
+++ b/config/architecture-inventory.json
@@ -370,9 +370,9 @@
}
],
"css_layers": [
- "/shared/tokens.css?v=20260820-2",
+ "/shared/tokens.css?v=20260820-3",
"/shared/base.css?v=20260806-1",
- "/shared/shell.css?v=20260820-6",
+ "/shared/shell.css?v=20260820-8",
"/shared/auth.css?v=20260820-5",
"/shared/components/controls.css?v=20260820-2",
"/shared/components/navigation.css?v=20260820-1",
@@ -451,8 +451,8 @@
},
{
"path": "frontend/shared/shell.css",
- "bytes": 63245,
- "lines": 3762
+ "bytes": 63550,
+ "lines": 3757
},
{
"path": "backend/features/heaven/engine.py",
diff --git a/frontend/index.html b/frontend/index.html
index 1e9cee1..d4306a9 100644
--- a/frontend/index.html
+++ b/frontend/index.html
@@ -17,9 +17,9 @@
document.documentElement.style.colorScheme = theme;
})();
-
+
-
+
diff --git a/frontend/shared/shell.css b/frontend/shared/shell.css
index 8038db0..0de2241 100644
--- a/frontend/shared/shell.css
+++ b/frontend/shared/shell.css
@@ -2280,9 +2280,9 @@ body.sidebar-collapsed .status-bar {
align-items: center;
- gap: 24px;
+ gap: var(--header-cluster-gap);
- padding: 0 20px;
+ padding: 0 var(--header-pad-x);
height: var(--topbar-height);
}
@@ -3217,8 +3217,8 @@ body.sidebar-collapsed .status-bar {
/* Generic page title slot in the shell header. Kept hidden so each page owns
its own in-view title header (mentor renders one inside #mentorView). */
.app-page-context {
- min-width: 148px;
- max-width: 280px;
+ min-width: var(--header-page-context-min);
+ max-width: var(--header-page-context-max);
flex: 0 1 auto;
flex-direction: column;
justify-content: center;
@@ -3227,6 +3227,19 @@ body.sidebar-collapsed .status-bar {
display: flex;
}
+.app-header .app-page-context,
+.main .app-header .app-page-context {
+ min-width: var(--header-page-context-min);
+ max-width: var(--header-page-context-max);
+ overflow: hidden;
+}
+
+.main .app-header .app-page-context span {
+ overflow: hidden;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+}
+
#themeModeText {
display: none;
}
@@ -3255,11 +3268,11 @@ body.sidebar-collapsed .status-bar {
.app-header .overview-strip[data-overview-expanded="true"] {
position: static;
display: flex;
- flex: 1 1 auto;
+ flex: 1 0 auto;
align-items: stretch;
width: auto;
height: auto;
- min-width: 0;
+ min-width: max-content;
min-height: 0;
margin: 0;
padding: 0;
@@ -3271,22 +3284,29 @@ body.sidebar-collapsed .status-bar {
.app-header .market-tape {
display: flex;
- flex: 1 1 auto;
+ flex: 1 0 auto;
align-items: center;
- min-width: 0;
+ min-width: max-content;
gap: 0;
- overflow: hidden;
+ overflow: visible;
white-space: nowrap;
}
.tape-metric {
position: relative;
display: flex;
+ flex: 0 0 auto;
flex-direction: column;
justify-content: center;
gap: 2px;
- min-width: 0;
- padding: 0 16px;
+ min-width: max-content;
+ padding: 0 var(--header-tape-metric-pad-x);
+}
+
+.app-header .tape-metric,
+.app-header .tape-sentiment.sentiment-block {
+ flex: 0 0 auto;
+ min-width: max-content;
}
.tape-metric + .tape-metric::before,
@@ -3311,7 +3331,7 @@ body.sidebar-collapsed .status-bar {
display: flex;
align-items: center;
gap: 5px;
- overflow: hidden;
+ overflow: visible;
color: var(--text-1);
font-size: 15px;
font-weight: 600;
@@ -3400,7 +3420,7 @@ body.sidebar-collapsed .status-bar {
gap: 4px;
height: 28px;
min-height: 28px;
- margin-left: 14px;
+ margin-left: var(--header-tape-toggle-gap);
padding: 0 10px;
border: 0;
border-radius: var(--radius-md);
@@ -3473,7 +3493,7 @@ body.sidebar-collapsed .status-bar {
display: flex;
flex: 0 0 auto;
align-items: center;
- gap: 6px;
+ gap: var(--header-action-gap);
margin-left: 0;
overflow: visible;
}
@@ -3601,31 +3621,6 @@ body.sidebar-collapsed .status-bar {
}
}
-@media (min-width: 1280px) {
- .tape-metric {
- padding: 0 10px;
- }
-
- .app-header {
- gap: 16px;
- }
-}
-
-@media (min-width: 1280px) and (max-width: 1599px) {
- .app-page-context {
- min-width: 96px;
- max-width: 128px;
- }
-
- .app-header .header-actions {
- gap: 4px;
- }
-
- .app-header .header-command-group {
- gap: 4px;
- }
-}
-
@media (max-width: 1440px) {
.app-page-context {
display: none;
@@ -3711,7 +3706,7 @@ body.sidebar-collapsed .status-bar {
flex: 0 0 auto;
flex-direction: row;
align-items: center;
- gap: 6px;
+ gap: var(--header-command-gap);
width: auto;
max-width: none;
padding: 0;
diff --git a/frontend/shared/tokens.css b/frontend/shared/tokens.css
index 0fec78e..5391ed9 100644
--- a/frontend/shared/tokens.css
+++ b/frontend/shared/tokens.css
@@ -182,6 +182,14 @@
--sidebar-width: var(--size-sidebar);
--sidebar-compact-width: 64px;
--topbar-height: var(--size-topbar);
+ --header-pad-x: 20px;
+ --header-cluster-gap: 24px;
+ --header-action-gap: 6px;
+ --header-command-gap: 6px;
+ --header-tape-metric-pad-x: 16px;
+ --header-tape-toggle-gap: 14px;
+ --header-page-context-min: 148px;
+ --header-page-context-max: 280px;
--summary-height: var(--size-summary);
--statusbar-height: var(--size-statusbar);
--page-pad-y: var(--size-page-pad-y);
@@ -517,3 +525,16 @@
--shadow-soft: 0 1px 2px rgba(0, 0, 0, .28), 0 8px 24px rgba(0, 0, 0, .16);
--shadow: 0 18px 50px rgba(0, 0, 0, .46);
}
+
+@media (min-width: 1280px) and (max-width: 1919px) {
+ :root {
+ --header-pad-x: 12px;
+ --header-cluster-gap: 8px;
+ --header-action-gap: 4px;
+ --header-command-gap: 4px;
+ --header-tape-metric-pad-x: 6px;
+ --header-tape-toggle-gap: 4px;
+ --header-page-context-min: 96px;
+ --header-page-context-max: 148px;
+ }
+}
diff --git a/tests/e2e/app-shell.spec.js b/tests/e2e/app-shell.spec.js
index 9fc9bbe..8364d6f 100644
--- a/tests/e2e/app-shell.spec.js
+++ b/tests/e2e/app-shell.spec.js
@@ -3610,3 +3610,127 @@ test("header username stays untruncated on desktop and in the mobile menu", asyn
fs.writeFileSync(path.join(shotDir, "measurements.json"), `${JSON.stringify(measurements, null, 2)}\n`);
});
+
+function measureHeaderProbe() {
+ const name = document.querySelector("#accountName");
+ const group = document.querySelector("#headerCommandGroup");
+ const strip = document.querySelector(".app-header .overview-strip");
+ const stripBox = strip ? strip.getBoundingClientRect() : null;
+ const stripShown = Boolean(stripBox && stripBox.width > 0 && stripBox.height > 0);
+ const tapeClip = stripShown
+ ? [...strip.querySelectorAll("*")].filter((el) => (
+ el.children.length === 0 && el.scrollWidth > el.clientWidth + 1
+ )).map((el) => (el.textContent || "").trim()).filter(Boolean)
+ : [];
+ const groupBox = group ? group.getBoundingClientRect() : null;
+ const nameBox = name ? name.getBoundingClientRect() : null;
+ return {
+ groupRight: groupBox ? groupBox.right : 0,
+ nameRight: nameBox ? nameBox.right : 0,
+ nameFits: name ? name.clientWidth >= name.scrollWidth : false,
+ nameText: name ? String(name.textContent || "") : "",
+ tapeClip,
+ documentOverflow: document.documentElement.scrollWidth - window.innerWidth,
+ viewport: window.innerWidth,
+ };
+}
+
+async function applyColorTheme(page, theme) {
+ const wanted = theme === "night" ? "dark" : "light";
+ await page.evaluate((value) => {
+ document.documentElement.dataset.theme = value;
+ document.documentElement.style.colorScheme = value;
+ try { localStorage.setItem("xiaobaiTheme", value); } catch (_error) {}
+ }, wanted);
+ await expect(page.locator("html")).toHaveAttribute("data-theme", wanted);
+}
+
+async function probeHeaderWorkspaces(page, { account, theme, viewport, views, measurements }) {
+ await page.setViewportSize(viewport);
+ await applyColorTheme(page, theme);
+ for (const view of views) {
+ await page.locator(`[data-view="${view}"]`).first().click();
+ await expect(page.locator(`#${view}`)).toHaveClass(/active-view/);
+ const geometry = await page.evaluate(measureHeaderProbe);
+ const label = `${account.label}-${view}-${viewport.width}-${theme}`;
+ expect(geometry.nameText, `${label} username`).toBe(account.username);
+ expect(geometry.nameFits, `${label} accountName truncated`).toBe(true);
+ expect(geometry.groupRight, `${label} command group overflow ${geometry.groupRight}>${geometry.viewport}`).toBeLessThanOrEqual(geometry.viewport + 0.5);
+ expect(geometry.nameRight, `${label} accountName overflow ${geometry.nameRight}>${geometry.viewport}`).toBeLessThanOrEqual(geometry.viewport + 0.5);
+ expect(geometry.documentOverflow, `${label} document overflow`).toBeLessThanOrEqual(1);
+ expect(geometry.tapeClip, `${label} tape clipped ${geometry.tapeClip.join("|")}`).toEqual([]);
+ measurements.push({ account: account.username, view, theme, width: viewport.width, ...geometry });
+ }
+}
+
+test("desktop header keeps commands in view and tape text unclipped across workspaces", async ({ page }) => {
+ test.setTimeout(120_000);
+ const fs = require("node:fs");
+ const path = require("node:path");
+ const shotDir = path.join(__dirname, "../../runtime/b221-shots");
+ fs.mkdirSync(shotDir, { recursive: true });
+ const measurements = [];
+ const views = [
+ "sentimentCycleView",
+ "auctionView",
+ "themeLibraryView",
+ "popularityView",
+ "rotationView",
+ "mentorView",
+ "screenerView",
+ "reviewWorkspaceView",
+ "heavenView",
+ ];
+ const viewports = [
+ { width: 1600, height: 1000 },
+ { width: 1440, height: 900 },
+ { width: 1280, height: 800 },
+ ];
+ 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 viewport of viewports) {
+ await probeHeaderWorkspaces(page, { account, theme: "day", viewport, views, measurements });
+ await page.locator('[data-view="themeLibraryView"]').first().click();
+ await page.locator(".app-header").screenshot({
+ path: path.join(shotDir, `${account.label}-${viewport.width}-day.png`),
+ });
+ }
+ await probeHeaderWorkspaces(page, {
+ account,
+ theme: "night",
+ viewport: { width: 1600, height: 1000 },
+ views,
+ measurements,
+ });
+ await page.locator('[data-view="sentimentCycleView"]').first().click();
+ await page.locator(".app-header").screenshot({
+ path: path.join(shotDir, `${account.label}-1600-night.png`),
+ });
+ }
+
+ await mockApplication(page, accounts[0].auth);
+ await page.goto("/index.html");
+ await page.setViewportSize({ width: 390, height: 844 });
+ await applyColorTheme(page, "day");
+ await page.locator("#headerMenuButton").click();
+ await expect(page.locator("#headerCommandGroup")).toBeVisible();
+ const mobileDay = await page.evaluate(measureAccountName);
+ expect(mobileDay.fits, "390 day account name truncated").toBe(true);
+ expect(mobileDay.stacked, "390 day identity and account should stack").toBe(true);
+ await page.screenshot({ path: path.join(shotDir, "admin-390-day.png") });
+ await page.keyboard.press("Escape");
+ await applyColorTheme(page, "night");
+ await page.locator("#headerMenuButton").click();
+ const mobileNight = await page.evaluate(measureAccountName);
+ expect(mobileNight.fits, "390 night account name truncated").toBe(true);
+ expect(mobileNight.stacked, "390 night identity and account should stack").toBe(true);
+ await page.screenshot({ path: path.join(shotDir, "admin-390-night.png") });
+ fs.writeFileSync(path.join(shotDir, "measurements.json"), `${JSON.stringify(measurements, null, 2)}\n`);
+});