fix(ui): unify Memory and Evidence reading typography
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
```
|
||||
@@ -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([]);
|
||||
});
|
||||
|
||||
+43
-1
@@ -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; }
|
||||
|
||||
@@ -29,7 +29,7 @@ function CopyText({ text, label, showText = true }: { text: string; label: strin
|
||||
const { t } = useI18n();
|
||||
const [notice, setNotice] = useState("");
|
||||
return <div className="flex min-w-0 flex-wrap items-start gap-2">
|
||||
{showText && <code className="min-w-0 flex-1 [overflow-wrap:anywhere] text-xs select-text">{text}</code>}
|
||||
{showText && <code className="min-w-0 flex-1 [overflow-wrap:anywhere] text-sm select-text">{text}</code>}
|
||||
<Button size="icon" variant="ghost" title={t("Copy {label}", { label })} aria-label={t("Copy {label}", { label })} onClick={async () => {
|
||||
try { await navigator.clipboard.writeText(text); setNotice("Copied"); }
|
||||
catch { setNotice("Select the text and copy it manually."); }
|
||||
@@ -131,15 +131,15 @@ function UnitDetail({ item, location }: { item: EvidenceUnit; location: Evidence
|
||||
const { t } = useI18n();
|
||||
const source = item.provenance.kind === "manual" ? item.provenance.original : item.provenance;
|
||||
const file = location.workspace ? `${location.workspace.replace(/\\/g, "/")}/evidence/${item.file}` : null;
|
||||
return <article className="min-w-0 w-full space-y-5 pb-8 [overflow-wrap:anywhere]">
|
||||
<header><p className="text-xs text-muted-foreground">{t(states[item.status])} · {item.purposes.map(purpose => t(purpose.replaceAll("_", " "))).join(", ")}</p><h2 className="mt-1 font-sans text-xl font-semibold">{item.title}</h2></header>
|
||||
return <article className="thot-knowledge-reader space-y-6 pb-8">
|
||||
<header><p className="thot-knowledge-meta text-muted-foreground">{t(states[item.status])} · {item.purposes.map(purpose => t(purpose.replaceAll("_", " "))).join(", ")}</p><h2 className="thot-knowledge-title">{item.title}</h2></header>
|
||||
{file && <CopyText text={file} label={t("Evidence file path")} />}
|
||||
{item.status !== "active" && <p className="text-sm text-muted-foreground">{["invalid", "removed"].includes(item.status) ? t("The file is invalid or removed. Its last active content is shown here.") : t("This is the file version awaiting consolidation.")} {t("The core continues to use its last active version.")}</p>}
|
||||
{Object.entries(item.applies_to).some(([, values]) => values.length > 0) && <section><h3 className="font-medium">{t("Scope")}</h3>{Object.entries(item.applies_to).map(([name, values]) => values.length > 0 && <p className="mt-2 break-words text-sm" key={name}><span className="capitalize">{t(name)}</span>: {values.join(", ")}</p>)}</section>}
|
||||
{Object.entries(item.payload).map(([field, value]) => <section key={field}><h3 className="mb-2 text-sm font-semibold capitalize">{t(field.replaceAll("_", " "))}</h3>{typeof value === "string" ? field === "sql" ? <pre className="overflow-auto whitespace-pre-wrap [overflow-wrap:anywhere] rounded-md bg-muted p-3 text-sm">{value}</pre> : <MarkdownView source={value} readable /> : Array.isArray(value) ? <ul className="list-inside list-disc space-y-2 text-sm">{value.map(v => <li key={v}>{v}</li>)}</ul> : <dl className="space-y-2 text-sm">{Object.entries(value).map(([key, text]) => <div key={key}><dt className="font-mono font-medium">{key || t("(empty value)")}</dt><dd><MarkdownView source={text} readable /></dd></div>)}</dl>}</section>)}
|
||||
<section className="border-t pt-4"><h3 className="font-medium">{t("Provenance")}</h3><p className="mt-2 text-sm">{item.provenance.kind === "manual" ? t("Manual declaration by {author}.", { author: item.provenance.declared_by ?? "" }) : t("Derived from a source document.")}</p>
|
||||
{source?.source_file && <><p className="mt-2 [overflow-wrap:anywhere] text-xs">{item.provenance.kind === "manual" ? t("Original document (lineage): ") : t("Source: ")}{source.source_file}</p>{source.supporting_excerpts?.map((text, i) => <div key={i} className="mt-4"><MarkdownView source={text} readable /></div>)}</>}
|
||||
{Object.entries(item.applies_to).some(([, values]) => values.length > 0) && <section><h3 className="thot-knowledge-heading">{t("Scope")}</h3>{Object.entries(item.applies_to).map(([name, values]) => values.length > 0 && <p className="mt-3" key={name}><span className="capitalize">{t(name)}</span>: {values.join(", ")}</p>)}</section>}
|
||||
{Object.entries(item.payload).map(([field, value]) => <section key={field}><h3 className="thot-knowledge-heading capitalize">{t(field.replaceAll("_", " "))}</h3>{typeof value === "string" ? field === "sql" ? <pre className="overflow-auto whitespace-pre-wrap [overflow-wrap:anywhere] rounded-md bg-muted p-3">{value}</pre> : <MarkdownView source={value} readable /> : Array.isArray(value) ? <ul className="list-outside list-disc space-y-2 pl-5">{value.map(v => <li key={v}>{v}</li>)}</ul> : <dl className="space-y-4">{Object.entries(value).map(([key, text]) => <div key={key}><dt className="mb-2 font-semibold">{key ? <code>{key}</code> : t("(empty value)")}</dt><dd><MarkdownView source={text} readable /></dd></div>)}</dl>}</section>)}
|
||||
<section className="border-t pt-6"><h3 className="thot-knowledge-heading">{t("Provenance")}</h3><p>{item.provenance.kind === "manual" ? t("Manual declaration by {author}.", { author: item.provenance.declared_by ?? "" }) : t("Derived from a source document.")}</p>
|
||||
{source?.source_file && <><p className="thot-knowledge-meta mt-3">{item.provenance.kind === "manual" ? t("Original document (lineage): ") : t("Source: ")}<code>{source.source_file}</code></p>{source.supporting_excerpts?.map((text, i) => <div key={i} className="mt-5"><MarkdownView source={text} readable /></div>)}</>}
|
||||
</section>
|
||||
{item.review_items.length > 0 && <section><h3 className="font-medium">{t("Review required")}</h3>{item.review_items.map((review, i) => <p key={i} className="mt-2 text-sm">{t(review.message)}</p>)}</section>}
|
||||
{item.review_items.length > 0 && <section><h3 className="thot-knowledge-heading">{t("Review required")}</h3>{item.review_items.map((review, i) => <p key={i} className="mt-3">{t(review.message)}</p>)}</section>}
|
||||
</article>;
|
||||
}
|
||||
|
||||
@@ -180,21 +180,21 @@ function MemoryWorkspace({ workspace, onDirtyChange, onBusyChange }: {
|
||||
</section>
|
||||
<section aria-label={t("Memory detail")} className="thot-administration-detail">
|
||||
{!draft && !selected && <p className="py-8 text-sm text-muted-foreground">{t("Select a card to inspect its content, scope and provenance.")}</p>}
|
||||
{selected && !draft && <div className="min-w-0 w-full space-y-5 [overflow-wrap:anywhere]">
|
||||
{selected && !draft && <div className="thot-knowledge-reader space-y-6">
|
||||
{formattingExample && <p role="note" className="rounded-md border bg-muted p-3 text-sm">{t("Simulated cards for visual review only. Not saved, indexed or sent to the model.")}</p>}
|
||||
<div><p className="text-xs text-muted-foreground">{t(families[selected.family])}</p><h2 className="mt-1 break-words font-sans text-xl font-semibold">{selected.subject}</h2></div>
|
||||
<dl className="space-y-4 text-sm">
|
||||
<div><p className="thot-knowledge-meta text-muted-foreground">{t(families[selected.family])}</p><h2 className="thot-knowledge-title">{selected.subject}</h2></div>
|
||||
<dl className="space-y-6">
|
||||
{(["detail", "scope", "rationale", "question", "sql"] as const).filter(k => selected[k]).map(key => <div key={key}>
|
||||
<dt className="mb-1 font-semibold capitalize">{key === "detail" ? t("Content") : key === "sql" ? t("Approved SQL") : t(key)}</dt>
|
||||
<dd className="min-w-0 w-full">{key === "sql" ? <pre className="whitespace-pre-wrap [overflow-wrap:anywhere] rounded-md bg-muted p-3 font-mono text-sm">{selected[key]}</pre> : <MarkdownView source={selected[key]} readable />}</dd></div>)}
|
||||
<div><dt className="font-semibold">{t("Provenance")}</dt><dd>{formattingExample ? t("Formatting example, not workspace knowledge.") : selected.origin === "manual" ? t("Created manually") : t("Workflow session {session}", { session: selected.session_id ?? "" })}</dd></div>
|
||||
{selected.decision_seq !== null && <div><dt className="font-semibold">{t("Source decision")}</dt><dd>{selected.decision_seq}</dd></div>}
|
||||
<div><dt className="font-semibold">{t("Card ID")}</dt><dd className="break-all font-mono text-xs">{selected.id}</dd></div>
|
||||
<div><dt className="font-semibold">{t("Updated")}</dt><dd>{new Date(selected.updated_at).toLocaleString(getLocale())}</dd></div>
|
||||
<div><dt className="font-semibold">{t("Created")}</dt><dd>{new Date(selected.created_at).toLocaleString(getLocale())}</dd></div>
|
||||
{selected.concepts.length > 0 && <div><dt className="font-semibold">{t("Concepts")}</dt><dd>{selected.concepts.join(", ")}</dd></div>}
|
||||
{selected.dependencies.length > 0 && <div><dt className="font-semibold">{t("Schema dependencies")}</dt><dd><ul>{selected.dependencies.map((d, i) => <li key={i}>{[d.database, d.schema_name, d.table, d.column].filter(Boolean).join(" / ")}</li>)}</ul></dd></div>}
|
||||
{selected.links.length > 0 && <div><dt className="font-semibold">{t("Linked cards")}</dt><dd><ul>{selected.links.map(link => <li key={link.target_id} className="my-2"><button className="text-left text-primary underline" onClick={() => void open(link.target_id)}>{link.meaning}</button></li>)}</ul></dd></div>}
|
||||
<dt className="thot-knowledge-heading capitalize">{key === "detail" ? t("Content") : key === "sql" ? t("Approved SQL") : t(key)}</dt>
|
||||
<dd className="min-w-0 w-full">{key === "sql" ? <pre className="whitespace-pre-wrap [overflow-wrap:anywhere] rounded-md bg-muted p-3">{selected[key]}</pre> : <MarkdownView source={selected[key]} readable />}</dd></div>)}
|
||||
<div className="border-t pt-6"><dt className="thot-knowledge-heading">{t("Provenance")}</dt><dd>{formattingExample ? t("Formatting example, not workspace knowledge.") : selected.origin === "manual" ? t("Created manually") : t("Workflow session {session}", { session: selected.session_id ?? "" })}</dd></div>
|
||||
{selected.decision_seq !== null && <div className="thot-knowledge-meta"><dt>{t("Source decision")}</dt><dd>{selected.decision_seq}</dd></div>}
|
||||
<div className="thot-knowledge-meta"><dt>{t("Card ID")}</dt><dd><code>{selected.id}</code></dd></div>
|
||||
<div className="thot-knowledge-meta"><dt>{t("Updated")}</dt><dd>{new Date(selected.updated_at).toLocaleString(getLocale())}</dd></div>
|
||||
<div className="thot-knowledge-meta"><dt>{t("Created")}</dt><dd>{new Date(selected.created_at).toLocaleString(getLocale())}</dd></div>
|
||||
{selected.concepts.length > 0 && <div><dt className="thot-knowledge-heading">{t("Concepts")}</dt><dd>{selected.concepts.join(", ")}</dd></div>}
|
||||
{selected.dependencies.length > 0 && <div><dt className="thot-knowledge-heading">{t("Schema dependencies")}</dt><dd><ul className="list-outside list-disc space-y-2 pl-5">{selected.dependencies.map((d, i) => <li key={i}><code>{[d.database, d.schema_name, d.table, d.column].filter(Boolean).join(" / ")}</code></li>)}</ul></dd></div>}
|
||||
{selected.links.length > 0 && <div><dt className="thot-knowledge-heading">{t("Linked cards")}</dt><dd><ul>{selected.links.map(link => <li key={link.target_id} className="my-2"><button className="text-left text-primary underline" onClick={() => void open(link.target_id)}>{link.meaning}</button></li>)}</ul></dd></div>}
|
||||
</dl>
|
||||
{!formattingExample && <div className="flex gap-2"><Button disabled={busy} onClick={() => setDraft(memoryInput(selected))}>{t("Edit card")}</Button>
|
||||
<Button variant="outline" disabled={busy} onClick={() => setDeleteConfirm(true)}>{t("Delete card")}</Button></div>}
|
||||
|
||||
Reference in New Issue
Block a user