fix: remeasure composer after hidden Core becomes visible
This commit is contained in:
@@ -51,6 +51,21 @@ controlli geometrici per tutte e tre le etichette alle cinque larghezze responsi
|
|||||||
Il successivo aggiornamento del medesimo tag frontend include questa correzione;
|
Il successivo aggiornamento del medesimo tag frontend include questa correzione;
|
||||||
il digest del follow-up precedente identifica quindi una versione superata.
|
il digest del follow-up precedente identifica quindi una versione superata.
|
||||||
|
|
||||||
|
## Follow-up: altezza del prompt Core (2026-09-13)
|
||||||
|
|
||||||
|
Riprodotto con Playwright il campo schiacciato: altezza inline 0 px, spazio utile 8 px,
|
||||||
|
a fronte di 31 px necessari per la prima riga. L'autosizing misurava il Core ancora
|
||||||
|
nascosto durante il caricamento del contesto e non reagiva alla successiva apertura.
|
||||||
|
Il reset isolato dell'altezza ad auto riportava immediatamente il campo a 31 px.
|
||||||
|
|
||||||
|
Il composer ora evita misure nascoste, ricalcola al cambio di larghezza/visibilità e
|
||||||
|
al caricamento dei font, conserva un minimo di 32 px e cresce fino a 160 px con scroll.
|
||||||
|
Il focus usa il contorno esterno esistente, senza il secondo bordo interno.
|
||||||
|
Nessuna modifica all'invio o al workflow. Passati 9 scenari Playwright e 17 test Vitest
|
||||||
|
mirati; coperti ingresso dal Core e dall'Admin, Shift+Enter, testo lungo, mobile,
|
||||||
|
navigazione con bozza conservata e svuotamento. Build Docker frontend aggiornata;
|
||||||
|
branch di revisione isolato, immagine di ripristino invariata.
|
||||||
|
|
||||||
## Verifica
|
## Verifica
|
||||||
|
|
||||||
| Verifica | Esito |
|
| Verifica | Esito |
|
||||||
|
|||||||
@@ -90,6 +90,50 @@ async function inspect(page: Page) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (const route of ["/", "/?thoth_route=administration%2Fdatabase"]) {
|
||||||
|
test(`Core prompt remains readable after loading ${route}`, async ({ page }, testInfo) => {
|
||||||
|
const writes = await fixtures(page);
|
||||||
|
await page.goto(route);
|
||||||
|
if (route !== "/") {
|
||||||
|
await navigation(page);
|
||||||
|
await page.getByRole("button", { name: "Return to session", exact: true }).click();
|
||||||
|
}
|
||||||
|
const prompt = page.getByRole("textbox", { name: "New question", exact: true });
|
||||||
|
await expect(prompt).toBeVisible();
|
||||||
|
await prompt.focus();
|
||||||
|
const readable = async () => {
|
||||||
|
await expect.poll(() => prompt.evaluate(el => {
|
||||||
|
const style = getComputedStyle(el);
|
||||||
|
return el.clientHeight >= parseFloat(style.lineHeight) + parseFloat(style.paddingTop) + parseFloat(style.paddingBottom);
|
||||||
|
})).toBe(true);
|
||||||
|
};
|
||||||
|
await readable();
|
||||||
|
await expect(prompt).toHaveCSS("outline-style", "none");
|
||||||
|
await page.screenshot({ path: testInfo.outputPath("Prompt-focused.png") });
|
||||||
|
await prompt.fill("First line");
|
||||||
|
await prompt.press("Shift+Enter");
|
||||||
|
await prompt.press("a");
|
||||||
|
await expect(prompt).toHaveValue("First line\na");
|
||||||
|
await expect.poll(() => prompt.evaluate(el => el.clientHeight)).toBeGreaterThan(40);
|
||||||
|
const question = "Compare eligible patients across the available cohorts. ".repeat(30);
|
||||||
|
await prompt.fill(question);
|
||||||
|
await page.setViewportSize({ width: 390, height: 900 });
|
||||||
|
await readable();
|
||||||
|
await expect.poll(() => prompt.evaluate(el => el.clientHeight)).toBe(160);
|
||||||
|
expect(await prompt.evaluate(el => el.scrollHeight > el.clientHeight)).toBe(true);
|
||||||
|
await admin(page, "Database");
|
||||||
|
await navigation(page);
|
||||||
|
await page.getByRole("button", { name: "Return to session", exact: true }).click();
|
||||||
|
await expect(prompt).toHaveValue(question);
|
||||||
|
await readable();
|
||||||
|
await page.screenshot({ path: testInfo.outputPath("Prompt-multiline-mobile.png") });
|
||||||
|
await prompt.fill("");
|
||||||
|
await readable();
|
||||||
|
await expect.poll(() => prompt.evaluate(el => el.clientHeight)).toBeLessThanOrEqual(40);
|
||||||
|
expect(writes).toEqual([]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
for (const width of [390, 649, 768, 1280, 1600]) {
|
for (const width of [390, 649, 768, 1280, 1600]) {
|
||||||
test(`all surfaces retain shared typography at ${width}px`, async ({ page }, testInfo) => {
|
test(`all surfaces retain shared typography at ${width}px`, async ({ page }, testInfo) => {
|
||||||
await page.setViewportSize({ width, height: 1000 });
|
await page.setViewportSize({ width, height: 1000 });
|
||||||
|
|||||||
@@ -158,6 +158,11 @@
|
|||||||
outline-offset: 2px;
|
outline-offset: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* The composer already exposes keyboard focus on its enclosing focus-within frame. */
|
||||||
|
#root textarea.thot-composer-input:focus-visible {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
#root :where(h1, h2, h3, h4, h5, h6),
|
#root :where(h1, h2, h3, h4, h5, h6),
|
||||||
[data-slot="dialog-content"] :where(h1, h2, h3, h4, h5, h6) {
|
[data-slot="dialog-content"] :where(h1, h2, h3, h4, h5, h6) {
|
||||||
font-family: var(--font-heading);
|
font-family: var(--font-heading);
|
||||||
|
|||||||
@@ -69,12 +69,32 @@ export function SteerInput({
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Grow the textarea to fit its content (up to a cap, then it scrolls), and shrink
|
// Grow the textarea to fit its content (up to a cap, then it scrolls), and shrink
|
||||||
// back when the text is cleared. Runs after every value change so height tracks text.
|
// back when cleared. Core stays mounted while hidden behind context/Admin pages,
|
||||||
|
// so a hidden scrollHeight must never become a persisted zero-pixel height.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const el = taRef.current;
|
const el = taRef.current;
|
||||||
if (!el) return;
|
if (!el) return;
|
||||||
|
let disposed = false;
|
||||||
|
const resize = () => {
|
||||||
|
if (disposed || el.getClientRects().length === 0) return;
|
||||||
el.style.height = "auto";
|
el.style.height = "auto";
|
||||||
el.style.height = `${Math.min(el.scrollHeight, 160)}px`;
|
el.style.height = `${Math.min(el.scrollHeight, 160)}px`;
|
||||||
|
};
|
||||||
|
resize();
|
||||||
|
let lastWidth = -1;
|
||||||
|
const observer = typeof ResizeObserver === "undefined" ? undefined : new ResizeObserver(([entry]) => {
|
||||||
|
const width = entry.contentRect.width;
|
||||||
|
if (width === lastWidth) return;
|
||||||
|
lastWidth = width;
|
||||||
|
// Ignore height-only notifications from our own autosizing writes.
|
||||||
|
if (width > 0) resize();
|
||||||
|
});
|
||||||
|
observer?.observe(el);
|
||||||
|
void document.fonts?.ready.then(resize);
|
||||||
|
return () => {
|
||||||
|
disposed = true;
|
||||||
|
observer?.disconnect();
|
||||||
|
};
|
||||||
}, [text]);
|
}, [text]);
|
||||||
|
|
||||||
async function submit() {
|
async function submit() {
|
||||||
@@ -162,7 +182,7 @@ export function SteerInput({
|
|||||||
placeholder={sessionId ? "Free steering (!)…" : "Ask a question to start a new session…"}
|
placeholder={sessionId ? "Free steering (!)…" : "Ask a question to start a new session…"}
|
||||||
aria-label={sessionId ? "Steering" : "New question"}
|
aria-label={sessionId ? "Steering" : "New question"}
|
||||||
data-awaiting-input={awaitingInput ? "true" : undefined}
|
data-awaiting-input={awaitingInput ? "true" : undefined}
|
||||||
className={["max-h-40 flex-1 resize-none rounded-lg bg-card px-1 py-1 text-sm leading-relaxed outline-none placeholder:text-muted-foreground", awaitingInput && "thot-awaiting-input"].filter(Boolean).join(" ")}
|
className={["thot-composer-input min-h-8 min-w-0 max-h-40 flex-1 resize-none rounded-lg bg-card px-1 py-1 text-sm leading-relaxed outline-none placeholder:text-muted-foreground", awaitingInput && "thot-awaiting-input"].filter(Boolean).join(" ")}
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
|||||||
Reference in New Issue
Block a user