feat(knowledge): restore complete human rendering layer
This commit is contained in:
parent
893c1a0a12
commit
3466b507c6
15 changed files with 665 additions and 23 deletions
|
|
@ -4,6 +4,12 @@ import { relaunch } from "@tauri-apps/plugin-process";
|
|||
import { Icon } from "./icons";
|
||||
import * as api from "./runtime";
|
||||
import type { KnowledgeDocument, SourceKind, SystemSnapshot } from "./types";
|
||||
import {
|
||||
MarkdownDocument,
|
||||
documentOutline,
|
||||
jumpToHeading,
|
||||
splitFrontmatter,
|
||||
} from "./modules/knowledge-render";
|
||||
|
||||
type View =
|
||||
| "channel"
|
||||
|
|
@ -248,9 +254,10 @@ function Channel({
|
|||
<article>
|
||||
<h2>公共更新</h2>
|
||||
<strong>
|
||||
{data.publicDistributionState === "SIGNED_UPDATE_FEED_READY_NO_RELEASE"
|
||||
? "更新入口在线 · 暂无发行"
|
||||
: data.publicDistributionState}
|
||||
{data.publicDistributionState ===
|
||||
"SIGNED_UPDATE_FEED_READY_NO_RELEASE"
|
||||
? "更新入口在线 · 暂无发行"
|
||||
: data.publicDistributionState}
|
||||
</strong>
|
||||
<p>只接收签名发行,不自动执行普通 Git 拉取。</p>
|
||||
</article>
|
||||
|
|
@ -310,12 +317,15 @@ function Knowledge({
|
|||
[doc, setDoc] = useState<KnowledgeDocument | null>(null),
|
||||
[body, setBody] = useState(""),
|
||||
[query, setQuery] = useState(""),
|
||||
[newTitle, setNewTitle] = useState<string | null>(null);
|
||||
[newTitle, setNewTitle] = useState<string | null>(null),
|
||||
[editing, setEditing] = useState(false),
|
||||
[inspectorOpen, setInspectorOpen] = useState(true);
|
||||
useEffect(() => {
|
||||
if (selected)
|
||||
api.readDocument(selected).then((d) => {
|
||||
setDoc(d);
|
||||
setBody(d.body);
|
||||
setEditing(false);
|
||||
});
|
||||
}, [selected]);
|
||||
const list = useMemo(
|
||||
|
|
@ -337,6 +347,8 @@ function Knowledge({
|
|||
const save = async () => {
|
||||
if (doc) {
|
||||
await api.saveDocument(doc.path, body);
|
||||
setDoc({ ...doc, body });
|
||||
setEditing(false);
|
||||
refresh();
|
||||
}
|
||||
};
|
||||
|
|
@ -348,6 +360,20 @@ function Knowledge({
|
|||
refresh();
|
||||
}
|
||||
};
|
||||
const parsed = useMemo(() => splitFrontmatter(doc?.body ?? ""), [doc?.body]);
|
||||
const outline = useMemo(() => documentOutline(doc?.body ?? ""), [doc?.body]);
|
||||
const stats = useMemo(() => {
|
||||
const chars = parsed.content.replace(/\s/g, "").length;
|
||||
return { chars, minutes: Math.max(1, Math.ceil(chars / 450)) };
|
||||
}, [parsed.content]);
|
||||
const openWiki = (target: string) => {
|
||||
const normalized = target.replace(/[\s·•・\-_|*`]/g, "").toLowerCase();
|
||||
const hit = data.documents.find(
|
||||
(item) =>
|
||||
item.title.replace(/[\s·•・\-_|*`]/g, "").toLowerCase() === normalized,
|
||||
);
|
||||
if (hit) setSelected(hit.path);
|
||||
};
|
||||
return (
|
||||
<div className="page knowledge-page">
|
||||
<header>
|
||||
|
|
@ -368,8 +394,14 @@ function Knowledge({
|
|||
<button onClick={() => setNewTitle("")}>新建知识页</button>
|
||||
</div>
|
||||
</header>
|
||||
<div className="knowledge-workspace">
|
||||
<aside>
|
||||
<div
|
||||
className={`knowledge-workspace complete ${inspectorOpen ? "" : "inspector-closed"}`}
|
||||
>
|
||||
<aside className="knowledge-browser">
|
||||
<div className="browser-title">
|
||||
<strong>知识目录</strong>
|
||||
<span>{data.documents.length} 篇</span>
|
||||
</div>
|
||||
<input
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
|
|
@ -392,25 +424,69 @@ function Knowledge({
|
|||
))
|
||||
)}
|
||||
</aside>
|
||||
<section>
|
||||
<section className="document-workspace">
|
||||
{doc ? (
|
||||
<>
|
||||
<div className="editor-tools">
|
||||
<strong>{doc.title}</strong>
|
||||
<span />
|
||||
<button onClick={() => setInspectorOpen((value) => !value)}>
|
||||
{inspectorOpen ? "收起大纲" : "展开大纲"}
|
||||
</button>
|
||||
<button onClick={() => api.exportDocument(doc.path)}>
|
||||
下载
|
||||
</button>
|
||||
<button className="danger" onClick={del}>
|
||||
删除
|
||||
</button>
|
||||
<button onClick={save}>保存</button>
|
||||
{editing ? (
|
||||
<>
|
||||
<button
|
||||
onClick={() => {
|
||||
setBody(doc.body);
|
||||
setEditing(false);
|
||||
}}
|
||||
>
|
||||
取消
|
||||
</button>
|
||||
<button onClick={save}>保存</button>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<button className="danger" onClick={del}>
|
||||
删除
|
||||
</button>
|
||||
<button onClick={() => setEditing(true)}>编辑</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<textarea
|
||||
className="editor"
|
||||
value={body}
|
||||
onChange={(e) => setBody(e.target.value)}
|
||||
/>
|
||||
{editing ? (
|
||||
<textarea
|
||||
className="editor"
|
||||
aria-label="Markdown 编辑器"
|
||||
value={body}
|
||||
onChange={(e) => setBody(e.target.value)}
|
||||
/>
|
||||
) : (
|
||||
<div className="document-scroll">
|
||||
<header className="reader-heading">
|
||||
<h1>{doc.title}</h1>
|
||||
<p>
|
||||
{stats.chars.toLocaleString()} 字 · 约 {stats.minutes}{" "}
|
||||
分钟读完
|
||||
</p>
|
||||
{parsed.tags.length > 0 && (
|
||||
<div className="meta-tags">
|
||||
{parsed.tags.map((tag) => (
|
||||
<span key={tag}>{tag}</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</header>
|
||||
<MarkdownDocument
|
||||
body={doc.body}
|
||||
knownTitles={data.documents.map((item) => item.title)}
|
||||
onWiki={openWiki}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<div className="welcome">
|
||||
|
|
@ -421,6 +497,53 @@ function Knowledge({
|
|||
</div>
|
||||
)}
|
||||
</section>
|
||||
{inspectorOpen && (
|
||||
<aside className="document-inspector">
|
||||
<header>
|
||||
<strong>页面大纲</strong>
|
||||
<span>来源与证据</span>
|
||||
</header>
|
||||
{doc ? (
|
||||
<>
|
||||
<nav className="outline-list">
|
||||
{outline.length ? (
|
||||
outline.map((item) => (
|
||||
<button
|
||||
key={item.id}
|
||||
style={{ paddingLeft: 12 + (item.level - 1) * 10 }}
|
||||
onClick={() => jumpToHeading(item.id)}
|
||||
>
|
||||
{item.title}
|
||||
</button>
|
||||
))
|
||||
) : (
|
||||
<p>当前页面没有标题层级。</p>
|
||||
)}
|
||||
</nav>
|
||||
<dl className="document-evidence">
|
||||
<div>
|
||||
<dt>知识根</dt>
|
||||
<dd>本机私人 Git</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>路径</dt>
|
||||
<dd>{doc.path}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>内容指纹</dt>
|
||||
<dd>{doc.sha256.slice(0, 16)}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>状态</dt>
|
||||
<dd>可编辑 · 可回收</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</>
|
||||
) : (
|
||||
<p className="empty">选择页面后显示大纲与证据。</p>
|
||||
)}
|
||||
</aside>
|
||||
)}
|
||||
</div>
|
||||
{newTitle !== null && (
|
||||
<div className="modal-backdrop" onMouseDown={() => setNewTitle(null)}>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,223 @@
|
|||
//! 知识渲染件 · 渲染入口(常驻模块 · 插座口 knowledge-render)
|
||||
//!
|
||||
//! 职责:把仓库里的 Markdown 文件变成可读的页面——frontmatter 解析、
|
||||
//! Notion/Outline 导出兼容、标题锚点、大纲同源、跳转。
|
||||
import { useMemo } from "react";
|
||||
import DOMPurify from "dompurify";
|
||||
import { marked, Parser, type Tokens } from "marked";
|
||||
export { knowledgeRenderManifest } from "./manifest";
|
||||
|
||||
function escapeHtml(value: string) {
|
||||
return value
|
||||
.replaceAll("&", "&")
|
||||
.replaceAll("<", "<")
|
||||
.replaceAll(">", ">")
|
||||
.replaceAll('"', """);
|
||||
}
|
||||
|
||||
export function splitFrontmatter(body: string) {
|
||||
const normalized = body.replace(/\r\n/g, "\n");
|
||||
if (!normalized.startsWith("---\n"))
|
||||
return {
|
||||
content: normalized,
|
||||
metadata: {} as Record<string, string>,
|
||||
tags: [] as string[],
|
||||
};
|
||||
const closing = normalized.indexOf("\n---\n", 4);
|
||||
if (closing < 0)
|
||||
return {
|
||||
content: normalized,
|
||||
metadata: {} as Record<string, string>,
|
||||
tags: [] as string[],
|
||||
};
|
||||
const metadata: Record<string, string> = {};
|
||||
const tags: string[] = [];
|
||||
let pendingListKey = "";
|
||||
for (const line of normalized.slice(4, closing).split("\n")) {
|
||||
const listItem = /^\s+-\s+(.+)$/.exec(line);
|
||||
if (listItem && pendingListKey) {
|
||||
if (pendingListKey === "tags")
|
||||
tags.push(listItem[1].trim().replace(/^['"]|['"]$/g, ""));
|
||||
continue;
|
||||
}
|
||||
const match = /^([A-Za-z0-9_-]+):\s*(.*)$/.exec(line);
|
||||
if (!match) continue;
|
||||
pendingListKey = match[2] === "" ? match[1] : "";
|
||||
const value = match[2].replace(/^['"]|['"]$/g, "");
|
||||
metadata[match[1]] = value;
|
||||
if (match[1] === "tags") {
|
||||
const inline = /^\[(.*)\]$/.exec(match[2].trim());
|
||||
const source = inline ? inline[1] : value;
|
||||
source
|
||||
.split(",")
|
||||
.map((part) => part.trim().replace(/^['"]|['"]$/g, ""))
|
||||
.filter(Boolean)
|
||||
.forEach((tag) => tags.push(tag));
|
||||
}
|
||||
}
|
||||
return { content: normalized.slice(closing + 5), metadata, tags };
|
||||
}
|
||||
|
||||
function stripNotionLinks(line: string) {
|
||||
// Notion 页面链接 → 只留链接文字(跳转关系按谕旨失效)
|
||||
return line.replace(
|
||||
/\[([^\]]*)\]\(([^)]*)\)/g,
|
||||
(whole, text: string, href: string) =>
|
||||
/notion\.(?:so|site)/.test(href) ? text : whole,
|
||||
);
|
||||
}
|
||||
|
||||
export function notionCompat(content: string) {
|
||||
// 存量旧库文件尚未洗净:渲染时按"进门即洗"同规矩兜底过滤一遍。
|
||||
// 内容一字不改,只把外来知识库的外壳换成光湖原生 Markdown(冰朔 2026-08-15 谕:不兼容,进门就转)。
|
||||
const result: string[] = [];
|
||||
for (const line of content.replace(/\r\n/g, "\n").split("\n")) {
|
||||
const trimmed = line.trim();
|
||||
if (trimmed === "<aside>" || trimmed === "</aside>") continue;
|
||||
if (trimmed.startsWith(":::toggle")) {
|
||||
const title = trimmed.slice(":::toggle".length).trim();
|
||||
result.push(title ? `**▸ ${title}**` : "**▸ 详情**");
|
||||
continue;
|
||||
}
|
||||
if (trimmed === ":::" || trimmed.startsWith(":::toc")) continue;
|
||||
result.push(stripNotionLinks(line.replace(/<br\s*\/?>/g, "\n")));
|
||||
}
|
||||
// Notion 粗体写法(**文字:**紧接正文)不合 CommonMark:给收尾星号补一口气
|
||||
return result
|
||||
.join("\n")
|
||||
.replace(/(\*\*[^*\n]+\*\*)(?=[^\s.,;:!?,。;:!?、)\]"'])/g, "$1 ");
|
||||
}
|
||||
|
||||
const CALLOUT_TINTS: [RegExp, string][] = [
|
||||
[/📌|🌌|🧭|💜|🗂|🏷|📚/, "callout-lavender"],
|
||||
[/💡|⚡|🌟|☀|✨|🔑/, "callout-amber"],
|
||||
[/⚠|🔥|❗|🚨|❌/, "callout-rose"],
|
||||
[/✅|🌿|🍀|💚|✔/, "callout-mint"],
|
||||
[/🌊|💧|🔵|❄|🧊/, "callout-sky"],
|
||||
];
|
||||
function normalizeTitle(value: string) {
|
||||
return value.replace(/[\s·•・\-_|*`]/g, "").toLowerCase();
|
||||
}
|
||||
|
||||
function calloutTint(text: string) {
|
||||
for (const [pattern, tint] of CALLOUT_TINTS)
|
||||
if (pattern.test(text)) return tint;
|
||||
return "callout-slate";
|
||||
}
|
||||
|
||||
export function markdownHtml(
|
||||
body: string,
|
||||
knownTitles: readonly string[] = [],
|
||||
) {
|
||||
const compat = notionCompat(body);
|
||||
const titleMap = new Map<string, string>();
|
||||
for (const title of knownTitles) titleMap.set(normalizeTitle(title), title);
|
||||
const withWikiLinks = compat.replace(
|
||||
/\[\[([^\]|]+)(?:\|([^\]]+))?\]\]/g,
|
||||
(_whole, target: string, label?: string) =>
|
||||
`<button class="wiki-link" type="button" data-wiki="${escapeHtml(target.trim())}">${escapeHtml((label || target).trim())}</button>`,
|
||||
);
|
||||
let headingIndex = 0;
|
||||
const renderer = new marked.Renderer();
|
||||
renderer.heading = (token) => {
|
||||
const id = `heading-${headingIndex}`;
|
||||
headingIndex += 1;
|
||||
const inlineHtml = token.tokens.length
|
||||
? Parser.parseInline(token.tokens)
|
||||
: escapeHtml(token.text);
|
||||
return `<h${token.depth} id="${id}">${inlineHtml}</h${token.depth}>\n`;
|
||||
};
|
||||
renderer.blockquote = (token) => {
|
||||
const inner = Parser.parse(token.tokens) as string;
|
||||
return `<blockquote class="callout ${calloutTint(token.raw)}">${inner}</blockquote>\n`;
|
||||
};
|
||||
renderer.link = (token) => {
|
||||
const inner = token.tokens.length
|
||||
? Parser.parseInline(token.tokens)
|
||||
: escapeHtml(token.text);
|
||||
const href = token.href || "";
|
||||
if (/notion\.(?:so|site)/i.test(href)) {
|
||||
// Notion 残留链接:认得出是自家页面就转成内部跳转,认不出标"外来页面"。
|
||||
const hit = titleMap.get(normalizeTitle(token.text));
|
||||
if (hit)
|
||||
return `<button class="wiki-link" type="button" data-wiki="${escapeHtml(hit)}">${inner}</button>`;
|
||||
return `<a class="external-link" href="${escapeHtml(href)}" target="_blank" rel="noopener noreferrer">${inner}<span class="external-mark">外来页面</span></a>`;
|
||||
}
|
||||
if (/^https?:\/\//i.test(href))
|
||||
return `<a href="${escapeHtml(href)}" target="_blank" rel="noopener noreferrer">${inner}</a>`;
|
||||
return `<a href="${escapeHtml(href)}">${inner}</a>`;
|
||||
};
|
||||
const html = marked.parse(withWikiLinks, {
|
||||
async: false,
|
||||
gfm: true,
|
||||
breaks: false,
|
||||
renderer,
|
||||
}) as string;
|
||||
return DOMPurify.sanitize(html, {
|
||||
ADD_ATTR: ["data-wiki", "id", "target", "rel"],
|
||||
ADD_TAGS: ["button"],
|
||||
});
|
||||
}
|
||||
|
||||
export function documentOutline(body: string) {
|
||||
const headings: { id: string; level: number; title: string }[] = [];
|
||||
const plain = (tokens: unknown): string =>
|
||||
(tokens as { type: string; text?: string; tokens?: unknown[] }[])
|
||||
.map((token) =>
|
||||
token.tokens && token.tokens.length
|
||||
? plain(token.tokens)
|
||||
: token.text || "",
|
||||
)
|
||||
.join("");
|
||||
marked
|
||||
.lexer(splitFrontmatter(body).content, { gfm: true })
|
||||
.filter((token): token is Tokens.Heading => token.type === "heading")
|
||||
.forEach((token, index) => {
|
||||
if (index >= 24) return;
|
||||
headings.push({
|
||||
id: `heading-${index}`,
|
||||
level: token.depth,
|
||||
title: plain(token.tokens).trim(),
|
||||
});
|
||||
});
|
||||
return headings;
|
||||
}
|
||||
|
||||
export function jumpToHeading(id: string) {
|
||||
const element = document.querySelector<HTMLElement>(
|
||||
`.document-scroll [id="${id}"]`,
|
||||
);
|
||||
if (!element) return;
|
||||
element.scrollIntoView({ behavior: "smooth", block: "start" });
|
||||
element.classList.remove("heading-flash");
|
||||
void element.offsetWidth;
|
||||
element.classList.add("heading-flash");
|
||||
}
|
||||
|
||||
export function MarkdownDocument({
|
||||
body,
|
||||
knownTitles,
|
||||
onWiki,
|
||||
}: {
|
||||
body: string;
|
||||
knownTitles?: readonly string[];
|
||||
onWiki?: (target: string) => void;
|
||||
}) {
|
||||
const parsed = useMemo(() => splitFrontmatter(body), [body]);
|
||||
const html = useMemo(
|
||||
() => markdownHtml(parsed.content, knownTitles),
|
||||
[parsed.content, knownTitles],
|
||||
);
|
||||
return (
|
||||
<article
|
||||
className="markdown-document"
|
||||
onClick={(event) => {
|
||||
const element = (event.target as HTMLElement).closest<HTMLElement>(
|
||||
"[data-wiki]",
|
||||
);
|
||||
if (element?.dataset.wiki && onWiki) onWiki(element.dataset.wiki);
|
||||
}}
|
||||
dangerouslySetInnerHTML={{ __html: html }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
//! 模块形状声明 · 插座制(冰朔 2026-08-15 架构谕)
|
||||
//!
|
||||
//! 软件只认这一个插座形状:每个模块 = manifest(我是谁/插哪个口/从哪来)
|
||||
//! + index(渲染入口导出)。常驻模块住在频道里;将来 origin='repo' 的
|
||||
//! 模块躺代码仓库,人格体按需拉取部署。
|
||||
export const knowledgeRenderManifest = {
|
||||
moduleId: "hololake.knowledge-render",
|
||||
name: "知识渲染件",
|
||||
version: "0.1.0",
|
||||
slot: "knowledge-render",
|
||||
origin: "resident",
|
||||
exports: [
|
||||
"splitFrontmatter",
|
||||
"documentOutline",
|
||||
"jumpToHeading",
|
||||
"MarkdownDocument",
|
||||
],
|
||||
} as const;
|
||||
|
||||
export type ModuleManifest = {
|
||||
moduleId: string;
|
||||
name: string;
|
||||
version: string;
|
||||
slot: string;
|
||||
origin: "resident" | "repo";
|
||||
exports: readonly string[];
|
||||
};
|
||||
|
|
@ -459,6 +459,32 @@ main {
|
|||
overflow: hidden;
|
||||
background: rgba(5, 17, 31, 0.76);
|
||||
}
|
||||
.knowledge-workspace.complete {
|
||||
grid-template-columns: 260px minmax(0, 1fr) 232px;
|
||||
height: min(720px, calc(100vh - 175px));
|
||||
min-height: 560px;
|
||||
}
|
||||
.knowledge-workspace.complete.inspector-closed {
|
||||
grid-template-columns: 260px minmax(0, 1fr);
|
||||
}
|
||||
.knowledge-browser {
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
}
|
||||
.browser-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 4px 2px 13px;
|
||||
}
|
||||
.browser-title strong {
|
||||
font-size: 12px;
|
||||
color: #cad8e6;
|
||||
}
|
||||
.browser-title span {
|
||||
font-size: 10px;
|
||||
color: #6f879e;
|
||||
}
|
||||
.knowledge-workspace > aside {
|
||||
border-right: 1px solid var(--line);
|
||||
padding: 13px;
|
||||
|
|
@ -532,6 +558,197 @@ main {
|
|||
font-family: "SFMono-Regular", Menlo, monospace;
|
||||
font-size: 14px;
|
||||
}
|
||||
.document-workspace {
|
||||
min-height: 0;
|
||||
}
|
||||
.document-scroll {
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
padding: 42px clamp(28px, 5vw, 70px) 70px;
|
||||
}
|
||||
.reader-heading {
|
||||
padding-bottom: 25px;
|
||||
margin-bottom: 25px;
|
||||
border-bottom: 1px solid var(--line);
|
||||
}
|
||||
.reader-heading h1 {
|
||||
font-size: 34px;
|
||||
margin: 0 0 10px;
|
||||
letter-spacing: -0.025em;
|
||||
}
|
||||
.reader-heading p {
|
||||
color: #8097ad;
|
||||
font-size: 12px;
|
||||
margin: 0;
|
||||
}
|
||||
.meta-tags {
|
||||
display: flex;
|
||||
gap: 7px;
|
||||
margin-top: 14px;
|
||||
}
|
||||
.meta-tags span {
|
||||
padding: 4px 8px;
|
||||
border-radius: 7px;
|
||||
color: #b8d8ef;
|
||||
background: rgba(71, 139, 188, 0.14);
|
||||
font-size: 10px;
|
||||
}
|
||||
.markdown-document {
|
||||
color: #cbd8e6;
|
||||
font-size: 15px;
|
||||
line-height: 1.9;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
.markdown-document h1,
|
||||
.markdown-document h2,
|
||||
.markdown-document h3,
|
||||
.markdown-document h4 {
|
||||
color: #f0f5fb;
|
||||
line-height: 1.35;
|
||||
scroll-margin-top: 24px;
|
||||
}
|
||||
.markdown-document h1 {
|
||||
font-size: 28px;
|
||||
margin: 1.4em 0 0.6em;
|
||||
}
|
||||
.markdown-document h2 {
|
||||
font-size: 22px;
|
||||
margin: 1.7em 0 0.55em;
|
||||
}
|
||||
.markdown-document h3 {
|
||||
font-size: 18px;
|
||||
margin: 1.5em 0 0.5em;
|
||||
}
|
||||
.markdown-document p {
|
||||
margin: 0 0 1.1em;
|
||||
}
|
||||
.markdown-document a,
|
||||
.markdown-document .wiki-link {
|
||||
color: #84c9f3;
|
||||
}
|
||||
.markdown-document .wiki-link {
|
||||
display: inline;
|
||||
border: 0;
|
||||
padding: 0 2px;
|
||||
background: transparent;
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 3px;
|
||||
}
|
||||
.markdown-document blockquote.callout {
|
||||
margin: 1.3em 0;
|
||||
padding: 14px 17px;
|
||||
border-left: 3px solid #74bee9;
|
||||
border-radius: 0 10px 10px 0;
|
||||
background: rgba(72, 137, 181, 0.1);
|
||||
}
|
||||
.markdown-document blockquote.callout-amber {
|
||||
border-left-color: var(--gold);
|
||||
background: rgba(205, 153, 64, 0.1);
|
||||
}
|
||||
.markdown-document blockquote.callout-rose {
|
||||
border-left-color: #df8991;
|
||||
background: rgba(190, 83, 94, 0.1);
|
||||
}
|
||||
.markdown-document blockquote.callout-mint {
|
||||
border-left-color: var(--green);
|
||||
background: rgba(81, 170, 119, 0.1);
|
||||
}
|
||||
.markdown-document pre {
|
||||
overflow: auto;
|
||||
padding: 16px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 12px;
|
||||
background: rgba(1, 7, 14, 0.65);
|
||||
}
|
||||
.markdown-document code {
|
||||
font-family: "SFMono-Regular", Menlo, monospace;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
.markdown-document table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin: 1.4em 0;
|
||||
}
|
||||
.markdown-document th,
|
||||
.markdown-document td {
|
||||
padding: 9px 11px;
|
||||
border-bottom: 1px solid var(--line);
|
||||
text-align: left;
|
||||
}
|
||||
.markdown-document img {
|
||||
max-width: 100%;
|
||||
border-radius: 12px;
|
||||
}
|
||||
.heading-flash {
|
||||
animation: headingFlash 1.2s ease;
|
||||
}
|
||||
@keyframes headingFlash {
|
||||
0% {
|
||||
color: var(--gold);
|
||||
text-shadow: 0 0 18px rgba(240, 197, 107, 0.4);
|
||||
}
|
||||
}
|
||||
.document-inspector {
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
border-left: 1px solid var(--line);
|
||||
border-right: 0 !important;
|
||||
padding: 0 !important;
|
||||
}
|
||||
.document-inspector > header {
|
||||
padding: 18px 16px;
|
||||
border-bottom: 1px solid var(--line);
|
||||
}
|
||||
.document-inspector > header strong,
|
||||
.document-inspector > header span {
|
||||
display: block;
|
||||
}
|
||||
.document-inspector > header strong {
|
||||
font-size: 13px;
|
||||
}
|
||||
.document-inspector > header span {
|
||||
color: #72899f;
|
||||
font-size: 10px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
.outline-list {
|
||||
padding: 10px 8px 18px;
|
||||
border-bottom: 1px solid var(--line);
|
||||
}
|
||||
.outline-list button {
|
||||
display: block;
|
||||
width: 100%;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
text-align: left;
|
||||
color: #9fb2c5;
|
||||
font-size: 11px;
|
||||
line-height: 1.45;
|
||||
padding-block: 7px;
|
||||
}
|
||||
.outline-list p {
|
||||
color: #72899f;
|
||||
font-size: 11px;
|
||||
padding: 8px;
|
||||
}
|
||||
.document-evidence {
|
||||
padding: 14px 16px;
|
||||
margin: 0;
|
||||
}
|
||||
.document-evidence div {
|
||||
margin-bottom: 13px;
|
||||
}
|
||||
.document-evidence dt {
|
||||
color: #6f879e;
|
||||
font-size: 10px;
|
||||
}
|
||||
.document-evidence dd {
|
||||
color: #bdcddd;
|
||||
font-size: 11px;
|
||||
line-height: 1.5;
|
||||
margin: 4px 0 0;
|
||||
word-break: break-all;
|
||||
}
|
||||
.split-list {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
|
|
@ -848,6 +1065,13 @@ main {
|
|||
.knowledge-workspace {
|
||||
grid-template-columns: 210px 1fr;
|
||||
}
|
||||
.knowledge-workspace.complete,
|
||||
.knowledge-workspace.complete.inspector-closed {
|
||||
grid-template-columns: 210px minmax(0, 1fr);
|
||||
}
|
||||
.document-inspector {
|
||||
display: none;
|
||||
}
|
||||
.split-list {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue