feat(web): redesign Knowledge as an editable wiki
Some checks failed
ci / build-test (push) Has been cancelled
ci / docker-build (push) Has been cancelled
ci / web (push) Has been cancelled
Desktop App / Build Linux (amd64) (push) Has been cancelled
Desktop App / Attach to Release (push) Has been cancelled

Replaces the read-only stats dashboard with a three-pane wiki: a
navigator tree (group by folder/type/tag/entity), a reader/editor with
bare-slug auto-linking and revision history + diff, and a context rail
for backlinks and related notes. Adds a Cleanup mode for the drift
tools (duplicates, tag manager, orphans, trash) and a Cmd+K quick-open.

Also:
- Adds a real landing view (hero count, KPI row, Nomos-share meter,
  recently-updated, busiest tags) in place of the old "Select a note"
  empty state, and extends the design pass across the tree/reader/rail
  (kind icons instead of repeated text badges, accent-bar selection,
  constrained prose measure).
- Guards every note-selection path behind a confirm when there's an
  unsaved edit in progress, so switching notes can no longer silently
  discard a draft.
- Extracts the markdown-rendering CSS duplicated across ChatThread,
  EntityDetailContent, and the new WikiReader into a shared
  .markdown-body class in app.css, with ChatThread keeping only its
  decorative deltas.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-27 22:50:54 +02:00
parent ce0e4142ff
commit 89312a9ce4
14 changed files with 2626 additions and 268 deletions

View File

@@ -274,7 +274,9 @@
/>
{/if}
{#if msg.text}
<div class="prose-chat max-w-none text-sm leading-relaxed assistant-msg">
<div
class="markdown-body prose-chat max-w-none text-sm leading-relaxed assistant-msg"
>
<!-- eslint-disable-next-line svelte/no-at-html-tags — sanitized via DOMPurify -->
{@html render(msg.text)}
{#if isLast && streaming}
@@ -416,50 +418,36 @@
overflow-wrap: break-word;
}
/* Prose overrides */
.prose-chat :global(p) {
margin: 0 0 0.5rem;
}
.prose-chat :global(p:last-child) {
margin-bottom: 0;
}
.prose-chat :global(ul),
.prose-chat :global(ol) {
margin: 0 0 0.5rem;
padding-left: 1.25rem;
}
.prose-chat :global(ul) {
list-style-type: disc;
}
.prose-chat :global(ol) {
list-style-type: decimal;
}
/* Prose overrides — deltas on top of the shared .markdown-body base
(app.css) only. The template applies both classes together
(class="markdown-body prose-chat ..."); everything below either adds a
look .markdown-body doesn't have (li::marker, the pre/blockquote
::before ornaments, hr, strong, the table-wrapper, the code-copy
button) or overrides a .markdown-body value that this "Art Nouveau"
chat treatment wants different (code/pre padding, heading size, th/td
padding, blockquote border color, link underline style). Anywhere a
value is actually overridden, the selector is
`.markdown-body.prose-chat` rather than `.prose-chat` alone —
:global() selectors from two different <style> blocks land in the same
stylesheet with no scoping to arbitrate between them, so equal
specificity would leave the winner to injection order (unreliable
across dev/build). The two-class selector's higher specificity wins
deterministically regardless. */
.prose-chat :global(li) {
margin-bottom: 0.125rem;
padding-left: 0.25rem;
}
.prose-chat :global(li::marker) {
color: var(--primary);
}
.prose-chat :global(code) {
background: var(--muted);
.markdown-body.prose-chat :global(code) {
border: 1px solid var(--border);
border-radius: 4px;
padding: 0.15em 0.4em;
font-family: var(--font-mono);
font-size: 0.85em;
color: var(--primary);
}
.prose-chat :global(pre) {
background: var(--muted);
border: 1px solid var(--border);
border-radius: 8px;
.markdown-body.prose-chat :global(pre) {
padding: 0.75rem 0.875rem;
overflow-x: auto;
margin: 0 0 0.5rem;
position: relative;
}
.prose-chat :global(pre)::before {
@@ -473,34 +461,28 @@
opacity: 0.4;
}
.prose-chat :global(pre code) {
background: none;
padding: 0;
font-size: 0.8125rem;
color: inherit;
border: none;
}
/* Section headings — serif (Inknut) with a short accent rule. Extra top
margin separates sections; the first heading in a message doesn't. */
.prose-chat :global(h1) {
.markdown-body.prose-chat :global(h1) {
font-size: 1.15em;
font-weight: 600;
margin: 1.15rem 0 0.4rem;
letter-spacing: 0.01em;
position: relative;
display: inline-block;
}
.prose-chat :global(h2) {
.markdown-body.prose-chat :global(h2) {
font-size: 1.08em;
font-weight: 600;
margin: 1.15rem 0 0.4rem;
letter-spacing: 0.01em;
position: relative;
display: inline-block;
}
.prose-chat :global(h3) {
.markdown-body.prose-chat :global(h3) {
font-size: 1.02em;
font-weight: 600;
margin: 1.15rem 0 0.4rem;
letter-spacing: 0.01em;
position: relative;
@@ -524,11 +506,6 @@
opacity: 0.55;
}
.prose-chat :global(table) {
border-collapse: collapse;
margin: 0 0 0.5rem;
font-size: 0.8125rem;
}
.prose-chat :global(.table-wrapper) {
overflow-x: auto;
margin: 0 0 0.5rem;
@@ -540,18 +517,13 @@
background: var(--muted);
font-weight: 600;
}
.prose-chat :global(th),
.prose-chat :global(td) {
border: 1px solid var(--border);
.markdown-body.prose-chat :global(th),
.markdown-body.prose-chat :global(td) {
padding: 0.3rem 0.6rem;
text-align: left;
}
.prose-chat :global(blockquote) {
.markdown-body.prose-chat :global(blockquote) {
border-left: 3px solid var(--primary);
padding-left: 0.75rem;
color: var(--muted-foreground);
margin: 0 0 0.5rem;
font-style: italic;
position: relative;
}
@@ -587,8 +559,7 @@
font-weight: 600;
}
.prose-chat :global(a) {
color: var(--primary);
.markdown-body.prose-chat :global(a) {
text-decoration: underline;
text-decoration-style: dotted;
text-underline-offset: 2px;