fix(ui): simplify login and suppress pointer focus ring on locale select

This commit is contained in:
Codex
2026-09-13 15:57:57 +02:00
parent 7d826e46c0
commit 45db3a239b
7 changed files with 93 additions and 9 deletions
+11
View File
@@ -48,6 +48,17 @@ PSD Gitea push and production deployment are still pending.
## Current product shape
### Login copy and language-selector focus — 2026-09-13
Removed the redundant login eyebrow/icon and installation-account explanation;
the main sign-in heading remains. The full-header language select no longer
shows an outer focus ring after pointer interaction; keyboard focus remains
visible, including after returning with Tab. EN/IT login and both themes are
covered by 36 targeted tests and 14 browser scenarios; typecheck/build pass.
Only the Mac frontend was rebuilt/recreated: `204efd40d3eb`, image `7dd0752ff823`,
healthy. Other containers are unchanged. Rollback image:
`thothii-frontend:before-login-focus-20260913`. No production deployment.
### Full-header and layout refinements — 2026-09-13
The owner's four visual adjustments are implemented: full header uses Omics
@@ -49,3 +49,27 @@ bash /private/tmp/thothii-memory-preview.sh up -d --no-deps --no-build --wait fr
Per tornare alla revisione nuova, ricostruire il frontend dal branch aggiornato
e ripetere il comando di avvio. Non toccare volumi, Core o configurazione Omics.
## Follow-up: login e focus del selettore lingua
Rimossi dalla form login il soprattitolo «Accesso a ThothII», il relativo lucchetto
decorativo e la spiegazione «Usa l’account della tua installazione per continuare.».
Il titolo «Accedi a ThothII» rimane. La modifica vale in inglese e italiano.
I select nativi possono mantenere `:focus-visible` anche dopo un clic. Il selettore
lingua distingue ora il focus da puntatore: niente outline esterno dopo il clic;
il normale bordo del controllo rimane. Il ritorno con Tab e l'interazione da
tastiera conservano l'indicatore visibile. Escape chiude il menu senza cambiare
la modalità di focus. Nessuna sfocatura forzata o modifica all'header Omics.
Verificati 36 test LoginPage/AuthGate/AppShell.host e 14 scenari browser, inclusi
login EN/IT e clic, Escape, Tab/Shift+Tab in entrambi i temi. Screenshot controllati;
nessuna autenticazione reale o sessione utente è stata modificata dalle fixture.
Typecheck e build Docker superati. Impeccable ha guidato la rimozione dei testi
ridondanti mantenendo titolo principale e accessibilità da tastiera.
Solo il frontend Mac è stato ricreato, healthy: container `204efd40d3eb`, immagine
`7dd0752ff823`. Il CSS effettivamente servito include la correzione del focus.
Core, catalogo, Qdrant ed embedding sono invariati. Per annullare soltanto questo
follow-up, usare nei comandi di rollback sopra il tag
`thothii-frontend:before-login-focus-20260913`.
+34
View File
@@ -162,6 +162,40 @@ for (const width of [390, 1280]) {
});
}
for (const locale of ["en", "it"]) {
test(`login keeps only the main title in ${locale}`, async ({ page }, testInfo) => {
await fixtures(page);
await page.route("**/config.js", route => route.fulfill({ contentType: "application/javascript",
body: `window.__THOTHII_CONFIG__ = ${JSON.stringify({ backendBaseUrl: "/api", shell: { mode: "full", defaultLocale: locale } })};` }));
await page.route("**/api/auth/config", route => route.fulfill({ json: { mode: "local", localLogin: true, oidcLogin: false } }));
await page.route("**/api/me", route => route.fulfill({ status: 401, json: { code: "unauthenticated" } }));
await page.goto("/");
await expect(page.getByRole("heading", { name: locale === "it" ? "Accedi a ThothII" : "Sign in to ThothII" })).toBeVisible();
await expect(page.getByText(locale === "it" ? "Accesso a ThothII" : "ThothII access", { exact: true })).toHaveCount(0);
await expect(page.getByText(locale === "it" ? "Usa l’account della tua installazione per continuare." : "Use your installation account to continue.", { exact: true })).toHaveCount(0);
await page.screenshot({ path: testInfo.outputPath(`login-${locale}.png`), animations: "disabled" });
});
}
test("language selector has no pointer ring and retains keyboard focus in both themes", async ({ page }, testInfo) => {
await fixtures(page);
await page.goto("/");
const language = page.getByRole("combobox", { name: "Interface language" });
for (const theme of ["light", "dark"]) {
if (theme === "dark") await page.getByRole("button", { name: "Use dark theme", exact: true }).click();
await language.click();
await page.keyboard.press("Escape");
await expect(language).toBeFocused();
await expect(language).toHaveCSS("outline-style", "none");
await expect(language).toHaveCSS("box-shadow", "none");
await page.screenshot({ path: testInfo.outputPath(`language-pointer-${theme}.png`), animations: "disabled" });
await page.keyboard.press("Tab");
await page.keyboard.press("Shift+Tab");
await expect(language).toBeFocused();
await expect(language).toHaveCSS("outline-style", "solid");
}
});
for (const route of ["/", "/?thoth_route=administration%2Fdatabase"]) {
test(`Core prompt remains readable after loading ${route}`, async ({ page }, testInfo) => {
const writes = await fixtures(page);
+9 -1
View File
@@ -7,6 +7,7 @@ import { LoginPage } from "./LoginPage";
import { clearAuthState, setAuthState } from "./authState";
import * as authApi from "../api/auth";
import { server } from "../test/msw";
import { setLocale } from "../i18n";
const localConfig = { mode: "local", localLogin: true, oidcLogin: false } as const;
const oidcConfig = { mode: "oidc", localLogin: false, oidcLogin: true } as const;
@@ -26,9 +27,16 @@ const authenticated = {
};
beforeEach(() => clearAuthState());
afterEach(() => clearAuthState());
afterEach(() => { clearAuthState(); setLocale("en"); });
describe("LoginPage", () => {
test.each(["en", "it"])("keeps only the main sign-in title in %s", locale => {
setLocale(locale);
render(<LoginPage config={localConfig} onAuthenticated={vi.fn()} />);
expect(screen.getByRole("heading", { name: locale === "it" ? "Accedi a ThothII" : "Sign in to ThothII" })).toBeInTheDocument();
expect(screen.queryByText(locale === "it" ? "Accesso a ThothII" : "ThothII access")).not.toBeInTheDocument();
expect(screen.queryByText(locale === "it" ? "Usa l’account della tua installazione per continuare." : "Use your installation account to continue.")).not.toBeInTheDocument();
});
test("shows an unchecked local Remember me control and clears the password after failure", async () => {
let submittedPassword = "";
server.use(http.post("/api/auth/local/login", async ({ request }) => {
+2 -7
View File
@@ -1,7 +1,7 @@
import { useI18n } from "../i18n";
import { useEffect, useRef, useState } from "react";
import type { FormEvent } from "react";
import { AlertTriangle, ArrowRight, Eye, EyeOff, LockKeyhole } from "lucide-react";
import { AlertTriangle, ArrowRight, Eye, EyeOff } from "lucide-react";
import { ApiError } from "../api/client";
import { beginOidcLogin, loginLocal } from "../api/auth";
import type { AuthenticatedUser, AuthPublicConfig } from "../api/types";
@@ -85,12 +85,7 @@ export function LoginPage({ config, onAuthenticated, onRetry }: LoginPageProps)
<section className="mx-auto w-full max-w-md rounded-2xl border border-border/80 bg-card p-6 shadow-md sm:p-8">
<div className="mb-7">
<div className="flex items-center gap-2 text-primary" aria-hidden="true">
<LockKeyhole className="size-4" />
<span className="thot-label text-primary">{t("ThothII access")}</span>
</div>
<h2 className="mt-3 font-heading text-3xl font-semibold tracking-tight">{t("Sign in to ThothII")}</h2>
<p className="mt-2 text-sm leading-6 text-muted-foreground">{t("Use your installation account to continue.")}</p>
<h2 className="font-heading text-3xl font-semibold tracking-tight">{t("Sign in to ThothII")}</h2>
</div>
{error && (
+6
View File
@@ -267,6 +267,12 @@ body.thot-full-document { margin: 0; }
color: oklch(var(--foreground));
background: oklch(var(--card));
}
/* Native selects can match :focus-visible after a pointer click as well.
Suppress only that outer ring; keyboard focus retains its visible outline. */
.thot-full-header .thot-shell-select[data-pointer-focus="true"]:focus {
outline: none;
box-shadow: none;
}
.thot-shell-select {
min-height: 2rem;
max-width: 9rem;
+7 -1
View File
@@ -12,6 +12,7 @@ export function FullHeader({ onLogout }: { onLogout?: () => Promise<void> }) {
const user = useAuthUser();
const [fullscreenPending, setFullscreenPending] = useState(false);
const [logoutPending, setLogoutPending] = useState(false);
const [languagePointerFocus, setLanguagePointerFocus] = useState(false);
const [error, setError] = useState<string>();
if (shell?.mode !== "full") return null;
const fullscreenLabel = shell.fullscreen ? t("Exit fullscreen") : t("Enter fullscreen");
@@ -41,7 +42,12 @@ export function FullHeader({ onLogout }: { onLogout?: () => Promise<void> }) {
<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)}>
<select id="thot-ui-language" className="thot-shell-select" value={shell.locale}
data-pointer-focus={languagePointerFocus || undefined}
onPointerDown={() => setLanguagePointerFocus(true)}
onKeyDown={event => { if (event.key !== "Escape") setLanguagePointerFocus(false); }}
onBlur={() => setLanguagePointerFocus(false)}
onChange={event => shell.changeLocale(event.target.value)}>
{availableLocales.map(({ code, name }) => <option key={code} value={code} lang={code}>{name}</option>)}
</select>
<Button variant="ghost" size="icon" aria-label={shell.theme === "light" ? t("Use dark theme") : t("Use light theme")}