fix(ui): match Omics header and compact workspace layout
This commit is contained in:
@@ -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
@@ -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");
|
||||
|
||||
@@ -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`;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)}>
|
||||
|
||||
Reference in New Issue
Block a user