diff --git a/DESIGN.md b/DESIGN.md index 0e83b63e..b33c1931 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -351,6 +351,25 @@ default, hover, focus, active, disabled, loading, and error behavior where those ### Curated Evidence Documents +Memory and Evidence share the `thot-knowledge-reader` reading contract. Use locally +bundled Manrope with normal tracking for prose and labels, and these fixed roles: + +- Card title: 24px, weight 600, line-height 1.3 (`thot-knowledge-title`). +- Field/section heading, including Scope and Provenance: 20px, weight 600, + line-height 1.4, 8px clearance below (`thot-knowledge-heading`). +- All narrative text, including scope, lists and provenance: 16px, weight 400, + line-height 1.65. Do not apply compact UI text sizes to these fields. +- Authored Markdown subheadings inside a field: 16px, weight 600, line-height 1.5, + 24px above/8px below. They remain subordinate to the enclosing field heading; + their semantic heading levels and original content are preserved. +- Technical metadata labels/values: 14px/1.5, with weight 600 for labels. + Only code, paths and machine identifiers use the technical monospace family at + 14px/1.65, identical for inline and fenced code (never compound `em` shrinkage). + +Separate reading sections by 24px; keep the first Markdown block flush with its +field heading's 8px bottom gap. The same typography applies in light/dark and at +all responsive widths. Controls and archive indexes retain their compact UI roles. + Memory and Evidence detail readers use the entire available content width, without the ordinary 72–75ch prose cap. This is the owner's explicit reading-layout choice. Long unstructured paragraphs are split for display at existing sentence/semicolon diff --git a/PROJECT_STATE.md b/PROJECT_STATE.md index 6d7acfc3..1d764efc 100644 --- a/PROJECT_STATE.md +++ b/PROJECT_STATE.md @@ -48,6 +48,19 @@ PSD Gitea push and production deployment are still pending. ## Current product shape +### Shared Memory/Evidence typography — 2026-09-13 + +Both detail readers now share Manrope and fixed reading roles: 24px card title, +20px section headings, 16px/1.65 narrative text, and 14px metadata/code. Authored +Markdown subheadings remain subordinate to field headings; inline/fenced code no +longer shrinks cumulatively. Full-width layout, paragraph separation and isolated +FAKE examples are retained. All 762 frontend tests and 18 browser scenarios pass, +including computed typography checks at 390/1280/2400px; typecheck, i18n and Docker +build pass. Only Mac frontend was recreated: `87fca0dc5e19`, image `19f1704b6bb2`, +healthy. Core and data services are unchanged. Rollback image: +`thothii-frontend:before-knowledge-typography-20260913`. See +`docs/reports/2026-09-13-knowledge-typography.md`. No PSD/Omics deployment. + ### Knowledge reading and isolated fake Memory examples — 2026-09-13 Memory/Evidence details now use all available width, with display-only paragraph diff --git a/docs/reports/2026-09-13-knowledge-typography.md b/docs/reports/2026-09-13-knowledge-typography.md new file mode 100644 index 00000000..ace23522 --- /dev/null +++ b/docs/reports/2026-09-13-knowledge-typography.md @@ -0,0 +1,52 @@ +# Tipografia condivisa per Memory ed Evidence + +## Modifica + +I lettori mescolavano etichette da 14px, sezioni da 16px e Markdown con una +gerarchia indipendente. I blocchi di codice applicavano una riduzione relativa +anche al carattere già ridotto del contenitore. + +La regola condivisa in `frontend/src/index.css`, documentata in `DESIGN.md`, usa: + +- Manrope per titolo, sezioni, paragrafi, elenchi e provenienza. +- Titolo della scheda: 24px, peso 600, interlinea 1.3. +- Titoli dei campi: 20px, peso 600, interlinea 1.4, 8px prima del contenuto. +- Testo narrativo: 16px, peso 400, interlinea 1.65. +- Sottotitoli Markdown interni: 16px, peso 600; non competono con il titolo del + campo. I livelli semantici originali sono conservati. +- Metadati: 14px; codice, percorsi e identificatori: monospaziato a 14px, senza + riduzioni cumulative nei blocchi. +- Sezioni distanziate di 24px; paragrafi consecutivi distanziati di 20px. + +L'uso della skill Impeccable ha guidato la gerarchia a ruoli fissi e la scelta di +un'unica famiglia, riutilizzando quella già distribuita dall'applicazione. +Rimane valida la richiesta esplicita di occupare tutta la larghezza disponibile: +nessun limite di riga in caratteri e nessuna riduzione dei font su mobile. +Controlli, indici degli archivi e altri lettori non ricevono questa nuova scala. +Documenti originali e schede salvate non sono stati riscritti; gli esempi FAKE +restano esclusi da salvataggio, indicizzazione e uso da parte del modello. + +## Verifiche + +- Typecheck frontend e build Docker completati. +- 762 test Vitest su 91 file e 18 scenari Playwright superati. +- Controlli CSS calcolati a 390, 1280 e 2400px: famiglia, dimensioni, pesi, + interlinea, margini, codice inline/fenced e sottotitoli Markdown da h1 a h6. +- Ispezione delle schermate Evidence in light e Memory in dark, anche mobile. +- Nessun overflow orizzontale; nessuna scrittura API negli scenari FAKE. +- Catalogo i18n: 1686 messaggi italiani, 1707 riferimenti statici verificati. + +## Installazione locale + +Ricreato solo il frontend del progetto Docker `thothii-18998cca7b0a`: +container `87fca0dc5e19`, immagine `19f1704b6bb2`, stato healthy. +Il CSS servito da `http://127.0.0.1:8080` contiene le nuove regole condivise. +Core, PostgreSQL, Qdrant ed embedding conservano i container precedenti. +Nessuna modifica alla configurazione full/en, a Omics o al server PSD. + +Ripristino della precedente immagine locale: + +```bash +docker image tag thothii-frontend:before-knowledge-typography-20260913 thothii-frontend:local +bash /private/tmp/thothii-memory-preview.sh up -d --no-deps --no-build --wait frontend +``` diff --git a/frontend/e2e/ui-visual-review.spec.ts b/frontend/e2e/ui-visual-review.spec.ts index 9c4f64c0..95dc6730 100644 --- a/frontend/e2e/ui-visual-review.spec.ts +++ b/frontend/e2e/ui-visual-review.spec.ts @@ -109,7 +109,7 @@ for (const width of [390, 1280, 2400]) { const writes = await fixtures(page); const sample = memoryFormattingExamples[0]; const longEvidence = { ...evidence, applies_to: { concepts: ["Esempio simulato"], tables: ["demo_visuale.identificatore_lungo_per_la_verifica_della_formattazione"], columns: [] }, - payload: { rule: sample.detail }, provenance: { source_file: "source/demo.md", supporting_excerpts: ["**Esempio simulato** con `codice_inline` e un collegamento alla regola.\n\nSecondo paragrafo di provenienza."] } }; + payload: { rule: sample.detail + "\n\n" + [1, 2, 3, 4, 5, 6].map(level => "#".repeat(level) + " Sottotitolo " + level + "\n\nTesto della sottosezione.").join("\n\n") + "\n\n```sql\nSELECT 1;\n```" }, provenance: { source_file: "source/demo.md", supporting_excerpts: ["**Esempio simulato** con `codice_inline` e un collegamento alla regola.\n\nSecondo paragrafo di provenienza."] } }; await page.route(`**/api/workspaces/${workspace}/evidence/*`, route => route.fulfill({ json: { ...evidencePage, item: longEvidence } })); await page.goto("/"); await admin(page, "Evidence"); @@ -120,6 +120,26 @@ for (const width of [390, 1280, 2400]) { const rule = article.locator(".thot-knowledge-prose").first(); await expect(rule.locator(":scope > p")).not.toHaveCount(1); await expect(rule.locator("p").first()).toHaveCSS("max-width", "none"); + await expect(article.locator(".thot-knowledge-title")).toHaveCSS("font-size", "24px"); + for (const heading of await article.locator(".thot-knowledge-heading").all()) { + await expect(heading).toHaveCSS("font-size", "20px"); + await expect(heading).toHaveCSS("font-weight", "600"); + await expect(heading).toHaveCSS("margin-bottom", "8px"); + } + const bodyFamily = await rule.evaluate(el => getComputedStyle(el).fontFamily); + expect(bodyFamily).toContain("Manrope"); + for (const paragraph of await article.locator("section > p:not(.thot-knowledge-meta), .thot-knowledge-prose p").all()) { + await expect(paragraph).toHaveCSS("font-size", "16px"); + await expect(paragraph).toHaveCSS("line-height", "26.4px"); + await expect(paragraph).toHaveCSS("font-family", bodyFamily); + } + for (const heading of await rule.locator("h1,h2,h3,h4,h5,h6").all()) { + await expect(heading).toHaveCSS("font-size", "16px"); + await expect(heading).toHaveCSS("font-weight", "600"); + await expect(heading).toHaveCSS("font-family", bodyFamily); + } + await expect(rule.locator("pre code")).toHaveCSS("font-size", "14px"); + await expect(rule.locator("p code").first()).toHaveCSS("font-size", "14px"); expect(await article.evaluate(el => { const parent = el.parentElement!; const css = getComputedStyle(parent); @@ -129,7 +149,7 @@ for (const width of [390, 1280, 2400]) { await expect(copy).toHaveText(""); await expect(copy.locator("svg")).toHaveCount(1); await expect(article.locator("strong").filter({ hasText: "Esempio simulato" })).toBeVisible(); - await article.locator("h2").scrollIntoViewIfNeeded(); + await article.locator(".thot-knowledge-title").scrollIntoViewIfNeeded(); await page.screenshot({ path: testInfo.outputPath("evidence-readable.png"), animations: "disabled" }); expect((await inspect(page)).overflow).toBe(0); @@ -141,6 +161,16 @@ for (const width of [390, 1280, 2400]) { await expect(content.locator(":scope > p")).not.toHaveCount(1); await expect(content.locator("p").first()).toHaveCSS("max-width", "none"); await expect(content.locator("p").nth(1)).toHaveCSS("margin-top", "20px"); + await expect(memoryDetail.locator(".thot-knowledge-title")).toHaveCSS("font-size", "24px"); + for (const heading of await memoryDetail.locator(".thot-knowledge-heading").all()) { + await expect(heading).toHaveCSS("font-size", "20px"); + await expect(heading).toHaveCSS("font-weight", "600"); + await expect(heading).toHaveCSS("font-family", bodyFamily); + await expect(heading).toHaveCSS("margin-bottom", "8px"); + } + await expect(content.locator("p").first()).toHaveCSS("font-size", "16px"); + await expect(content.locator("p").first()).toHaveCSS("line-height", "26.4px"); + await expect(content.locator("p").first()).toHaveCSS("font-family", bodyFamily); expect(await content.evaluate(el => Math.abs(el.getBoundingClientRect().width - el.parentElement!.clientWidth))).toBeLessThan(2); await memoryDetail.getByRole("heading", { name: sample.subject, exact: true }).scrollIntoViewIfNeeded(); await page.screenshot({ path: testInfo.outputPath("memory-readable.png"), animations: "disabled" }); @@ -150,6 +180,14 @@ for (const width of [390, 1280, 2400]) { await expect(page.locator(".thot-host")).toHaveAttribute("data-theme", "dark"); await memoryDetail.getByRole("heading", { name: sample.subject, exact: true }).scrollIntoViewIfNeeded(); await page.screenshot({ path: testInfo.outputPath("memory-readable-dark.png"), animations: "disabled" }); + await expect(content.locator("p").first()).toHaveCSS("line-height", "26.4px"); + await page.getByRole("button", { name: memoryFormattingExamples[1].subject, exact: true }).click(); + await expect(memoryDetail.getByRole("heading", { name: "Prima del conteggio" })).toHaveCSS("font-size", "16px"); + await expect(memoryDetail.getByRole("heading", { name: "Prima del conteggio" })).toHaveCSS("font-family", bodyFamily); + await memoryDetail.locator(".thot-knowledge-title").scrollIntoViewIfNeeded(); + await page.screenshot({ path: testInfo.outputPath("memory-headings-dark.png"), animations: "disabled" }); + await page.getByRole("button", { name: memoryFormattingExamples[2].subject, exact: true }).click(); + await expect(memoryDetail.locator("pre")).toHaveCSS("font-size", "14px"); expect((await inspect(page)).overflow).toBe(0); expect(writes).toEqual([]); }); diff --git a/frontend/src/index.css b/frontend/src/index.css index 0ae36793..f5adbffb 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -349,7 +349,49 @@ body.thot-full-document { margin: 0; } .thot-prose h2 { font-size: var(--text-section); } .thot-prose h3 { font-size: var(--text-body); } .thot-prose :where(p, ul, ol, blockquote) { max-width: 75ch; } - .thot-knowledge-prose { width: 100%; min-width: 0; max-width: none; } + /* Shared reading contract for Memory and Evidence, independent of UI controls. */ + .thot-knowledge-reader, + .thot-knowledge-prose { + font-family: var(--font-sans); + font-size: var(--text-body); + font-weight: 400; + line-height: 1.65; + letter-spacing: normal; + color: oklch(var(--foreground)); + width: 100%; + min-width: 0; + max-width: none; + overflow-wrap: anywhere; + } + .thot-knowledge-title { + font: 600 var(--text-page)/1.3 var(--font-sans); + letter-spacing: -0.015em; + margin-top: 0.25rem; + } + .thot-knowledge-heading { + font: 600 var(--text-section)/1.4 var(--font-sans); + letter-spacing: normal; + margin: 0 0 0.5rem; + } + .thot-knowledge-meta { + font: 400 var(--text-control)/1.5 var(--font-sans); + letter-spacing: normal; + } + .thot-knowledge-meta dt { font-weight: 600; margin-bottom: 0.25rem; } + .thot-knowledge-reader code, + .thot-knowledge-reader pre, + .thot-prose.thot-knowledge-prose code { + font-family: var(--font-mono); + font-size: var(--text-control); + line-height: 1.65; + } + .thot-prose.thot-knowledge-prose :where(h1, h2, h3, h4, h5, h6) { + font: 600 var(--text-body)/1.5 var(--font-sans); + letter-spacing: normal; + margin: 1.5rem 0 0.5rem; + } + .thot-prose.thot-knowledge-prose > :first-child { margin-top: 0; } + .thot-knowledge-prose strong { font-weight: 600; } .thot-knowledge-prose :where(p, ul, ol, blockquote) { max-width: none; } .thot-prose.thot-knowledge-prose p + p { margin-top: 1.25em; } .thot-knowledge-prose pre { white-space: pre-wrap; overflow-wrap: anywhere; } diff --git a/frontend/src/shell/EvidenceManagementPage.tsx b/frontend/src/shell/EvidenceManagementPage.tsx index b7d64588..fd1504cb 100644 --- a/frontend/src/shell/EvidenceManagementPage.tsx +++ b/frontend/src/shell/EvidenceManagementPage.tsx @@ -29,7 +29,7 @@ function CopyText({ text, label, showText = true }: { text: string; label: strin const { t } = useI18n(); const [notice, setNotice] = useState(""); return
{text}}
+ {showText && {text}}