fix(knowledge): restore classification colors and scroll-synced outline
This commit is contained in:
parent
3466b507c6
commit
71cece74b0
10 changed files with 404 additions and 38 deletions
|
|
@ -17,4 +17,4 @@
|
|||
|
||||
## 知识库人类视觉渲染层纠正
|
||||
|
||||
冰朔指出首轮只有 Markdown 文本编辑器,不能称为完整知识库模块。现已从只读旧产品供体精确拆入 `knowledge-render` 切片,并在新壳中重新接线:目录、阅读渲染、frontmatter、标签、大纲、wikilink、callout、代码块、表格、来源证据、编辑/阅读切换均可见。验收截图为 `design/concepts/hololake-knowledge-human-render-1536x1024.png`;旧产品其他模块未合并。
|
||||
冰朔指出首轮只有 Markdown 文本编辑器,不能称为完整知识库模块;第二次又指出初步渲染仍缺旧版颜色、分类、阅读时间和滚动大纲细节。现已从只读旧产品供体精确拆入 `knowledge-render` 切片,并在新壳中重新接线:文件夹分类树、阅读渲染、frontmatter、稳定标签配色、阅读时间、正文滚动同步高亮大纲、wikilink、callout、代码块、表格、来源证据、编辑/阅读切换均可见。浏览器真实测试在滚动后读回大纲活动项“分类与颜色”;验收截图为 `design/concepts/hololake-knowledge-human-render-1536x1024.png`。旧产品其他模块未合并。
|
||||
|
|
|
|||
|
|
@ -18,13 +18,13 @@
|
|||
"rust_clippy_denied_warnings": "PASS_100",
|
||||
"production_dependency_audit": "0_VULNERABILITIES",
|
||||
"browser_channel_create_message_separation_knowledge_create_edit_save": "PASS_100",
|
||||
"knowledge_human_visual_rendering_frontmatter_outline_wikilink_callout_table_code_evidence": "PASS_100",
|
||||
"knowledge_human_visual_rendering_frontmatter_classification_colors_reading_time_scroll_outline_wikilink_callout_table_code_evidence": "PASS_100",
|
||||
"responsive_viewports": ["1536x1024_PASS", "820x900_PASS"],
|
||||
"developer_id_signature": "PASS_100",
|
||||
"installed_application": "/Applications/HoloLake.app",
|
||||
"installed_version": "1.0.0",
|
||||
"installed_binary_sha256": "50e6206be32ce7c70d42998b6217956aaa99687976e3d9ee383d3671e03c250d",
|
||||
"updater_archive_sha256": "b126efa723415beacfad916621dbecceff7ea3ff24c79e6adacabd5bb343736a",
|
||||
"installed_binary_sha256": "b8ba374fd9e97f318b27dd34d71e02d59637b458d5b8fa0a3cc7b649c1ed6636",
|
||||
"updater_archive_sha256": "bb3405ba849aee9d7dccf71fa4d215fef0bea41dabb910257507c861d24d3b36",
|
||||
"installed_process_readback": "PASS_100",
|
||||
"updater_artifact_signature": "PASS_100",
|
||||
"update_endpoint_http": 204,
|
||||
|
|
|
|||
Binary file not shown.
|
Before Width: | Height: | Size: 43 KiB After Width: | Height: | Size: 77 KiB |
|
|
@ -18,7 +18,7 @@ HoloLake 是公众个人初始化频道语言驱动操作系统。第一版只
|
|||
- 对话和执行事实使用不同来源类型,不把系统文字塞进用户或人格消息。
|
||||
- 公共四域由企业门户和企业服务器承载,不嵌入个人频道。
|
||||
- 第五域、冰朔私人核 `TCS-iZero∞` 及私人语料不进入公众发行。
|
||||
- 当前知识库具备人类视觉阅读层:安全 Markdown 渲染、frontmatter、页面大纲、callout、表格、代码块与 wikilink 跳转;可处理 Markdown 和纯文本文件夹。PDF、Office、网页结构化转换尚未实现。
|
||||
- 当前知识库具备完整人类视觉阅读层:安全 Markdown 渲染、frontmatter、文件夹分类树、稳定标签配色、阅读时间、随正文滚动同步高亮的大纲、callout、表格、代码块与 wikilink 跳转;可处理 Markdown 和纯文本文件夹并保留目录分类。PDF、Office、网页结构化转换尚未实现。
|
||||
- 当前外部 AI 入口是本机来源明确的 inbox/outbox;实时加密通信尚未实现。
|
||||
- 更新客户端只信任光湖控制的 HTTPS 清单与签名;服务器发行清单尚未发布。
|
||||
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@
|
|||
"kind": "PREINSTALLED_USER_MODULE",
|
||||
"state": "LOCAL_INSTALLED_VERIFIED",
|
||||
"source": "src-tauri/src/lib.rs#knowledge",
|
||||
"capabilities": ["CREATE", "READ", "EDIT", "HUMAN_MARKDOWN_RENDERING", "FRONTMATTER", "OUTLINE", "WIKILINK_NAVIGATION", "IMPORT_FOLDER", "EXPORT", "MOVE_TO_TRASH", "PRIVATE_GIT_COMMIT"]
|
||||
"capabilities": ["CREATE", "READ", "EDIT", "HUMAN_MARKDOWN_RENDERING", "FRONTMATTER", "FOLDER_CLASSIFICATION_TREE", "STABLE_TAG_COLORS", "READING_TIME", "SCROLL_SYNCHRONIZED_OUTLINE", "WIKILINK_NAVIGATION", "IMPORT_FOLDER", "EXPORT", "MOVE_TO_TRASH", "PRIVATE_GIT_COMMIT"]
|
||||
},
|
||||
{
|
||||
"module_id": "HLP-MOD-GLP-BRIDGE-0001",
|
||||
|
|
|
|||
|
|
@ -200,7 +200,14 @@ fn copy_import(src: &Path, dst: &Path, count: &mut usize) -> Result<(), String>
|
|||
continue;
|
||||
}
|
||||
if p.is_dir() {
|
||||
copy_import(&p, dst, count)?;
|
||||
let folder_name = clean_title(
|
||||
p.file_name()
|
||||
.and_then(|value| value.to_str())
|
||||
.unwrap_or("未命名分类"),
|
||||
)?;
|
||||
let child = dst.join(folder_name);
|
||||
fs::create_dir_all(&child).map_err(|e| e.to_string())?;
|
||||
copy_import(&p, &child, count)?;
|
||||
continue;
|
||||
}
|
||||
let ext = p
|
||||
|
|
@ -243,7 +250,12 @@ async fn import_knowledge_folder(app: AppHandle) -> Result<Option<MutationReceip
|
|||
};
|
||||
let src = folder.into_path().map_err(|e| e.to_string())?;
|
||||
let root = storage::knowledge_root(&app)?;
|
||||
let dst = root.join("docs").join("导入");
|
||||
let source_name = clean_title(
|
||||
src.file_name()
|
||||
.and_then(|value| value.to_str())
|
||||
.unwrap_or("外部知识库"),
|
||||
)?;
|
||||
let dst = root.join("docs").join("导入").join(source_name);
|
||||
fs::create_dir_all(&dst).map_err(|e| e.to_string())?;
|
||||
let mut count = 0;
|
||||
copy_import(&src, &dst, &mut count)?;
|
||||
|
|
@ -367,4 +379,27 @@ mod tests {
|
|||
fn title_sanitizes_paths() {
|
||||
assert_eq!(clean_title("a/b").unwrap(), "a-b")
|
||||
}
|
||||
#[test]
|
||||
fn folder_import_preserves_classification_tree() {
|
||||
let source = tempfile::tempdir().unwrap();
|
||||
let destination = tempfile::tempdir().unwrap();
|
||||
let category = source.path().join("产品架构");
|
||||
fs::create_dir_all(&category).unwrap();
|
||||
fs::write(category.join("决定.txt"), "分类内容").unwrap();
|
||||
let mut count = 0;
|
||||
copy_import(source.path(), destination.path(), &mut count).unwrap();
|
||||
assert_eq!(count, 1);
|
||||
let imported = fs::read_dir(destination.path().join("产品架构"))
|
||||
.unwrap()
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.unwrap();
|
||||
assert_eq!(imported.len(), 1);
|
||||
assert_eq!(
|
||||
imported[0]
|
||||
.path()
|
||||
.extension()
|
||||
.and_then(|value| value.to_str()),
|
||||
Some("md")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,14 @@
|
|||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { check } from "@tauri-apps/plugin-updater";
|
||||
import { relaunch } from "@tauri-apps/plugin-process";
|
||||
import { Icon } from "./icons";
|
||||
import * as api from "./runtime";
|
||||
import type { KnowledgeDocument, SourceKind, SystemSnapshot } from "./types";
|
||||
import type {
|
||||
KnowledgeDocument,
|
||||
KnowledgeSummary,
|
||||
SourceKind,
|
||||
SystemSnapshot,
|
||||
} from "./types";
|
||||
import {
|
||||
MarkdownDocument,
|
||||
documentOutline,
|
||||
|
|
@ -304,6 +309,120 @@ function ReceiptStream({ data }: { data: SystemSnapshot }) {
|
|||
);
|
||||
}
|
||||
|
||||
const tagTints = ["tag-sky", "tag-violet", "tag-amber", "tag-mint", "tag-rose"];
|
||||
function tagTint(tag: string) {
|
||||
let hash = 0;
|
||||
for (const char of tag) hash = (hash * 31 + (char.codePointAt(0) || 0)) % 997;
|
||||
return tagTints[hash % tagTints.length];
|
||||
}
|
||||
|
||||
interface KnowledgeTreeNode {
|
||||
name: string;
|
||||
key: string;
|
||||
folders: Map<string, KnowledgeTreeNode>;
|
||||
documents: KnowledgeSummary[];
|
||||
}
|
||||
function buildKnowledgeTree(documents: KnowledgeSummary[]) {
|
||||
const root: KnowledgeTreeNode = {
|
||||
name: "知识",
|
||||
key: "",
|
||||
folders: new Map(),
|
||||
documents: [],
|
||||
};
|
||||
for (const document of documents) {
|
||||
const segments = document.path.split("/").filter(Boolean);
|
||||
segments.pop();
|
||||
let cursor = root;
|
||||
for (const segment of segments) {
|
||||
const key = cursor.key ? `${cursor.key}/${segment}` : segment;
|
||||
if (!cursor.folders.has(segment))
|
||||
cursor.folders.set(segment, {
|
||||
name: segment,
|
||||
key,
|
||||
folders: new Map(),
|
||||
documents: [],
|
||||
});
|
||||
cursor = cursor.folders.get(segment)!;
|
||||
}
|
||||
cursor.documents.push(document);
|
||||
}
|
||||
return root;
|
||||
}
|
||||
function treeCount(node: KnowledgeTreeNode): number {
|
||||
return (
|
||||
node.documents.length +
|
||||
[...node.folders.values()].reduce((sum, child) => sum + treeCount(child), 0)
|
||||
);
|
||||
}
|
||||
function KnowledgeTree({
|
||||
node,
|
||||
depth,
|
||||
expanded,
|
||||
active,
|
||||
onToggle,
|
||||
onOpen,
|
||||
}: {
|
||||
node: KnowledgeTreeNode;
|
||||
depth: number;
|
||||
expanded: Set<string>;
|
||||
active: string | null;
|
||||
onToggle: (key: string) => void;
|
||||
onOpen: (path: string) => void;
|
||||
}) {
|
||||
const folders = [...node.folders.values()].sort((a, b) =>
|
||||
a.name.localeCompare(b.name, "zh-CN"),
|
||||
);
|
||||
const documents = [...node.documents].sort((a, b) =>
|
||||
a.title.localeCompare(b.title, "zh-CN"),
|
||||
);
|
||||
return (
|
||||
<>
|
||||
{folders.map((folder) => {
|
||||
const open = depth === 0 || expanded.has(folder.key);
|
||||
return (
|
||||
<div className="tree-branch" key={folder.key}>
|
||||
<button
|
||||
className="tree-folder"
|
||||
style={{ paddingLeft: 10 + depth * 13 }}
|
||||
onClick={() => onToggle(folder.key)}
|
||||
>
|
||||
<span className={open ? "tree-chevron open" : "tree-chevron"}>
|
||||
›
|
||||
</span>
|
||||
<Icon name="folder" />
|
||||
<span>{folder.name}</span>
|
||||
<em>{treeCount(folder)}</em>
|
||||
</button>
|
||||
{open && (
|
||||
<KnowledgeTree
|
||||
node={folder}
|
||||
depth={depth + 1}
|
||||
expanded={expanded}
|
||||
active={active}
|
||||
onToggle={onToggle}
|
||||
onOpen={onOpen}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{documents.map((document) => (
|
||||
<button
|
||||
className={
|
||||
active === document.path ? "tree-document active" : "tree-document"
|
||||
}
|
||||
style={{ paddingLeft: 28 + depth * 13 }}
|
||||
key={document.path}
|
||||
onClick={() => onOpen(document.path)}
|
||||
>
|
||||
<Icon name="file" />
|
||||
<span>{document.title}</span>
|
||||
</button>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function Knowledge({
|
||||
data,
|
||||
refresh,
|
||||
|
|
@ -319,13 +438,17 @@ function Knowledge({
|
|||
[query, setQuery] = useState(""),
|
||||
[newTitle, setNewTitle] = useState<string | null>(null),
|
||||
[editing, setEditing] = useState(false),
|
||||
[inspectorOpen, setInspectorOpen] = useState(true);
|
||||
[inspectorOpen, setInspectorOpen] = useState(true),
|
||||
[expanded, setExpanded] = useState<Set<string>>(new Set(["导入"])),
|
||||
[activeHeadingId, setActiveHeadingId] = useState<string | null>(null);
|
||||
const headingScrollTimer = useRef<number | null>(null);
|
||||
useEffect(() => {
|
||||
if (selected)
|
||||
api.readDocument(selected).then((d) => {
|
||||
setDoc(d);
|
||||
setBody(d.body);
|
||||
setEditing(false);
|
||||
setActiveHeadingId(null);
|
||||
});
|
||||
}, [selected]);
|
||||
const list = useMemo(
|
||||
|
|
@ -335,6 +458,7 @@ function Knowledge({
|
|||
),
|
||||
[data.documents, query],
|
||||
);
|
||||
const tree = useMemo(() => buildKnowledgeTree(list), [list]);
|
||||
const create = async () => {
|
||||
const title = newTitle?.trim();
|
||||
if (title) {
|
||||
|
|
@ -364,8 +488,33 @@ function Knowledge({
|
|||
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)) };
|
||||
return { chars, minutes: Math.max(1, Math.ceil(chars / 400)) };
|
||||
}, [parsed.content]);
|
||||
useEffect(() => {
|
||||
if (!activeHeadingId) return;
|
||||
document
|
||||
.querySelector(".outline-list button.active")
|
||||
?.scrollIntoView({ block: "nearest" });
|
||||
}, [activeHeadingId]);
|
||||
const handleReaderScroll = (event: React.UIEvent<HTMLDivElement>) => {
|
||||
const container = event.currentTarget;
|
||||
if (headingScrollTimer.current !== null) return;
|
||||
headingScrollTimer.current = window.setTimeout(() => {
|
||||
headingScrollTimer.current = null;
|
||||
const containerTop = container.getBoundingClientRect().top;
|
||||
let current: string | null = null;
|
||||
for (const heading of Array.from(
|
||||
container.querySelectorAll<HTMLElement>(
|
||||
"h1[id], h2[id], h3[id], h4[id]",
|
||||
),
|
||||
)) {
|
||||
if (heading.getBoundingClientRect().top - containerTop <= 28)
|
||||
current = heading.id;
|
||||
else break;
|
||||
}
|
||||
setActiveHeadingId(current);
|
||||
}, 100);
|
||||
};
|
||||
const openWiki = (target: string) => {
|
||||
const normalized = target.replace(/[\s·•・\-_|*`]/g, "").toLowerCase();
|
||||
const hit = data.documents.find(
|
||||
|
|
@ -410,18 +559,23 @@ function Knowledge({
|
|||
{list.length === 0 ? (
|
||||
<p className="empty">暂无知识页</p>
|
||||
) : (
|
||||
list.map((d) => (
|
||||
<button
|
||||
className={selected === d.path ? "selected" : ""}
|
||||
key={d.path}
|
||||
onClick={() => setSelected(d.path)}
|
||||
>
|
||||
<strong>{d.title}</strong>
|
||||
<small>
|
||||
{new Date(d.updatedAt).toLocaleDateString("zh-CN")}
|
||||
</small>
|
||||
</button>
|
||||
))
|
||||
<div className="knowledge-tree">
|
||||
<KnowledgeTree
|
||||
node={tree}
|
||||
depth={0}
|
||||
expanded={expanded}
|
||||
active={selected}
|
||||
onToggle={(key) =>
|
||||
setExpanded((current) => {
|
||||
const next = new Set(current);
|
||||
if (next.has(key)) next.delete(key);
|
||||
else next.add(key);
|
||||
return next;
|
||||
})
|
||||
}
|
||||
onOpen={setSelected}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</aside>
|
||||
<section className="document-workspace">
|
||||
|
|
@ -465,7 +619,7 @@ function Knowledge({
|
|||
onChange={(e) => setBody(e.target.value)}
|
||||
/>
|
||||
) : (
|
||||
<div className="document-scroll">
|
||||
<div className="document-scroll" onScroll={handleReaderScroll}>
|
||||
<header className="reader-heading">
|
||||
<h1>{doc.title}</h1>
|
||||
<p>
|
||||
|
|
@ -475,10 +629,23 @@ function Knowledge({
|
|||
{parsed.tags.length > 0 && (
|
||||
<div className="meta-tags">
|
||||
{parsed.tags.map((tag) => (
|
||||
<span key={tag}>{tag}</span>
|
||||
<span key={tag} className={tagTint(tag)}>
|
||||
{tag}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<div className="meta-properties">
|
||||
{Object.entries(parsed.metadata)
|
||||
.filter(([key]) => key !== "tags")
|
||||
.slice(0, 6)
|
||||
.map(([key, value]) => (
|
||||
<span key={key}>
|
||||
<b>{key}</b>
|
||||
{value}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</header>
|
||||
<MarkdownDocument
|
||||
body={doc.body}
|
||||
|
|
@ -509,9 +676,13 @@ function Knowledge({
|
|||
{outline.length ? (
|
||||
outline.map((item) => (
|
||||
<button
|
||||
className={activeHeadingId === item.id ? "active" : ""}
|
||||
key={item.id}
|
||||
style={{ paddingLeft: 12 + (item.level - 1) * 10 }}
|
||||
onClick={() => jumpToHeading(item.id)}
|
||||
onClick={() => {
|
||||
jumpToHeading(item.id);
|
||||
setActiveHeadingId(item.id);
|
||||
}}
|
||||
>
|
||||
{item.title}
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -1,12 +1,63 @@
|
|||
import type { SVGProps } from "react";
|
||||
export function Icon({name,...props}:{name:string}&SVGProps<SVGSVGElement>){
|
||||
const paths:Record<string,React.ReactNode>={
|
||||
channel:<><path d="M4 5.5h16v11H8l-4 3v-14Z"/><path d="M8 10h8M8 13h5"/></>,
|
||||
book:<><path d="M4 5.5A3.5 3.5 0 0 1 7.5 2H11v17H7.5A3.5 3.5 0 0 0 4 22V5.5ZM20 5.5A3.5 3.5 0 0 0 16.5 2H13v17h3.5A3.5 3.5 0 0 1 20 22V5.5Z"/></>,
|
||||
cube:<><path d="m12 2 8 4.5v10L12 21l-8-4.5v-10L12 2Z"/><path d="m4 6.5 8 4.5 8-4.5M12 11v10"/></>,
|
||||
history:<><path d="M3 12a9 9 0 1 0 3-6.7L3 8"/><path d="M3 3v5h5M12 7v5l3 2"/></>,
|
||||
building:<><path d="M4 21V4h10v17M14 9h6v12M8 8h2M8 12h2M8 16h2M17 13h1M17 17h1M2 21h20"/></>,
|
||||
settings:<><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.7 1.7 0 0 0 .3 1.9l.1.1-2.8 2.8-.1-.1a1.7 1.7 0 0 0-1.9-.3 1.7 1.7 0 0 0-1 1.6v.2h-4V21a1.7 1.7 0 0 0-1-1.6 1.7 1.7 0 0 0-1.9.3l-.1.1L4.2 17l.1-.1a1.7 1.7 0 0 0 .3-1.9A1.7 1.7 0 0 0 3 14H2.8v-4H3a1.7 1.7 0 0 0 1.6-1 1.7 1.7 0 0 0-.3-1.9L4.2 7 7 4.2l.1.1a1.7 1.7 0 0 0 1.9.3A1.7 1.7 0 0 0 10 3V2.8h4V3a1.7 1.7 0 0 0 1 1.6 1.7 1.7 0 0 0 1.9-.3l.1-.1L19.8 7l-.1.1a1.7 1.7 0 0 0-.3 1.9 1.7 1.7 0 0 0 1.6 1h.2v4H21a1.7 1.7 0 0 0-1.6 1Z"/></>
|
||||
};
|
||||
return <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.7" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true" {...props}>{paths[name]}</svg>
|
||||
export function Icon({
|
||||
name,
|
||||
...props
|
||||
}: { name: string } & SVGProps<SVGSVGElement>) {
|
||||
const paths: Record<string, React.ReactNode> = {
|
||||
channel: (
|
||||
<>
|
||||
<path d="M4 5.5h16v11H8l-4 3v-14Z" />
|
||||
<path d="M8 10h8M8 13h5" />
|
||||
</>
|
||||
),
|
||||
book: (
|
||||
<>
|
||||
<path d="M4 5.5A3.5 3.5 0 0 1 7.5 2H11v17H7.5A3.5 3.5 0 0 0 4 22V5.5ZM20 5.5A3.5 3.5 0 0 0 16.5 2H13v17h3.5A3.5 3.5 0 0 1 20 22V5.5Z" />
|
||||
</>
|
||||
),
|
||||
cube: (
|
||||
<>
|
||||
<path d="m12 2 8 4.5v10L12 21l-8-4.5v-10L12 2Z" />
|
||||
<path d="m4 6.5 8 4.5 8-4.5M12 11v10" />
|
||||
</>
|
||||
),
|
||||
history: (
|
||||
<>
|
||||
<path d="M3 12a9 9 0 1 0 3-6.7L3 8" />
|
||||
<path d="M3 3v5h5M12 7v5l3 2" />
|
||||
</>
|
||||
),
|
||||
building: (
|
||||
<>
|
||||
<path d="M4 21V4h10v17M14 9h6v12M8 8h2M8 12h2M8 16h2M17 13h1M17 17h1M2 21h20" />
|
||||
</>
|
||||
),
|
||||
settings: (
|
||||
<>
|
||||
<circle cx="12" cy="12" r="3" />
|
||||
<path d="M19.4 15a1.7 1.7 0 0 0 .3 1.9l.1.1-2.8 2.8-.1-.1a1.7 1.7 0 0 0-1.9-.3 1.7 1.7 0 0 0-1 1.6v.2h-4V21a1.7 1.7 0 0 0-1-1.6 1.7 1.7 0 0 0-1.9.3l-.1.1L4.2 17l.1-.1a1.7 1.7 0 0 0 .3-1.9A1.7 1.7 0 0 0 3 14H2.8v-4H3a1.7 1.7 0 0 0 1.6-1 1.7 1.7 0 0 0-.3-1.9L4.2 7 7 4.2l.1.1a1.7 1.7 0 0 0 1.9.3A1.7 1.7 0 0 0 10 3V2.8h4V3a1.7 1.7 0 0 0 1 1.6 1.7 1.7 0 0 0 1.9-.3l.1-.1L19.8 7l-.1.1a1.7 1.7 0 0 0-.3 1.9 1.7 1.7 0 0 0 1.6 1h.2v4H21a1.7 1.7 0 0 0-1.6 1Z" />
|
||||
</>
|
||||
),
|
||||
folder: <path d="M3.5 7.5h6l2-2h9v13h-17Z" />,
|
||||
file: (
|
||||
<>
|
||||
<path d="M6 3.5h8l4 4v13H6Z" />
|
||||
<path d="M14 3.5v4h4" />
|
||||
</>
|
||||
),
|
||||
};
|
||||
return (
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.7"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
aria-hidden="true"
|
||||
{...props}
|
||||
>
|
||||
{paths[name]}
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -485,6 +485,71 @@ main {
|
|||
font-size: 10px;
|
||||
color: #6f879e;
|
||||
}
|
||||
.knowledge-tree {
|
||||
padding: 9px 0 18px;
|
||||
}
|
||||
.tree-branch {
|
||||
position: relative;
|
||||
}
|
||||
.tree-folder,
|
||||
.tree-document {
|
||||
width: 100%;
|
||||
min-height: 38px;
|
||||
display: grid;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
border: 0 !important;
|
||||
border-radius: 8px !important;
|
||||
background: transparent !important;
|
||||
color: #8fa5ba;
|
||||
text-align: left;
|
||||
}
|
||||
.tree-folder {
|
||||
grid-template-columns: 13px 18px minmax(0, 1fr) auto;
|
||||
font-size: 13px;
|
||||
font-weight: 590;
|
||||
}
|
||||
.tree-document {
|
||||
grid-template-columns: 18px minmax(0, 1fr);
|
||||
font-size: 12.5px;
|
||||
font-weight: 480;
|
||||
}
|
||||
.tree-folder svg,
|
||||
.tree-document svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
color: #78bce6;
|
||||
}
|
||||
.tree-folder span,
|
||||
.tree-document span {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.tree-folder em {
|
||||
padding-right: 7px;
|
||||
color: #617a91;
|
||||
font-size: 10px;
|
||||
font-style: normal;
|
||||
}
|
||||
.tree-folder:hover,
|
||||
.tree-document:hover,
|
||||
.tree-document.active {
|
||||
color: #eef6ff;
|
||||
background: rgba(78, 144, 194, 0.12) !important;
|
||||
}
|
||||
.tree-document.active {
|
||||
box-shadow: inset 2px 0 #74bee9;
|
||||
}
|
||||
.tree-chevron {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
font-size: 18px;
|
||||
transition: transform 0.14s ease;
|
||||
}
|
||||
.tree-chevron.open {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
.knowledge-workspace > aside {
|
||||
border-right: 1px solid var(--line);
|
||||
padding: 13px;
|
||||
|
|
@ -593,6 +658,45 @@ main {
|
|||
background: rgba(71, 139, 188, 0.14);
|
||||
font-size: 10px;
|
||||
}
|
||||
.meta-tags .tag-sky {
|
||||
color: #a9dcff;
|
||||
background: rgba(57, 145, 205, 0.18);
|
||||
}
|
||||
.meta-tags .tag-violet {
|
||||
color: #d3b8ff;
|
||||
background: rgba(128, 79, 190, 0.2);
|
||||
}
|
||||
.meta-tags .tag-amber {
|
||||
color: #f6d28a;
|
||||
background: rgba(188, 130, 35, 0.2);
|
||||
}
|
||||
.meta-tags .tag-mint {
|
||||
color: #9fe2ba;
|
||||
background: rgba(48, 148, 91, 0.2);
|
||||
}
|
||||
.meta-tags .tag-rose {
|
||||
color: #ffb2ba;
|
||||
background: rgba(185, 65, 81, 0.2);
|
||||
}
|
||||
.meta-properties {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 7px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
.meta-properties span {
|
||||
display: inline-flex;
|
||||
gap: 6px;
|
||||
padding: 5px 8px;
|
||||
border-radius: 6px;
|
||||
color: #93a9bd;
|
||||
background: rgba(255, 255, 255, 0.035);
|
||||
font-size: 10px;
|
||||
}
|
||||
.meta-properties b {
|
||||
color: #c5d4e1;
|
||||
font-weight: 620;
|
||||
}
|
||||
.markdown-document {
|
||||
color: #cbd8e6;
|
||||
font-size: 15px;
|
||||
|
|
@ -726,6 +830,11 @@ main {
|
|||
line-height: 1.45;
|
||||
padding-block: 7px;
|
||||
}
|
||||
.outline-list button.active {
|
||||
color: #f5d791;
|
||||
background: rgba(198, 146, 49, 0.12);
|
||||
box-shadow: inset 2px 0 var(--gold);
|
||||
}
|
||||
.outline-list p {
|
||||
color: #72899f;
|
||||
font-size: 11px;
|
||||
|
|
|
|||
Loading…
Reference in a new issue