diff --git a/DESIGN.md b/DESIGN.md index 0c6e724b..04e14b7a 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -160,6 +160,13 @@ users can scan structure without adding nested containers. ## 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 for action, focus, and important state. OKLCH values in the frontmatter are normative because the 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 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. - 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. ### Tabs - **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. - **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 diff --git a/PROJECT_STATE.md b/PROJECT_STATE.md index 4955ec1d..1131306a 100644 --- a/PROJECT_STATE.md +++ b/PROJECT_STATE.md @@ -48,6 +48,18 @@ PSD Gitea push and production deployment are still pending. ## 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 At the owner's request, the seven commits through `a59624a6` from diff --git a/docs/reports/2026-09-13-header-layout-refinements.md b/docs/reports/2026-09-13-header-layout-refinements.md new file mode 100644 index 00000000..3ffb4a13 --- /dev/null +++ b/docs/reports/2026-09-13-header-layout-refinements.md @@ -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. diff --git a/frontend/e2e/ui-visual-review.spec.ts b/frontend/e2e/ui-visual-review.spec.ts index 46687b9a..d0818cfa 100644 --- a/frontend/e2e/ui-visual-review.spec.ts +++ b/frontend/e2e/ui-visual-review.spec.ts @@ -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(); diff --git a/frontend/src/index.css b/frontend/src/index.css index c360856a..e9cf6183 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -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; diff --git a/frontend/src/shell/AppShell.administration.test.tsx b/frontend/src/shell/AppShell.administration.test.tsx index 1283627e..27486efc 100644 --- a/frontend/src/shell/AppShell.administration.test.tsx +++ b/frontend/src/shell/AppShell.administration.test.tsx @@ -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" })); diff --git a/frontend/src/shell/AppShell.session-mgmt.test.tsx b/frontend/src/shell/AppShell.session-mgmt.test.tsx index 7641568f..81084472 100644 --- a/frontend/src/shell/AppShell.session-mgmt.test.tsx +++ b/frontend/src/shell/AppShell.session-mgmt.test.tsx @@ -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"); diff --git a/frontend/src/shell/AppShell.tsx b/frontend/src/shell/AppShell.tsx index 76df283e..5e1ae3dd 100644 --- a/frontend/src/shell/AppShell.tsx +++ b/frontend/src/shell/AppShell.tsx @@ -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`; diff --git a/frontend/src/shell/WorkingContextShelf.css b/frontend/src/shell/WorkingContextShelf.css index d4099d38..51b82695 100644 --- a/frontend/src/shell/WorkingContextShelf.css +++ b/frontend/src/shell/WorkingContextShelf.css @@ -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) { diff --git a/frontend/src/shell/WorkingContextShelf.tsx b/frontend/src/shell/WorkingContextShelf.tsx index 1ad1d253..c74ccf42 100644 --- a/frontend/src/shell/WorkingContextShelf.tsx +++ b/frontend/src/shell/WorkingContextShelf.tsx @@ -31,11 +31,11 @@ export function WorkingContextShelf({ context, locked, onWorkspace, onModel }: { {context.models.map(model => )} +
{context.error && }{context.ready && }
{locked &&

{translate("Workspace and model are locked until the current operation finishes.")}

} {context.ready && context.error &&

{translate("Context refresh failed. Your current choices and unsaved work have been retained.")}

} {!context.ready &&

{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.")}

} -
{context.error && }{context.ready && }
; } diff --git a/frontend/src/shell/database-management/FleetLedgerShell.css b/frontend/src/shell/database-management/FleetLedgerShell.css index d964dd1c..09c89183 100644 --- a/frontend/src/shell/database-management/FleetLedgerShell.css +++ b/frontend/src/shell/database-management/FleetLedgerShell.css @@ -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, diff --git a/frontend/src/shell/host/FullHeader.tsx b/frontend/src/shell/host/FullHeader.tsx index 16534e5c..652029da 100644 --- a/frontend/src/shell/host/FullHeader.tsx +++ b/frontend/src/shell/host/FullHeader.tsx @@ -38,7 +38,7 @@ export function FullHeader({ onLogout }: { onLogout?: () => Promise }) { return (
- ThothII + ThothII