fix(ui): match Omics header and compact workspace layout
This commit is contained in:
@@ -160,6 +160,13 @@ users can scan structure without adding nested containers.
|
|||||||
|
|
||||||
## Colors
|
## Colors
|
||||||
|
|
||||||
|
The full-mode application header matches Omics Portal's `--gsd-red-primary`
|
||||||
|
(`#CB333B`) in both themes. Its complete wordmark, including `II`, and controls
|
||||||
|
use a near-white foreground. This header is absent in embedded mode. The sidebar
|
||||||
|
and welcome wordmarks retain their red suffix. Context editing places workspace,
|
||||||
|
model and Done in one desktop row, stacking on narrow containers. Session-scope
|
||||||
|
tabs retain their selected fill and accessible keyboard state without a bottom border.
|
||||||
|
|
||||||
The palette combines warm porcelain surfaces, warm graphite text, and an instrument red used only
|
The palette combines warm porcelain surfaces, warm graphite text, and an instrument red used only
|
||||||
for action, focus, and important state. OKLCH values in the frontmatter are normative because the
|
for action, focus, and important state. OKLCH values in the frontmatter are normative because the
|
||||||
frontend uses OKLCH tokens directly.
|
frontend uses OKLCH tokens directly.
|
||||||
@@ -311,13 +318,13 @@ default, hover, focus, active, disabled, loading, and error behavior where those
|
|||||||
width; a Navigation button opens the shared accessible dialog. Selecting another archive
|
width; a Navigation button opens the shared accessible dialog. Selecting another archive
|
||||||
page or pressing Escape closes it. Desktop retains the right session sidebar and its My sessions /
|
page or pressing Escape closes it. Desktop retains the right session sidebar and its My sessions /
|
||||||
All sessions tabs. Core retains question/answer, eight phases, reviewer gates and the left log.
|
All sessions tabs. Core retains question/answer, eight phases, reviewer gates and the left log.
|
||||||
The portal owns the red header and left sidebar; ThothII must not duplicate them. Size to the
|
In embedded mode the portal owns the red header and left sidebar; ThothII must not duplicate them. Size to the
|
||||||
actual application container. Narrow session document panels may use the available width.
|
actual application container. Narrow session document panels may use the available width.
|
||||||
|
|
||||||
### Tabs
|
### Tabs
|
||||||
|
|
||||||
- **Shape:** compact label tabs sit on a shared baseline with rounded top corners and a two-pixel
|
- **Shape:** compact label tabs sit on a shared baseline with rounded top corners and a two-pixel
|
||||||
lower edge. Inactive labels retain a complete Quiet Border and Porcelain Card surface, so every
|
lower edge, except session-scope tabs which have no bottom border. Inactive labels retain a Quiet Border and Porcelain Card surface, so every
|
||||||
label reads as a tab before interaction; hover feedback reinforces clickability.
|
label reads as a tab before interaction; hover feedback reinforces clickability.
|
||||||
- **Current:** the selected tab uses the muted Navigation Active red for its fill, text, and defined border.
|
- **Current:** the selected tab uses the muted Navigation Active red for its fill, text, and defined border.
|
||||||
It must expose `aria-selected`, participate in a labelled `tablist`/`tabpanel`, and be the only
|
It must expose `aria-selected`, participate in a labelled `tablist`/`tabpanel`, and be the only
|
||||||
|
|||||||
@@ -48,6 +48,18 @@ PSD Gitea push and production deployment are still pending.
|
|||||||
|
|
||||||
## Current product shape
|
## Current product shape
|
||||||
|
|
||||||
|
### Full-header and layout refinements — 2026-09-13
|
||||||
|
|
||||||
|
The owner's four visual adjustments are implemented: full header uses Omics
|
||||||
|
`#CB333B` in both themes with a light complete wordmark/controls; Database status
|
||||||
|
has 16px clearance below its top divider; workspace/model/Done share a compact
|
||||||
|
desktop row; session-scope tabs have no bottom border. Embedded has no extra header.
|
||||||
|
Verified with 71 targeted frontend tests, 11 browser scenarios, typecheck and
|
||||||
|
Docker production build. The Mac frontend was recreated alone and is healthy
|
||||||
|
(`8a1608ad7fc6`, image `a2f489ebe9de`); Core/data-service containers are unchanged.
|
||||||
|
Full/en is retained. Rollback image: `thothii-frontend:before-header-layout-20260913`.
|
||||||
|
See `docs/reports/2026-09-13-header-layout-refinements.md` for validation and rollback.
|
||||||
|
|
||||||
### Visual review integrated with the full/embedded shell
|
### Visual review integrated with the full/embedded shell
|
||||||
|
|
||||||
At the owner's request, the seven commits through `a59624a6` from
|
At the owner's request, the seven commits through `a59624a6` from
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
# Ritocchi header e layout
|
||||||
|
|
||||||
|
Data: 13 settembre 2026. Ambito: frontend ThothII e installazione locale del Mac.
|
||||||
|
Nessuna modifica al repository Omics o deploy sul server PSD.
|
||||||
|
|
||||||
|
## Modifiche
|
||||||
|
|
||||||
|
- Header full `#CB333B`, valore di `--gsd-red-primary` in Omics
|
||||||
|
`static/css/gsd-theme.css`. Marchio completo e controlli chiari in entrambi i
|
||||||
|
temi; opzioni lingua, hover, focus ed errori rimangono leggibili. Gli altri
|
||||||
|
marchi conservano il suffisso rosso. Embedded non aggiunge alcun header.
|
||||||
|
- Catalog, Operations e Metadata updated separati di 16px dal divisore superiore.
|
||||||
|
- Due selettori e Done allineati in un'unica riga desktop; controlli alti 32px.
|
||||||
|
Il contenitore stretto impila i controlli. Retry, blocchi operativi e messaggi
|
||||||
|
di errore rimangono disponibili; le scelte mantengono lo stesso comportamento.
|
||||||
|
- Bordo inferiore rimosso dai tab My sessions e All sessions; stato selezionato,
|
||||||
|
focus e navigazione da tastiera conservati.
|
||||||
|
|
||||||
|
Impeccable, registro product, ha guidato il mantenimento della gerarchia esistente
|
||||||
|
e dell'adattamento agli schermi stretti, senza introdurre nuovi componenti.
|
||||||
|
|
||||||
|
## Verifiche
|
||||||
|
|
||||||
|
Typecheck superato; 71 test nelle suite AppShell.host, AppShell.administration e
|
||||||
|
AppShell.session-mgmt; 11 scenari Playwright della revisione visuale superati.
|
||||||
|
Le nuove asserzioni controllano colore header light/dark, colore del suffisso,
|
||||||
|
spazio degli stati, posizione di Done, assenza del bordo e overflow. Verificati
|
||||||
|
gli screenshot desktop/mobile e scuro; le transizioni sono completate prima della
|
||||||
|
cattura. Fixture sintetiche, nessun modello o dato reale modificato dai test.
|
||||||
|
|
||||||
|
Build Docker frontend superata, inclusi test del CSS compilato. Rimane l'avviso
|
||||||
|
preesistente Vite sui bundle maggiori di 500 kB. Il CSS servito dal Mac include
|
||||||
|
il rosso Omics, le azioni inline e la rimozione del bordo; configurazione full/en
|
||||||
|
confermata. Container frontend `8a1608ad7fc6`, immagine `a2f489ebe9de`, healthy.
|
||||||
|
Core `3c3c0739b9af`, catalogo `89755e439dc7`, Qdrant `52ebd5c47955` ed embedding
|
||||||
|
`7f690e534eda` sono invariati e healthy.
|
||||||
|
|
||||||
|
## Aggiornamento e rollback
|
||||||
|
|
||||||
|
È stato ricostruito e ricreato soltanto il frontend attraverso il launcher
|
||||||
|
`/private/tmp/thothii-memory-preview.sh`, con `up -d --no-deps --no-build --wait frontend`.
|
||||||
|
L'immagine precedente è conservata come
|
||||||
|
`thothii-frontend:before-header-layout-20260913`. Per ripristinarla sul Mac:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker image tag thothii-frontend:before-header-layout-20260913 thothii-frontend:local
|
||||||
|
bash /private/tmp/thothii-memory-preview.sh up -d --no-deps --no-build --wait frontend
|
||||||
|
```
|
||||||
|
|
||||||
|
Per tornare alla revisione nuova, ricostruire il frontend dal branch aggiornato
|
||||||
|
e ripetere il comando di avvio. Non toccare volumi, Core o configurazione Omics.
|
||||||
@@ -103,12 +103,16 @@ async function inspect(page: Page) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (const width of [390, 1280]) {
|
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 });
|
await page.setViewportSize({ width, height: 1000 });
|
||||||
const writes = await fixtures(page);
|
const writes = await fixtures(page);
|
||||||
await page.goto("/");
|
await page.goto("/");
|
||||||
const header = page.locator(".thot-full-header");
|
const header = page.locator(".thot-full-header");
|
||||||
await expect(header).toBeVisible();
|
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);
|
await page.evaluate(() => document.fonts.ready);
|
||||||
expect(await page.evaluate(() => [...document.fonts].some(font =>
|
expect(await page.evaluate(() => [...document.fonts].some(font =>
|
||||||
font.family.includes("Manrope Variable") && font.status === "loaded",
|
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);
|
expect(Math.abs(layout!.x - (width - layout!.x - layout!.width))).toBeLessThan(1);
|
||||||
await admin(page, "Database");
|
await admin(page, "Database");
|
||||||
await expect(page.getByRole("heading", { name: "Database management", exact: true })).toBeVisible();
|
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 header.getByRole("combobox").selectOption("it");
|
||||||
await expect(page.getByRole("heading", { name: "Gestione database", exact: true })).toBeVisible();
|
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-administration-eyebrow")).toHaveText("Amministrazione");
|
||||||
await expect(page.locator(".thot-context-summary small")).toHaveText("Contesto di lavoro");
|
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 page.getByRole("button", { name: "Usa il tema scuro", exact: true }).click();
|
||||||
await expect(page.locator(".thot-host")).toHaveAttribute("data-theme", "dark");
|
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: [] });
|
expect(await inspect(page)).toEqual({ overflow: 0, serif: [], tiny: [] });
|
||||||
await page.reload();
|
await page.reload();
|
||||||
await expect(page.getByRole("heading", { name: "Gestione database", exact: true })).toBeVisible();
|
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-host > .thot-context-layout { flex: 1; min-height: 0; height: auto; }
|
||||||
.thot-full-header {
|
.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;
|
display: flex;
|
||||||
flex: none;
|
flex: none;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
@@ -238,13 +241,32 @@ body.thot-full-document { margin: 0; }
|
|||||||
gap: 0.5rem 1rem;
|
gap: 0.5rem 1rem;
|
||||||
min-height: 3.75rem;
|
min-height: 3.75rem;
|
||||||
padding: 0.625rem var(--thot-shell-gutter);
|
padding: 0.625rem var(--thot-shell-gutter);
|
||||||
border-bottom: 1px solid oklch(var(--border));
|
border-bottom: 1px solid var(--thot-header-background);
|
||||||
background: oklch(var(--card));
|
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__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__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__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 {
|
.thot-shell-select {
|
||||||
min-height: 2rem;
|
min-height: 2rem;
|
||||||
max-width: 9rem;
|
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/ }));
|
await userEvent.click(screen.getByRole("button", { name: /Working context/ }));
|
||||||
expect(screen.getAllByRole("combobox", { name: "Workspace" })).toHaveLength(1);
|
expect(screen.getAllByRole("combobox", { name: "Workspace" })).toHaveLength(1);
|
||||||
expect(screen.getAllByRole("combobox", { name: "Model" })).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");
|
await userEvent.selectOptions(screen.getByRole("combobox", { name: "Workspace" }), "beta");
|
||||||
expect(workspacePreferences.load()).toMatchObject({ workspaceId: "beta", provider: "test", model: "test" });
|
expect(workspacePreferences.load()).toMatchObject({ workspaceId: "beta", provider: "test", model: "test" });
|
||||||
await userEvent.click(screen.getByRole("button", { name: "Done" }));
|
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("tabindex", "0");
|
||||||
expect(mySessions).toHaveAttribute("data-tab-state", "active");
|
expect(mySessions).toHaveAttribute("data-tab-state", "active");
|
||||||
expect(mySessions).toHaveClass("bg-[oklch(var(--nav-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-selected", "false");
|
||||||
expect(allSessions).toHaveAttribute("aria-controls", "session-scope-panel");
|
expect(allSessions).toHaveAttribute("aria-controls", "session-scope-panel");
|
||||||
expect(allSessions).toHaveAttribute("tabindex", "-1");
|
expect(allSessions).toHaveAttribute("tabindex", "-1");
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ interface AppShellProps {
|
|||||||
const SESSION_SCOPES: readonly SessionScope[] = ["mine", "all"];
|
const SESSION_SCOPES: readonly SessionScope[] = ["mine", "all"];
|
||||||
|
|
||||||
function sessionScopeTabClass(selected: boolean): string {
|
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
|
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-[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`;
|
: `${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-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 { 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-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 { 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 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] { color: oklch(var(--muted-foreground)); font-size: .75rem; line-height: 1.6; }
|
||||||
.thot-context-body > p[role] { margin-top: 12px; }
|
.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 :is(button,select):focus-visible { outline: 2px solid oklch(var(--ring)); outline-offset: 3px; }
|
||||||
.thot-context-shelf select:disabled { opacity: .65; }
|
.thot-context-shelf select:disabled { opacity: .65; }
|
||||||
@container working-context (max-width: 38rem) {
|
@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>)}
|
<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>
|
</select>
|
||||||
</label>
|
</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>
|
</div>
|
||||||
{locked && <p role="status">{translate("Workspace and model are locked until the current operation finishes.")}</p>}
|
{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 && 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>}
|
{!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>
|
</div>
|
||||||
</section>;
|
</section>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -105,6 +105,7 @@
|
|||||||
grid-area: status;
|
grid-area: status;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
padding-inline: var(--fleet-summary-inset);
|
padding-inline: var(--fleet-summary-inset);
|
||||||
|
padding-top: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.thot-fleet-ledger .thot-administration-header,
|
.thot-fleet-ledger .thot-administration-header,
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ export function FullHeader({ onLogout }: { onLogout?: () => Promise<void> }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<header className="thot-full-header" aria-label={t("Application header")}>
|
<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">
|
<div className="thot-full-header__controls">
|
||||||
<label className="sr-only" htmlFor="thot-ui-language">{t("Interface language")}</label>
|
<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)}>
|
<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