fix(ui): match Omics header and compact workspace layout

This commit is contained in:
Codex
2026-09-13 15:36:08 +02:00
parent 648434a32e
commit 7d826e46c0
12 changed files with 134 additions and 12 deletions
+26 -1
View File
@@ -103,12 +103,16 @@ async function inspect(page: Page) {
}
for (const width of [390, 1280]) {
test(`full shell keeps the visual revision across language and theme changes at ${width}px`, async ({ page }) => {
test(`full shell keeps the visual revision across language and theme changes at ${width}px`, async ({ page }, testInfo) => {
await page.setViewportSize({ width, height: 1000 });
const writes = await fixtures(page);
await page.goto("/");
const header = page.locator(".thot-full-header");
await expect(header).toBeVisible();
await expect(header).toHaveCSS("background-color", "rgb(203, 51, 59)");
expect(await header.locator(".thot-full-header__brand span").evaluate(el =>
getComputedStyle(el).color === getComputedStyle(el.closest("header")!).color,
)).toBe(true);
await page.evaluate(() => document.fonts.ready);
expect(await page.evaluate(() => [...document.fonts].some(font =>
font.family.includes("Manrope Variable") && font.status === "loaded",
@@ -118,12 +122,33 @@ for (const width of [390, 1280]) {
expect(Math.abs(layout!.x - (width - layout!.x - layout!.width))).toBeLessThan(1);
await admin(page, "Database");
await expect(page.getByRole("heading", { name: "Database management", exact: true })).toBeVisible();
await expect(page.locator(".thot-fleet-ledger__header-status")).toHaveCSS("padding-top", "16px");
await page.getByRole("button", { name: /Working context/ }).click();
const fields = page.locator(".thot-context-fields");
const done = fields.getByRole("button", { name: "Done", exact: true });
await expect(done).toBeVisible();
if (width >= 1280) {
const workspaceBox = await fields.getByRole("combobox", { name: "Workspace", exact: true }).boundingBox();
const modelBox = await fields.getByRole("combobox", { name: "Model", exact: true }).boundingBox();
const doneBox = await done.boundingBox();
expect(Math.abs(workspaceBox!.y - modelBox!.y)).toBeLessThan(1);
expect(Math.abs(modelBox!.y - doneBox!.y)).toBeLessThan(1);
expect(doneBox!.x).toBeGreaterThan(modelBox!.x + modelBox!.width);
for (const name of ["My sessions", "All sessions"]) {
await expect(page.getByRole("tab", { name, exact: true })).toHaveCSS("border-bottom-width", "0px");
}
}
expect(await inspect(page)).toEqual({ overflow: 0, serif: [], tiny: [] });
await page.screenshot({ path: testInfo.outputPath("full-context-light.png"), animations: "disabled" });
await done.click();
await header.getByRole("combobox").selectOption("it");
await expect(page.getByRole("heading", { name: "Gestione database", exact: true })).toBeVisible();
await expect(page.locator(".thot-administration-eyebrow")).toHaveText("Amministrazione");
await expect(page.locator(".thot-context-summary small")).toHaveText("Contesto di lavoro");
await page.getByRole("button", { name: "Usa il tema scuro", exact: true }).click();
await expect(page.locator(".thot-host")).toHaveAttribute("data-theme", "dark");
await expect(header).toHaveCSS("background-color", "rgb(203, 51, 59)");
await page.screenshot({ path: testInfo.outputPath("full-header-dark.png"), animations: "disabled" });
expect(await inspect(page)).toEqual({ overflow: 0, serif: [], tiny: [] });
await page.reload();
await expect(page.getByRole("heading", { name: "Gestione database", exact: true })).toBeVisible();
+25 -3
View File
@@ -230,6 +230,9 @@ body.thot-full-document { margin: 0; }
}
.thot-host > .thot-context-layout { flex: 1; min-height: 0; height: auto; }
.thot-full-header {
/* Match Omics static/css/gsd-theme.css --gsd-red-primary in both themes. */
--thot-header-background: #cb333b;
--thot-header-foreground: oklch(99.85% 0.0006 17.2);
display: flex;
flex: none;
flex-wrap: wrap;
@@ -238,13 +241,32 @@ body.thot-full-document { margin: 0; }
gap: 0.5rem 1rem;
min-height: 3.75rem;
padding: 0.625rem var(--thot-shell-gutter);
border-bottom: 1px solid oklch(var(--border));
background: oklch(var(--card));
border-bottom: 1px solid var(--thot-header-background);
background: var(--thot-header-background);
color: var(--thot-header-foreground);
}
.thot-full-header__brand { font: 600 1.25rem/1.2 var(--font-heading); }
.thot-full-header__controls { display: flex; align-items: center; flex-wrap: wrap; gap: 0.25rem; min-width: 0; }
.thot-full-header__user { max-width: 15rem; min-width: 0; font-size: var(--text-control); }
.thot-full-header__error { flex-basis: 100%; font-size: var(--text-control); color: oklch(var(--destructive)); }
.thot-full-header__error { flex-basis: 100%; font-size: var(--text-control); color: inherit; }
.thot-full-header__controls button { color: var(--thot-header-foreground); }
.thot-full-header__controls button:is(:hover, [aria-expanded="true"]) {
color: var(--thot-header-foreground);
background: color-mix(in srgb, var(--thot-header-foreground) 14%, transparent);
}
.thot-full-header :is(button, select):focus-visible {
outline: 2px solid var(--thot-header-foreground);
outline-offset: 2px;
}
.thot-full-header .thot-shell-select {
color: var(--thot-header-foreground);
background: transparent;
border-color: color-mix(in srgb, var(--thot-header-foreground) 65%, transparent);
}
.thot-full-header .thot-shell-select option {
color: oklch(var(--foreground));
background: oklch(var(--card));
}
.thot-shell-select {
min-height: 2rem;
max-width: 9rem;
@@ -128,6 +128,8 @@ test("option A has one collapsible global context and preserves the Core draft a
await userEvent.click(screen.getByRole("button", { name: /Working context/ }));
expect(screen.getAllByRole("combobox", { name: "Workspace" })).toHaveLength(1);
expect(screen.getAllByRole("combobox", { name: "Model" })).toHaveLength(1);
expect(screen.getByRole("button", { name: "Done" }).closest(".thot-context-fields"))
.toBe(screen.getByRole("combobox", { name: "Model" }).closest(".thot-context-fields"));
await userEvent.selectOptions(screen.getByRole("combobox", { name: "Workspace" }), "beta");
expect(workspacePreferences.load()).toMatchObject({ workspaceId: "beta", provider: "test", model: "test" });
await userEvent.click(screen.getByRole("button", { name: "Done" }));
@@ -190,6 +190,8 @@ test("administrators can explicitly switch to all sessions and see owners", asyn
expect(mySessions).toHaveAttribute("tabindex", "0");
expect(mySessions).toHaveAttribute("data-tab-state", "active");
expect(mySessions).toHaveClass("bg-[oklch(var(--nav-active))]");
expect(mySessions).toHaveClass("border-b-0");
expect(allSessions).toHaveClass("border-b-0");
expect(allSessions).toHaveAttribute("aria-selected", "false");
expect(allSessions).toHaveAttribute("aria-controls", "session-scope-panel");
expect(allSessions).toHaveAttribute("tabindex", "-1");
+1 -1
View File
@@ -61,7 +61,7 @@ interface AppShellProps {
const SESSION_SCOPES: readonly SessionScope[] = ["mine", "all"];
function sessionScopeTabClass(selected: boolean): string {
const base = "relative -mb-px flex h-8 w-full cursor-pointer items-center justify-center rounded-t-md border border-b-2 px-2 text-xs font-semibold tracking-[0.005em] outline-none focus-visible:z-10 focus-visible:ring-3 focus-visible:ring-[oklch(var(--nav-active-border)/0.28)]";
const base = "relative -mb-px flex h-8 w-full cursor-pointer items-center justify-center rounded-t-md border border-b-0 px-2 text-xs font-semibold tracking-[0.005em] outline-none focus-visible:z-10 focus-visible:ring-3 focus-visible:ring-[oklch(var(--nav-active-border)/0.28)]";
return selected
? `${base} border-[oklch(var(--nav-active-border))] bg-[oklch(var(--nav-active))] text-[oklch(var(--nav-active-foreground))] hover:bg-[oklch(var(--nav-active-hover))]`
: `${base} border-border bg-card text-muted-foreground hover:border-muted-foreground/45 hover:bg-muted/55 hover:text-foreground`;
+3 -3
View File
@@ -10,13 +10,13 @@
.thot-context-trigger[aria-expanded=true] .thot-context-action > svg:last-child { transform: rotate(180deg); }
.thot-context-body { margin: 0 var(--thoth-content-gutter) var(--thoth-content-gutter); padding: var(--thoth-content-padding); background: oklch(var(--card)); }
.thot-context-body[hidden] { display: none; }
.thot-context-fields { display: grid; grid-template-columns: repeat(2,minmax(0,1fr)); gap: 24px; }
.thot-context-fields { display: grid; grid-template-columns: repeat(2,minmax(0,1fr)) auto; align-items: end; gap: 16px; }
.thot-context-fields label { display: grid; min-width: 0; gap: 8px; font-size: var(--text-control); }
.thot-context-fields label > span { display: flex; align-items: center; gap: 8px; font-weight: 600; }
.thot-context-fields select { min-width: 0; width: 100%; padding: 9px 12px; border: 1px solid oklch(var(--border)); border-radius: 6px; background: oklch(var(--card)); }
.thot-context-fields select { min-width: 0; width: 100%; height: 32px; padding: 4px 12px; border: 1px solid oklch(var(--border)); border-radius: 6px; background: oklch(var(--card)); }
.thot-context-body > p[role] { color: oklch(var(--muted-foreground)); font-size: .75rem; line-height: 1.6; }
.thot-context-body > p[role] { margin-top: 12px; }
.thot-context-footer { display: flex; justify-content: flex-end; gap: 8px; margin-top: 12px; }
.thot-context-actions { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: 8px; }
.thot-context-shelf :is(button,select):focus-visible { outline: 2px solid oklch(var(--ring)); outline-offset: 3px; }
.thot-context-shelf select:disabled { opacity: .65; }
@container working-context (max-width: 38rem) {
+1 -1
View File
@@ -31,11 +31,11 @@ export function WorkingContextShelf({ context, locked, onWorkspace, onModel }: {
<option value="" disabled>{translate("Choose a model")}</option>{context.models.map(model => <option key={`${model.provider}/${model.id}`} value={`${model.provider}/${model.id}`}>{model.name}</option>)}
</select>
</label>
<div className="thot-context-actions">{context.error && <Button variant="outline" onClick={context.retry}>{translate("Retry context")}</Button>}{context.ready && <Button variant="outline" onClick={() => setExpanded(false)}>{translate("Done")}</Button>}</div>
</div>
{locked && <p role="status">{translate("Workspace and model are locked until the current operation finishes.")}</p>}
{context.ready && context.error && <p role="status">{translate("Context refresh failed. Your current choices and unsaved work have been retained.")}</p>}
{!context.ready && <p role="status">{context.loading ? translate("Loading working context…") : context.error ? translate("Working context is unavailable. Retry or ask the installation operator to check configuration.") : translate("Select an available workspace and model to enable Core and Administration. Unavailable saved choices are not replaced automatically.")}</p>}
<div className="thot-context-footer">{context.error && <Button variant="outline" onClick={context.retry}>{translate("Retry context")}</Button>}{context.ready && <Button variant="outline" onClick={() => setExpanded(false)}>{translate("Done")}</Button>}</div>
</div>
</section>;
}
@@ -105,6 +105,7 @@
grid-area: status;
min-width: 0;
padding-inline: var(--fleet-summary-inset);
padding-top: 1rem;
}
.thot-fleet-ledger .thot-administration-header,
+1 -1
View File
@@ -38,7 +38,7 @@ export function FullHeader({ onLogout }: { onLogout?: () => Promise<void> }) {
return (
<header className="thot-full-header" aria-label={t("Application header")}>
<span className="thot-full-header__brand">Thoth<span className="text-primary">II</span></span>
<span className="thot-full-header__brand">Thoth<span>II</span></span>
<div className="thot-full-header__controls">
<label className="sr-only" htmlFor="thot-ui-language">{t("Interface language")}</label>
<select id="thot-ui-language" className="thot-shell-select" value={shell.locale} onChange={event => shell.changeLocale(event.target.value)}>