chore: land tree view-state, vault, Pi module split, and repo hygiene
Persist PageTree expand state via control-plane view-state and align chevron/DOM with restored expansion; keep Sidex-style shallow page-tree scan and drop the unused recursive scanner that only added cargo noise. Add password vault workbench routes/runtime/skill/CLI, split page_ai_pi into a module package, and retire Hermes/ACP/OpenHub recycle + root harness evidence from the index while gitignoring recycle and local diag dumps. Archive superseded design/bugs docs under old/, point architecture at ARCHITECTURE.md, and refresh smokes for Pi S1–S7, vault, and editor regressions so the working tree can stay clean.
This commit is contained in:
@@ -1365,8 +1365,9 @@ const AI_ADMIN_SCRIPT: &str = r#"
|
||||
}
|
||||
if (knowledgePanel) {
|
||||
var lightrag = effectivePayload && (effectivePayload.lightragProvider || effectivePayload.lightrag_provider) || {};
|
||||
// S5: hydrate read-only health + directory summary from /api/knowledge-rag/status
|
||||
knowledgePanel.innerHTML =
|
||||
'<div class="mnote-ai-admin-admin-list">' +
|
||||
'<div class="mnote-ai-admin-admin-list" data-ai-admin-knowledge-live>' +
|
||||
'<div class="mnote-ai-admin-admin-card">' +
|
||||
'<div class="mnote-ai-admin-admin-card-head"><h3>LightRAG</h3><span class="mnote-ai-admin-actions-bar">' +
|
||||
renderTag('唯一默认 provider', 'green') + renderTag('MNote facade', 'blue') +
|
||||
@@ -1377,7 +1378,11 @@ const AI_ADMIN_SCRIPT: &str = r#"
|
||||
renderKv('查询入口', 'mnote.knowledge_rag.query') +
|
||||
renderKv('引用回跳', 'citation / open-reference') +
|
||||
renderKv('Pi 接入方式', '只能通过 MNote knowledge facade 查询') +
|
||||
renderKv('Health', '加载中…') +
|
||||
renderKv('Pipeline', '加载中…') +
|
||||
renderKv('Documents', '加载中…') +
|
||||
'</div>' +
|
||||
'<p class="mnote-ai-admin-mini-desc" data-ai-admin-knowledge-summary>正在读取 /api/knowledge-rag/status…</p>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<div class="mnote-ai-admin-admin-card">' +
|
||||
@@ -1385,10 +1390,60 @@ const AI_ADMIN_SCRIPT: &str = r#"
|
||||
renderTag('复用 allowed roots', 'green') + renderTag('不引入第二套 RAG', 'orange') +
|
||||
'</span></div>' +
|
||||
'<div class="mnote-ai-admin-admin-card-body">' +
|
||||
'<p class="mnote-ai-admin-mini-desc">资料 source/index/status 后续接 /api/knowledge-rag/*;目录授权继续来自 MNote allowed roots,不在 AI 管理页维护第二套目录。</p>' +
|
||||
'<p class="mnote-ai-admin-mini-desc">目录授权继续来自 MNote allowed roots(directory_grants);本面板只读展示 LightRAG health / pipeline / documents 摘要,不维护第二套目录。</p>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
requestJson('/api/knowledge-rag/status', { method: 'GET' }).then(function (statusPayload) {
|
||||
if (!knowledgePanel) return;
|
||||
var health = statusPayload && statusPayload.health || {};
|
||||
var healthOk = health.ok === true || (health.health && health.health.ok === true);
|
||||
var healthLabel = healthOk ? 'ok' : (health.message || health.code || 'unavailable');
|
||||
var pipeline = statusPayload && statusPayload.pipeline || {};
|
||||
var pipelineLabel = pipeline.ok === false
|
||||
? (pipeline.message || pipeline.code || 'error')
|
||||
: (pipeline.status || pipeline.phase || pipeline.state || (pipeline.ok === true ? 'ok' : JSON.stringify(pipeline).slice(0, 80)));
|
||||
var docs = statusPayload && statusPayload.documents || {};
|
||||
var docList = Array.isArray(docs.documents) ? docs.documents : [];
|
||||
var groups = docs.rawStatusGroups || {};
|
||||
var groupKeys = Object.keys(groups || {});
|
||||
var groupSummary = groupKeys.length
|
||||
? groupKeys.map(function (k) { return k + '=' + groups[k]; }).join(', ')
|
||||
: (docList.length + ' docs');
|
||||
var registry = statusPayload && statusPayload.registry || null;
|
||||
var entryCount = registry && Array.isArray(registry.entries) ? registry.entries.length : 0;
|
||||
var rootCount = registry && Array.isArray(registry.indexed_roots || registry.indexedRoots)
|
||||
? (registry.indexed_roots || registry.indexedRoots).length
|
||||
: 0;
|
||||
var provider = (statusPayload && statusPayload.provider) || lightrag.provider || 'lightrag';
|
||||
knowledgePanel.innerHTML =
|
||||
'<div class="mnote-ai-admin-admin-list" data-ai-admin-knowledge-live>' +
|
||||
'<div class="mnote-ai-admin-admin-card">' +
|
||||
'<div class="mnote-ai-admin-admin-card-head"><h3>LightRAG</h3><span class="mnote-ai-admin-actions-bar">' +
|
||||
renderTag(healthOk ? 'health ok' : 'health down', healthOk ? 'green' : 'orange') +
|
||||
renderTag(String(provider), 'blue') +
|
||||
'</span></div>' +
|
||||
'<div class="mnote-ai-admin-admin-card-body">' +
|
||||
'<div class="mnote-ai-admin-kv-grid">' +
|
||||
renderKv('Provider', provider) +
|
||||
renderKv('Endpoint', (statusPayload && statusPayload.endpoint) || '-') +
|
||||
renderKv('Health', healthLabel) +
|
||||
renderKv('Pipeline', String(pipelineLabel)) +
|
||||
renderKv('Documents', groupSummary) +
|
||||
renderKv('Registry entries', String(entryCount)) +
|
||||
renderKv('Indexed roots', String(rootCount)) +
|
||||
renderKv('查询入口', 'mnote.knowledge_rag.query') +
|
||||
'</div>' +
|
||||
'<p class="mnote-ai-admin-mini-desc" data-ai-admin-knowledge-summary>只读摘要来自 /api/knowledge-rag/status;目录授权真相仍是 control-plane directory_grants / allowed roots。</p>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
}).catch(function (err) {
|
||||
if (!knowledgePanel) return;
|
||||
var msg = err && err.message ? err.message : 'status unavailable';
|
||||
var summary = knowledgePanel.querySelector('[data-ai-admin-knowledge-summary]');
|
||||
if (summary) summary.textContent = 'LightRAG status 读取失败: ' + msg;
|
||||
});
|
||||
}
|
||||
if (healthGrid) {
|
||||
healthGrid.innerHTML = [
|
||||
@@ -2747,10 +2802,14 @@ const AI_ADMIN_SCRIPT: &str = r#"
|
||||
collectSkillsFromDOM().forEach(function(skill) {
|
||||
var id = skill.id || skill.name;
|
||||
if (!id) return;
|
||||
// S2: preserve source/risk/scopes so delete/save does not soft-break skills.
|
||||
body.skills[id] = {
|
||||
name: skill.name,
|
||||
enabled: skill.enabled,
|
||||
description: skill.description || ''
|
||||
description: skill.description || '',
|
||||
source: skill.source || '',
|
||||
riskLevel: skill.riskLevel || skill.risk_level || 'low',
|
||||
requiredScopes: skill.requiredScopes || skill.required_scopes || []
|
||||
};
|
||||
});
|
||||
}
|
||||
@@ -2768,7 +2827,10 @@ const AI_ADMIN_SCRIPT: &str = r#"
|
||||
networkPolicy: server.networkPolicy || 'deny-all',
|
||||
secretRefs: server.secretRefs || [],
|
||||
facadeOnly: server.facadeOnly !== false,
|
||||
sandbox: true
|
||||
sandbox: true,
|
||||
description: server.description || '',
|
||||
riskLevel: server.riskLevel || server.risk_level || 'medium',
|
||||
requiredScopes: server.requiredScopes || server.required_scopes || []
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
@@ -97,8 +97,18 @@ pub(super) fn DocumentPane(model: DocumentPaneViewModel) -> impl IntoView {
|
||||
data-pane-role={pane_role_attr_three}
|
||||
data-title-endpoint="/api/documents/title"
|
||||
rows="1"
|
||||
readonly
|
||||
data-document-user-readonly-mode="true"
|
||||
>{model.title.clone()}</textarea>
|
||||
</h1>
|
||||
<button
|
||||
type="button"
|
||||
class="document-edit-mode-toggle"
|
||||
data-document-edit-mode-toggle="true"
|
||||
aria-pressed="false"
|
||||
data-document-editing="false"
|
||||
title="进入编辑模式"
|
||||
>"开始编辑"</button>
|
||||
</div>
|
||||
<div class="document-shell-meta" aria-label="页面元信息">
|
||||
<span data-page-title-current="true">{model.title.clone()}</span>
|
||||
@@ -161,6 +171,9 @@ pub fn DocumentPage(
|
||||
/// workspace shell 侧栏 sections HTML(可选)
|
||||
#[prop(optional)]
|
||||
workspace_sidebar_html: Option<String>,
|
||||
/// 顶栏页面祖先链 HTML(可选)
|
||||
#[prop(optional)]
|
||||
breadcrumb_html: Option<String>,
|
||||
/// Page Aggregate 子树 JSON(可选)
|
||||
#[prop(optional)]
|
||||
page_subtree_json: Option<String>,
|
||||
@@ -194,6 +207,9 @@ pub fn DocumentPage(
|
||||
/// 是否启用树实时流
|
||||
#[prop(optional, default = true)]
|
||||
enable_tree_live: bool,
|
||||
/// 密码箱导航 SSR href(含 local_folder 上下文)
|
||||
#[prop(optional)]
|
||||
vault_nav_href: Option<String>,
|
||||
) -> impl IntoView {
|
||||
let has_page_subtree = page_subtree_json
|
||||
.as_deref()
|
||||
@@ -293,7 +309,7 @@ pub fn DocumentPage(
|
||||
hide_title_header: secondary_hide_title_header,
|
||||
};
|
||||
view! {
|
||||
<PageLayout current_nav="documents" sidebar_tree_html={sidebar_tree_html.unwrap_or_default()} workspace_name={workspace_name.unwrap_or_default()} workspace_sidebar_html={workspace_sidebar_html.unwrap_or_default()} topbar_title={title.clone()} show_admin_access_policy={show_admin_access_policy} enable_tree_live={enable_tree_live}>
|
||||
<PageLayout current_nav="documents" sidebar_tree_html={sidebar_tree_html.unwrap_or_default()} workspace_name={workspace_name.unwrap_or_default()} workspace_sidebar_html={workspace_sidebar_html.unwrap_or_default()} topbar_title={title.clone()} breadcrumb_html={breadcrumb_html.unwrap_or_default()} show_admin_access_policy={show_admin_access_policy} enable_tree_live={enable_tree_live} vault_nav_href={vault_nav_href.unwrap_or_default()}>
|
||||
<div
|
||||
class="document-workspace"
|
||||
data-testid="mnote-document-workspace"
|
||||
|
||||
@@ -21,6 +21,9 @@ pub fn HomePage(
|
||||
/// workspace shell 侧栏 sections HTML(可选)
|
||||
#[prop(optional)]
|
||||
workspace_sidebar_html: Option<String>,
|
||||
/// 顶栏页面祖先链 HTML(可选)
|
||||
#[prop(optional)]
|
||||
breadcrumb_html: Option<String>,
|
||||
/// 当前选中的页面 id(可选)
|
||||
#[prop(optional)]
|
||||
active_page_id: Option<String>,
|
||||
@@ -36,6 +39,9 @@ pub fn HomePage(
|
||||
/// 是否启用树实时流
|
||||
#[prop(optional, default = true)]
|
||||
enable_tree_live: bool,
|
||||
/// 密码箱导航 SSR href(含 local_folder 上下文)
|
||||
#[prop(optional)]
|
||||
vault_nav_href: Option<String>,
|
||||
) -> impl IntoView {
|
||||
let active_page_id = active_page_id.unwrap_or_default();
|
||||
let active_page_title = active_page_title
|
||||
@@ -54,7 +60,7 @@ pub fn HomePage(
|
||||
.map(|workspace_id| format!("/documents/{active_page_id}?workspaceId={workspace_id}"))
|
||||
.unwrap_or_else(|| format!("/documents/{active_page_id}"));
|
||||
view! {
|
||||
<PageLayout current_nav="home" sidebar_tree_html={sidebar_tree_html.unwrap_or_default()} workspace_name={workspace_name.unwrap_or_default()} workspace_sidebar_html={workspace_sidebar_html.unwrap_or_default()} topbar_title={active_page_title.clone()} show_admin_access_policy={show_admin_access_policy} enable_tree_live={enable_tree_live}>
|
||||
<PageLayout current_nav="home" sidebar_tree_html={sidebar_tree_html.unwrap_or_default()} workspace_name={workspace_name.unwrap_or_default()} workspace_sidebar_html={workspace_sidebar_html.unwrap_or_default()} topbar_title={active_page_title.clone()} breadcrumb_html={breadcrumb_html.unwrap_or_default()} show_admin_access_policy={show_admin_access_policy} enable_tree_live={enable_tree_live} vault_nav_href={vault_nav_href.unwrap_or_default()}>
|
||||
{move || if has_active_page {
|
||||
view! {
|
||||
<main class="document-shell document-shell--workspace-entry" data-root-active-page-id={active_page_id.clone()} data-editor-host="leptos_tiptap_island">
|
||||
|
||||
@@ -62,15 +62,25 @@ pub fn PageLayout(
|
||||
/// 顶栏当前页面标题(可选)
|
||||
#[prop(optional)]
|
||||
topbar_title: Option<String>,
|
||||
/// 顶栏页面祖先链 HTML(可选)
|
||||
#[prop(optional)]
|
||||
breadcrumb_html: Option<String>,
|
||||
/// 是否显示管理员授权能力
|
||||
#[prop(optional)]
|
||||
show_admin_access_policy: bool,
|
||||
/// 是否启用树实时流
|
||||
#[prop(optional, default = true)]
|
||||
enable_tree_live: bool,
|
||||
/// 密码箱导航 SSR href(含 sourceKind/rootUri,避免首击全页丢失上下文)
|
||||
#[prop(optional)]
|
||||
vault_nav_href: Option<String>,
|
||||
) -> impl IntoView {
|
||||
let _ = show_admin_access_policy;
|
||||
let sidebar_tree_html = sidebar_tree_html.unwrap_or_default();
|
||||
let vault_nav_href = vault_nav_href
|
||||
.map(|value| value.trim().to_string())
|
||||
.filter(|value| !value.is_empty())
|
||||
.unwrap_or_else(|| "/vault".to_string());
|
||||
|
||||
let ws_name = workspace_name
|
||||
.map(|value| value.trim().to_string())
|
||||
@@ -80,6 +90,15 @@ pub fn PageLayout(
|
||||
.map(|value| value.trim().to_string())
|
||||
.filter(|value| !value.is_empty())
|
||||
.unwrap_or_else(|| "个人".to_string());
|
||||
let breadcrumb_html = breadcrumb_html
|
||||
.map(|value| value.trim().to_string())
|
||||
.filter(|value| !value.is_empty())
|
||||
.unwrap_or_else(|| {
|
||||
format!(
|
||||
r#"<span class="wolai-breadcrumb-current"><span class="material-symbols-outlined wolai-home-icon" data-icon="home" aria-hidden="true"></span><span data-page-title-current="true">{}</span></span>"#,
|
||||
crate::routes::web_shell::escape_html(&topbar_title),
|
||||
)
|
||||
});
|
||||
let sidebar_sections_html = workspace_sidebar_html
|
||||
.map(|value| value.trim().to_string())
|
||||
.filter(|value| !value.is_empty())
|
||||
@@ -135,7 +154,7 @@ pub fn PageLayout(
|
||||
<a href="/graph" title="关系图" aria-label="关系图"><span class="material-symbols-outlined nav-icon" data-icon="account_tree" aria-hidden="true"></span></a>
|
||||
<button type="button" title="导航页" aria-label="导航页" data-mnote-action="open-navigation-page"><span class="material-symbols-outlined nav-icon" data-icon="home" aria-hidden="true"></span></button>
|
||||
<a href="/help" title="帮助" aria-label="帮助"><span class="material-symbols-outlined nav-icon" data-icon="help" aria-hidden="true"></span></a>
|
||||
<a href="/files" title="文件" aria-label="文件"><span class="material-symbols-outlined nav-icon" data-icon="inventory_2" aria-hidden="true"></span></a>
|
||||
<a href={vault_nav_href.clone()} class:active={current_nav == "vault"} title="密码箱" aria-label="密码箱" data-testid="mnote-nav-vault" data-mnote-nav="vault"><span class="material-symbols-outlined nav-icon" data-icon="lock" aria-hidden="true"></span></a>
|
||||
<button type="button" title="打开本地文件夹" aria-label="打开本地文件夹" data-mnote-action="open-local-folder"><span class="material-symbols-outlined nav-icon" data-icon="folder_open" aria-hidden="true"></span></button>
|
||||
<button
|
||||
type="button"
|
||||
@@ -172,9 +191,9 @@ pub fn PageLayout(
|
||||
<div class="wolai-topbar-left">
|
||||
<button type="button" class="wolai-icon-button wolai-menu-button" aria-label="切换侧栏" data-state="closed" aria-haspopup="true" aria-expanded="false" aria-pressed="false" data-mnote-action="toggle-sidebar" data-testid="wolai-sidebar-toggle"><span class="material-symbols-outlined" data-icon="menu" aria-hidden="true"></span></button>
|
||||
<nav class="wolai-breadcrumb" aria-label="页面路径">
|
||||
<span class="wolai-breadcrumb-root">{ws_name.clone()}</span>
|
||||
<a class="wolai-breadcrumb-root wolai-breadcrumb-link" href="/" aria-label="返回工作区首页">{ws_name.clone()}</a>
|
||||
<span class="wolai-breadcrumb-separator" aria-hidden="true">"›"</span>
|
||||
<span class="wolai-breadcrumb-current"><span class="material-symbols-outlined wolai-home-icon" data-icon="home" aria-hidden="true"></span><span data-page-title-current="true">{topbar_title}</span></span>
|
||||
<div class="wolai-breadcrumb-pages" data-breadcrumb-pages="true" inner_html={breadcrumb_html}></div>
|
||||
</nav>
|
||||
</div>
|
||||
<div class="wolai-topbar-actions" aria-label="页面操作">
|
||||
@@ -288,10 +307,12 @@ mod tests {
|
||||
assert!(SIDEBAR_PAGE_SETTINGS_RUNTIME_JS.contains("/api/ui/preferences"));
|
||||
assert!(!SIDEBAR_PAGE_SETTINGS_RUNTIME_JS.contains("localStorage.setItem"));
|
||||
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("restoreSidebarTreeTab"));
|
||||
assert!(SIDEBAR_TREE_RUNTIME_JS
|
||||
.contains("applySearchSwitchState(overlay, { knowledge: false"));
|
||||
assert!(!SIDEBAR_TREE_RUNTIME_JS
|
||||
.contains("applySearchSwitchState(overlay, { knowledge: true"));
|
||||
assert!(
|
||||
SIDEBAR_TREE_RUNTIME_JS.contains("applySearchSwitchState(overlay, { knowledge: false")
|
||||
);
|
||||
assert!(
|
||||
!SIDEBAR_TREE_RUNTIME_JS.contains("applySearchSwitchState(overlay, { knowledge: true")
|
||||
);
|
||||
assert!(SIDEBAR_TREE_RUNTIME_JS
|
||||
.contains("data-search-switch=\"knowledge\" role=\"switch\" aria-checked=\"false\""));
|
||||
assert!(!SIDEBAR_TREE_RUNTIME_JS
|
||||
@@ -400,6 +421,29 @@ mod tests {
|
||||
.contains("var expanded = expandable && item.expandedByDefault !== false"),
|
||||
"PageTree 不能只依赖 projection expandedByDefault 决定展开"
|
||||
);
|
||||
assert!(
|
||||
render_page_rows.contains("item.expandedByDefault === true")
|
||||
|| render_page_rows.contains("expandedByDefault === true"),
|
||||
"无 user view-state 时 PageTree 只能按 expandedByDefault === true 展开(与 SSR 一致)"
|
||||
);
|
||||
|
||||
// PageTree view-state scope must be stable "root" (Sidex workspace-scoped),
|
||||
// not current fileTreeScope — otherwise refresh under a folder loses expands.
|
||||
let view_scope =
|
||||
js_function_body(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS, "sidebarTreeViewScope");
|
||||
assert!(
|
||||
view_scope.contains("pagetree") && view_scope.contains("'root'"),
|
||||
"pagetree view-state scope must pin to root"
|
||||
);
|
||||
assert!(
|
||||
SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS
|
||||
.contains("function restorePersistedPageTreeExpansionState"),
|
||||
"runtime must restore pagetree expandedIds after refresh"
|
||||
);
|
||||
assert!(
|
||||
SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains("function setPageTreeExpandPresentation"),
|
||||
"runtime must own a single presentation write path for page expand"
|
||||
);
|
||||
|
||||
let render_file_rows =
|
||||
js_function_body(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS, "renderFileRows");
|
||||
@@ -407,6 +451,154 @@ mod tests {
|
||||
assert!(render_file_rows.contains("expandedRelativePaths"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn page_tree_first_click_expands_ssr_children_without_waiting_for_fetch() {
|
||||
// Shallow SSR paints nested rows under a collapsed container without
|
||||
// data-page-tree-children-loaded. First toggle must expand immediately.
|
||||
let toggle_children =
|
||||
js_function_body(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS, "toggleChildren");
|
||||
assert!(
|
||||
toggle_children.contains("pageTreeRowIsOpen(row)"),
|
||||
"toggleChildren must use visual open state, not aria-expanded alone"
|
||||
);
|
||||
assert!(
|
||||
toggle_children.contains("children.children.length > 0"),
|
||||
"toggleChildren must expand when SSR already rendered child rows"
|
||||
);
|
||||
assert!(
|
||||
toggle_children.contains("data-page-tree-children-loaded"),
|
||||
"toggleChildren must mark SSR children as loaded on first expand"
|
||||
);
|
||||
|
||||
let load_page_children =
|
||||
js_function_body(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS, "loadPageTreeChildren");
|
||||
assert!(
|
||||
load_page_children.contains("children.children.length > 0"),
|
||||
"loadPageTreeChildren must treat non-empty SSR children as already loaded"
|
||||
);
|
||||
assert!(
|
||||
load_page_children.contains("Optimistic UI")
|
||||
|| load_page_children.contains("children.classList.remove('tree-children--collapsed')"),
|
||||
"lazy expand must open chevron/container before network"
|
||||
);
|
||||
|
||||
// Late view-state GET must not snap shut an expand the user just did
|
||||
// (privacy window / cold localStorage is the common repro).
|
||||
let load_view_state =
|
||||
js_function_body(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS, "loadSidebarTreeViewState");
|
||||
assert!(
|
||||
load_view_state.contains("current.hasUserState"),
|
||||
"async view-state load must merge with in-session expands"
|
||||
);
|
||||
assert!(
|
||||
load_view_state.contains("current.expandedIds.forEach"),
|
||||
"async view-state load must union expandedIds from the session"
|
||||
);
|
||||
|
||||
let apply_page_state =
|
||||
js_function_body(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS, "applyPageTreeExpansionState");
|
||||
assert!(
|
||||
apply_page_state.contains("pageTreeRowIsOpen(row)"),
|
||||
"applying view-state must preserve rows that are actually open this session"
|
||||
);
|
||||
|
||||
// Boot must reconcile false "expanded" arrows before the first click,
|
||||
// apply local view-state, and restore persisted expands (Sidex setInput).
|
||||
assert!(
|
||||
SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains("function syncPageTreeExpandVisualState"),
|
||||
"runtime must ship syncPageTreeExpandVisualState"
|
||||
);
|
||||
let install = js_function_body(
|
||||
SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS,
|
||||
"installTreeLiveApplyEventListeners",
|
||||
);
|
||||
assert!(
|
||||
install.contains("syncPageTreeExpandVisualState()"),
|
||||
"install must run expand/chevron sync on boot"
|
||||
);
|
||||
assert!(
|
||||
install.contains("applyPageTreeExpansionState")
|
||||
|| install.contains("scheduleRestorePersistedPageTreeExpansionState"),
|
||||
"install must apply/restore pagetree view-state on boot"
|
||||
);
|
||||
|
||||
let boot_state =
|
||||
js_function_body(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS, "sidebarTreeViewStateFor");
|
||||
assert!(
|
||||
boot_state.contains("applySidebarTreeViewState"),
|
||||
"localStorage view-state must apply to DOM immediately (not wait for API)"
|
||||
);
|
||||
|
||||
// PageTree view-state scope must stay stable "root" (not fileTreeScope),
|
||||
// otherwise refresh/navigation loses expandedIds under a different key.
|
||||
let scope_fn = js_function_body(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS, "sidebarTreeViewScope");
|
||||
assert!(
|
||||
scope_fn.contains("pagetree") && scope_fn.contains("'root'"),
|
||||
"pagetree view-state scope must be stable root, not current fileTreeScope"
|
||||
);
|
||||
|
||||
let restore_fn = js_function_body(
|
||||
SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS,
|
||||
"restorePersistedPageTreeExpansionState",
|
||||
);
|
||||
assert!(
|
||||
!restore_fn.is_empty(),
|
||||
"runtime must restore persisted page-tree expands after refresh"
|
||||
);
|
||||
assert!(
|
||||
restore_fn.contains("loadPageTreeChildren") || restore_fn.contains("setTreeRowExpanded"),
|
||||
"restore must open SSR children or hydrate empty bodies"
|
||||
);
|
||||
|
||||
let presentation =
|
||||
js_function_body(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS, "setPageTreeExpandPresentation");
|
||||
assert!(
|
||||
presentation.contains("data-expanded") && presentation.contains("aria-expanded"),
|
||||
"single write path must set both aria-expanded and data-expanded"
|
||||
);
|
||||
|
||||
let render_rows = js_function_body(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS, "renderPageRows");
|
||||
assert!(
|
||||
render_rows.contains("expandedByDefault === true")
|
||||
|| render_rows.contains("item.expandedByDefault === true"),
|
||||
"client default expand must match SSR (only true, not !== false)"
|
||||
);
|
||||
assert!(
|
||||
render_rows.contains("data-expanded="),
|
||||
"client-rendered page rows must emit data-expanded for chevron CSS"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sidebar_tree_toggle_uses_wolai_style_rotating_chevron() {
|
||||
// Hide non-rotating SSR SVG; show solid triangle via ::before that rotates 90°.
|
||||
const MAIN_CSS: &str = include_str!("../styles/components/main.css");
|
||||
assert!(
|
||||
MAIN_CSS.contains(".sidebar-tree .tree-toggle .tree-toggle-icon")
|
||||
|| MAIN_CSS.contains(".tree-toggle > svg"),
|
||||
"main.css must hide the static SVG chevron inside tree-toggle"
|
||||
);
|
||||
assert!(
|
||||
MAIN_CSS.contains(".sidebar-tree .tree-toggle::before"),
|
||||
"main.css must draw Wolai-style triangle via ::before"
|
||||
);
|
||||
assert!(
|
||||
MAIN_CSS.contains("rotate(90deg)"),
|
||||
"main.css must rotate chevron when expanded"
|
||||
);
|
||||
assert!(
|
||||
MAIN_CSS.contains("data-expanded=\"true\"")
|
||||
|| MAIN_CSS.contains("[data-expanded=\"true\"]"),
|
||||
"main.css must key rotation off data-expanded (real fold state)"
|
||||
);
|
||||
// Icon must track real fold state (children container), not only aria.
|
||||
assert!(
|
||||
MAIN_CSS.contains("tree-children--collapsed")
|
||||
&& MAIN_CSS.contains(":has(> .tree-children"),
|
||||
"main.css must bind chevron rotation to actual children open/collapsed state"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn page_layout_hides_public_state_and_exposes_sidebar_shortcut_star() {
|
||||
let html = crate::ssr::render_view(leptos::view! {
|
||||
@@ -650,6 +842,30 @@ mod tests {
|
||||
assert!(!folder_branch.contains("window.location.assign"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vault_nav_soft_opens_without_replacing_sidebar_tree() {
|
||||
// Left-rail 密码箱 must soft-swap .mnote-content so PageTree expand state survives.
|
||||
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("function openVaultWorkbenchSoft"));
|
||||
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("window.__mnoteOpenVaultWorkbench"));
|
||||
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("void openVaultWorkbenchSoft(vaultUrl)"));
|
||||
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("buildVaultWorkbenchShellHtml"));
|
||||
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("article.mnote-content"));
|
||||
// Must not full-assign when soft path is taken (vault click branch).
|
||||
let vault_branch = SIDEBAR_TREE_RUNTIME_JS
|
||||
.split("var vaultNavLink = closestAction(")
|
||||
.nth(1)
|
||||
.and_then(|rest| rest.split("var localFolderTrigger").next())
|
||||
.expect("vault nav click branch exists");
|
||||
assert!(
|
||||
vault_branch.contains("openVaultWorkbenchSoft"),
|
||||
"vault click must soft-open workbench"
|
||||
);
|
||||
assert!(
|
||||
!vault_branch.contains("window.location.assign(nextHref)"),
|
||||
"vault soft-open must not full-navigate on primary click"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn page_ai_context_uses_page_aggregate_subtree_as_single_truth() {
|
||||
assert!(
|
||||
@@ -1648,6 +1864,17 @@ mod tests {
|
||||
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains("data-mnote-tree-live-error-schema"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sidebar_filetree_watch_fallback_keeps_refresh_scoped() {
|
||||
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS
|
||||
.contains("data-mnote-local-folder-watch-batch-fallback-scoped"));
|
||||
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains(
|
||||
"addCommandRefreshParent(fallbackParents, parentRelativePathForPath(relativePath))"
|
||||
));
|
||||
assert!(!SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS
|
||||
.contains("fallbackResync === true || batch.requiresResync === true)) {\n document.documentElement.setAttribute('data-mnote-local-folder-watch-batch-fallback', String(batch.revision || 'resync'));\n void refreshLocalFolderSidebarSnapshot();"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sidebar_filetree_runtime_has_view_state_namespace() {
|
||||
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains("var fileTreeViewState = {"));
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
/// - `styles/components/search.css`: 搜索弹窗组件
|
||||
/// - `styles/components/page-ai.css`: Page AI 仪表盘组件
|
||||
/// - `styles/components/ui-debug.css`: UI Debug 组件矩阵
|
||||
/// - `styles/components/vault.css`: 密码箱 workbench
|
||||
pub const MNOTE_CSS: &str = concat!(
|
||||
include_str!("styles/tokens.css"),
|
||||
"\n",
|
||||
@@ -37,6 +38,8 @@ pub const MNOTE_CSS: &str = concat!(
|
||||
include_str!("styles/components/page-ai.css"),
|
||||
"\n",
|
||||
include_str!("styles/components/ui-debug.css"),
|
||||
"\n",
|
||||
include_str!("styles/components/vault.css"),
|
||||
);
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -215,8 +218,16 @@ mod tests {
|
||||
fn mnote_css_is_reasonably_sized() {
|
||||
// 至少 2000 字符才能包含完整样式
|
||||
assert!(MNOTE_CSS.len() > 2000);
|
||||
// CSS 已拆分为模块化文件,通过 concat!(include_str!()) 组装,
|
||||
// 仍保持在可审阅范围内。
|
||||
assert!(MNOTE_CSS.len() < 190000);
|
||||
// CSS 已拆分为模块化文件,通过 concat!(include_str!()) 组装;
|
||||
// 当前包含主壳、Page AI、搜索、toast、debug 与 vault 样式,继续用上限防止意外重复打包。
|
||||
assert!(MNOTE_CSS.len() < 220000);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mnote_css_contains_vault_workbench_selectors() {
|
||||
assert!(MNOTE_CSS.contains(".mnote-vault-workbench"));
|
||||
assert!(MNOTE_CSS.contains(".mnote-vault-list-item"));
|
||||
assert!(MNOTE_CSS.contains(".mnote-vault-secret-value"));
|
||||
assert!(MNOTE_CSS.contains("[data-testid=\"mnote-nav-vault\"]") || MNOTE_CSS.contains(".mnote-vault-header"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -324,7 +324,7 @@
|
||||
.sidebar-tree .tree-spacer {
|
||||
width: 20px;
|
||||
height: 24px;
|
||||
color: #B8B5AF;
|
||||
color: #9B968E;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
@@ -335,29 +335,60 @@
|
||||
justify-content: center;
|
||||
line-height: 1;
|
||||
font-size: 0;
|
||||
/* Wolai-style: one clear chevron via ::before; hide SSR/JS SVG that does not rotate. */
|
||||
}
|
||||
|
||||
.sidebar-tree .tree-toggle .tree-toggle-icon,
|
||||
.sidebar-tree .tree-toggle > svg {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.sidebar-tree .tree-toggle:hover {
|
||||
background: rgba(27, 28, 28, 0.06);
|
||||
color: #6F6A62;
|
||||
}
|
||||
|
||||
/* Collapsed: solid right-pointing triangle (Wolai ▶). Expanded: rotate to ▼.
|
||||
Prefer real fold state: aria-expanded / data-expanded must stay in lockstep
|
||||
with .tree-children--collapsed (syncPageTreeExpandVisualState). */
|
||||
.sidebar-tree .tree-toggle::before {
|
||||
content: "";
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-top: 4px solid transparent;
|
||||
border-bottom: 4px solid transparent;
|
||||
border-left: 5px solid currentColor;
|
||||
border-style: solid;
|
||||
border-width: 4.5px 0 4.5px 7px;
|
||||
border-color: transparent transparent transparent currentColor;
|
||||
display: block;
|
||||
transform-origin: 50% 50%;
|
||||
transform-origin: 40% 50%;
|
||||
transition: transform 0.12s ease;
|
||||
}
|
||||
|
||||
.sidebar-tree .tree-row[aria-expanded="true"] > .tree-toggle::before {
|
||||
/* Prefer data-expanded (set with children visibility) so aria alone cannot
|
||||
* leave the chevron open while .tree-children--collapsed is present. */
|
||||
.sidebar-tree .tree-row[data-shell-mode="page"][data-expanded="true"] > .tree-toggle::before,
|
||||
.sidebar-tree .tree-toggle[data-expanded="true"]::before,
|
||||
.sidebar-tree .tree-row[aria-expanded="true"]:not([data-expanded="false"]) > .tree-toggle::before,
|
||||
.sidebar-tree .tree-toggle[aria-expanded="true"]:not([data-expanded="false"])::before {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
/* When children are visibly collapsed, never keep a rotated chevron. */
|
||||
.sidebar-tree .tree-node:has(> .tree-children.tree-children--collapsed) > .tree-row > .tree-toggle::before {
|
||||
transform: none !important;
|
||||
}
|
||||
|
||||
/* Empty children body (lazy hydrate pending or failed): always ▶, never ▼. */
|
||||
.sidebar-tree .tree-node:has(> .tree-children:empty) > .tree-row > .tree-toggle::before {
|
||||
transform: none !important;
|
||||
}
|
||||
|
||||
/* When children are open (not collapsed) AND non-empty, force rotated chevron. */
|
||||
.sidebar-tree .tree-node:has(> .tree-children:not(.tree-children--collapsed):not(:empty)) > .tree-row > .tree-toggle::before {
|
||||
transform: rotate(90deg) !important;
|
||||
}
|
||||
|
||||
.sidebar-tree .tree-row[data-active="true"] .tree-toggle {
|
||||
color: #8F8A84;
|
||||
color: #6F6A62;
|
||||
}
|
||||
|
||||
.sidebar-tree:not([data-tree-shell-mode="filetree"]) .tree-kind-badge[data-kind="page"] {
|
||||
@@ -1058,6 +1089,32 @@
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.wolai-breadcrumb-pages {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 9px;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.wolai-breadcrumb-link,
|
||||
.wolai-breadcrumb-current {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.wolai-breadcrumb-link {
|
||||
color: #6D6A65;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.wolai-breadcrumb-link:hover {
|
||||
color: var(--atelier-text);
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 3px;
|
||||
}
|
||||
|
||||
.wolai-breadcrumb-separator {
|
||||
color: #D0CDC8;
|
||||
}
|
||||
@@ -2069,6 +2126,32 @@
|
||||
text-underline-offset: 6px;
|
||||
}
|
||||
|
||||
.document-edit-mode-toggle {
|
||||
min-width: 76px;
|
||||
height: 32px;
|
||||
margin-top: 8px;
|
||||
border: 1px solid rgba(55, 53, 47, 0.12);
|
||||
border-radius: 6px;
|
||||
background: #FFFFFF;
|
||||
color: #37352F;
|
||||
font-size: 13px;
|
||||
line-height: 1;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.document-edit-mode-toggle:hover {
|
||||
background: rgba(55, 53, 47, 0.06);
|
||||
}
|
||||
|
||||
.document-edit-mode-toggle[data-document-editing="true"] {
|
||||
border-color: rgba(35, 131, 226, 0.24);
|
||||
color: #0F6CBD;
|
||||
}
|
||||
|
||||
.document-title-input[readonly] {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.document-pane-close {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
|
||||
@@ -0,0 +1,734 @@
|
||||
/* Password vault workbench — dedicated /vault CRUD surface */
|
||||
|
||||
.mnote-vault-workbench {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
width: min(1120px, calc(100vw - 48px));
|
||||
min-height: calc(100vh - 120px);
|
||||
margin: 28px auto 40px;
|
||||
color: var(--atelier-text, #1b1c1c);
|
||||
}
|
||||
|
||||
.mnote-vault-header {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 16px 24px;
|
||||
}
|
||||
|
||||
.mnote-vault-header-main {
|
||||
min-width: 0;
|
||||
flex: 1 1 280px;
|
||||
}
|
||||
|
||||
.mnote-vault-header h1 {
|
||||
margin: 0 0 6px;
|
||||
font-size: 28px;
|
||||
font-weight: 650;
|
||||
line-height: 36px;
|
||||
}
|
||||
|
||||
.mnote-vault-header-main > p {
|
||||
margin: 0;
|
||||
color: #6d6a65;
|
||||
font-size: 13px;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.mnote-vault-status {
|
||||
margin: 8px 0 0;
|
||||
min-height: 18px;
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
color: #8b8782;
|
||||
}
|
||||
|
||||
.mnote-vault-status[data-type="error"] {
|
||||
color: #c93a32;
|
||||
}
|
||||
|
||||
.mnote-vault-status[data-type="success"] {
|
||||
color: #23834d;
|
||||
}
|
||||
|
||||
.mnote-vault-status[data-type="info"] {
|
||||
color: #6d6a65;
|
||||
}
|
||||
|
||||
.mnote-vault-header-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex: 0 1 auto;
|
||||
}
|
||||
|
||||
.mnote-vault-header-actions > button,
|
||||
.mnote-vault-detail-actions button,
|
||||
.mnote-vault-secret-actions button,
|
||||
.mnote-vault-secret-edit button,
|
||||
.mnote-vault-tabs button {
|
||||
height: 30px;
|
||||
padding: 0 12px;
|
||||
border: 1px solid rgba(27, 28, 28, 0.12);
|
||||
border-radius: 4px;
|
||||
background: #ffffff;
|
||||
color: #37352f;
|
||||
font: inherit;
|
||||
font-size: 12px;
|
||||
line-height: 28px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.mnote-vault-header-actions > button:hover,
|
||||
.mnote-vault-detail-actions button:hover,
|
||||
.mnote-vault-secret-actions button:hover,
|
||||
.mnote-vault-secret-edit button:hover,
|
||||
.mnote-vault-tabs button:hover {
|
||||
background: #f7f7f6;
|
||||
}
|
||||
|
||||
.mnote-vault-detail-actions button.is-danger {
|
||||
color: #c93a32;
|
||||
border-color: rgba(201, 58, 50, 0.28);
|
||||
}
|
||||
|
||||
.mnote-vault-detail-actions button.is-danger:hover {
|
||||
background: #fdf2f1;
|
||||
}
|
||||
|
||||
.mnote-vault-header-actions input[type="search"] {
|
||||
width: min(240px, 42vw);
|
||||
height: 30px;
|
||||
padding: 0 10px;
|
||||
border: 1px solid rgba(27, 28, 28, 0.12);
|
||||
border-radius: 4px;
|
||||
background: #ffffff;
|
||||
color: inherit;
|
||||
font: inherit;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.mnote-vault-tabs {
|
||||
display: inline-flex;
|
||||
gap: 0;
|
||||
border: 1px solid rgba(27, 28, 28, 0.12);
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.mnote-vault-tabs button {
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.mnote-vault-tabs button.is-active,
|
||||
.mnote-vault-tabs button[aria-selected="true"] {
|
||||
background: #f4f3f3;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.mnote-vault-body {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(220px, 280px) minmax(0, 1fr);
|
||||
gap: 0;
|
||||
min-height: 480px;
|
||||
border: 1px solid rgba(27, 28, 28, 0.1);
|
||||
border-radius: 6px;
|
||||
background: #ffffff;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.mnote-vault-list {
|
||||
min-height: 0;
|
||||
max-height: calc(100vh - 220px);
|
||||
overflow-y: auto;
|
||||
overscroll-behavior: contain;
|
||||
border-right: 1px solid rgba(27, 28, 28, 0.08);
|
||||
background: #fafaf9;
|
||||
}
|
||||
|
||||
.mnote-vault-list-item {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
width: 100%;
|
||||
min-height: 28px;
|
||||
margin: 0;
|
||||
padding: 4px 8px 4px 10px;
|
||||
border: 0;
|
||||
border-bottom: 1px solid rgba(27, 28, 28, 0.04);
|
||||
background: transparent;
|
||||
color: inherit;
|
||||
text-align: left;
|
||||
font: inherit;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.mnote-vault-list-item:hover {
|
||||
background: #f1f0ef;
|
||||
}
|
||||
|
||||
.mnote-vault-list-item.is-active {
|
||||
background: #ebe9e7;
|
||||
box-shadow: inset 2px 0 0 #1b1c1c;
|
||||
}
|
||||
|
||||
.mnote-vault-list-main {
|
||||
display: flex;
|
||||
flex: 1 1 auto;
|
||||
align-items: baseline;
|
||||
gap: 6px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.mnote-vault-list-title {
|
||||
flex: 0 1 auto;
|
||||
max-width: 62%;
|
||||
overflow: hidden;
|
||||
color: #1b1c1c;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
line-height: 18px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.mnote-vault-list-meta {
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
color: #9b9a97;
|
||||
font-size: 11px;
|
||||
font-weight: 400;
|
||||
line-height: 16px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.mnote-vault-list-icons {
|
||||
flex: 0 0 auto;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
margin-left: auto;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.mnote-vault-list-icon {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.mnote-vault-list-icon.is-shared {
|
||||
color: #2e7d32;
|
||||
}
|
||||
|
||||
.mnote-vault-list-icon.is-ai {
|
||||
color: #1565c0;
|
||||
font-size: 9px;
|
||||
}
|
||||
|
||||
.mnote-vault-list-tags {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.mnote-vault-tag {
|
||||
display: inline-block;
|
||||
padding: 1px 6px;
|
||||
border-radius: 999px;
|
||||
background: rgba(27, 28, 28, 0.06);
|
||||
color: #5a5a5a;
|
||||
font-size: 11px;
|
||||
line-height: 16px;
|
||||
}
|
||||
|
||||
.mnote-vault-tag--shared {
|
||||
background: rgba(46, 125, 50, 0.12);
|
||||
color: #2e7d32;
|
||||
}
|
||||
|
||||
.mnote-vault-tag--ai {
|
||||
background: rgba(25, 118, 210, 0.12);
|
||||
color: #1565c0;
|
||||
}
|
||||
|
||||
.mnote-vault-role-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
margin-top: 4px;
|
||||
padding: 2px 8px;
|
||||
border-radius: 999px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
line-height: 18px;
|
||||
background: rgba(25, 118, 210, 0.12);
|
||||
color: #1565c0;
|
||||
}
|
||||
|
||||
.mnote-vault-detail-title {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.mnote-vault-detail-title-text {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.mnote-vault-status-chip {
|
||||
flex: 0 0 auto;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 1px 7px;
|
||||
border-radius: 999px;
|
||||
font-size: 11px;
|
||||
font-weight: 650;
|
||||
line-height: 16px;
|
||||
letter-spacing: 0.01em;
|
||||
}
|
||||
|
||||
.mnote-vault-status-chip.is-shared {
|
||||
background: rgba(46, 125, 50, 0.12);
|
||||
color: #2e7d32;
|
||||
}
|
||||
|
||||
.mnote-vault-status-chip.is-ai {
|
||||
background: rgba(25, 118, 210, 0.12);
|
||||
color: #1565c0;
|
||||
}
|
||||
|
||||
.mnote-vault-hint-mini {
|
||||
margin: 4px 0 0;
|
||||
color: #9b9a97;
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
.mnote-vault-linkish {
|
||||
border: 0;
|
||||
padding: 0;
|
||||
background: none;
|
||||
color: #1565c0;
|
||||
font: inherit;
|
||||
font-size: inherit;
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.mnote-vault-list-item.is-shared-ai {
|
||||
box-shadow: inset 3px 0 0 #2e7d32;
|
||||
}
|
||||
|
||||
.mnote-vault-list-item.is-ai-copy {
|
||||
box-shadow: inset 3px 0 0 #1565c0;
|
||||
}
|
||||
|
||||
.mnote-vault-workbench[data-vault-role="ai"] .mnote-vault-header h1 {
|
||||
color: #1565c0;
|
||||
}
|
||||
|
||||
.mnote-vault-detail {
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
max-height: calc(100vh - 220px);
|
||||
overflow-y: auto;
|
||||
overscroll-behavior: contain;
|
||||
padding: 18px 22px 28px;
|
||||
}
|
||||
|
||||
.mnote-vault-empty {
|
||||
padding: 28px 12px;
|
||||
color: #8b8782;
|
||||
font-size: 13px;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.mnote-vault-detail-header {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.mnote-vault-detail-header h2 {
|
||||
margin: 0;
|
||||
min-width: 0;
|
||||
font-size: 20px;
|
||||
font-weight: 650;
|
||||
line-height: 28px;
|
||||
}
|
||||
|
||||
.mnote-vault-detail-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.mnote-vault-fields {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.mnote-vault-field-row {
|
||||
display: grid;
|
||||
grid-template-columns: 96px minmax(0, 1fr) auto;
|
||||
align-items: start;
|
||||
gap: 8px 12px;
|
||||
padding: 8px 0;
|
||||
border-top: 1px solid rgba(27, 28, 28, 0.06);
|
||||
}
|
||||
|
||||
.mnote-vault-field-row > label {
|
||||
padding-top: 4px;
|
||||
color: #6d6a65;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
.mnote-vault-field-row > div,
|
||||
.mnote-vault-field-row > pre {
|
||||
min-width: 0;
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
line-height: 20px;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.mnote-vault-field-row input[type="text"],
|
||||
.mnote-vault-field-row input[type="password"],
|
||||
.mnote-vault-field-row textarea {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
min-height: 30px;
|
||||
padding: 6px 10px;
|
||||
border: 1px solid rgba(27, 28, 28, 0.12);
|
||||
border-radius: 4px;
|
||||
background: #ffffff;
|
||||
color: inherit;
|
||||
font: inherit;
|
||||
font-size: 13px;
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
.mnote-vault-field-row textarea {
|
||||
min-height: 120px;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
.mnote-vault-secret-value {
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
|
||||
.mnote-vault-secret-actions,
|
||||
.mnote-vault-secret-edit {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.mnote-vault-secret-edit {
|
||||
grid-column: 2 / -1;
|
||||
}
|
||||
|
||||
.mnote-vault-secret-edit input {
|
||||
flex: 1 1 180px;
|
||||
}
|
||||
|
||||
.mnote-vault-muted {
|
||||
color: #9b9a97;
|
||||
}
|
||||
|
||||
.mnote-vault-notes {
|
||||
grid-template-columns: 96px minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.mnote-vault-notes pre {
|
||||
white-space: pre-wrap;
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
color: #37352f;
|
||||
}
|
||||
|
||||
.mnote-vault-form .mnote-vault-field-row {
|
||||
grid-template-columns: 120px minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.mnote-vault-folder-controls {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.mnote-vault-folder-controls select {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
min-height: 30px;
|
||||
padding: 6px 10px;
|
||||
border: 1px solid rgba(27, 28, 28, 0.12);
|
||||
border-radius: 4px;
|
||||
background: #ffffff;
|
||||
color: inherit;
|
||||
font: inherit;
|
||||
font-size: 13px;
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
.mnote-vault-folder-controls input[type="text"] {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
@media (max-width: 820px) {
|
||||
.mnote-vault-workbench {
|
||||
width: calc(100vw - 24px);
|
||||
margin: 18px auto 28px;
|
||||
}
|
||||
|
||||
.mnote-vault-body {
|
||||
grid-template-columns: 1fr;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.mnote-vault-list {
|
||||
max-height: 220px;
|
||||
border-right: 0;
|
||||
border-bottom: 1px solid rgba(27, 28, 28, 0.08);
|
||||
}
|
||||
|
||||
.mnote-vault-detail {
|
||||
max-height: none;
|
||||
}
|
||||
|
||||
.mnote-vault-field-row {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.mnote-vault-form .mnote-vault-field-row {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.mnote-vault-secret-edit {
|
||||
grid-column: auto;
|
||||
}
|
||||
}
|
||||
|
||||
/* Folder tree list */
|
||||
.mnote-vault-folder {
|
||||
border-bottom: 1px solid rgba(27, 28, 28, 0.05);
|
||||
}
|
||||
|
||||
.mnote-vault-site-group {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
.mnote-vault-site-toggle .mnote-vault-folder-name {
|
||||
font-weight: 500;
|
||||
color: #5c5a56;
|
||||
}
|
||||
|
||||
.mnote-vault-secret-input-plain {
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
||||
letter-spacing: 0.01em;
|
||||
}
|
||||
|
||||
.mnote-vault-folder-label {
|
||||
padding: 4px 10px 2px;
|
||||
color: #8b8782;
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.02em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.mnote-vault-folder-toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 3px 8px 3px calc(8px + var(--vault-depth, 0) * 10px);
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: #37352f;
|
||||
font: inherit;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
line-height: 16px;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.mnote-vault-folder-toggle:hover {
|
||||
background: #f1f0ef;
|
||||
}
|
||||
|
||||
.mnote-vault-folder-chevron {
|
||||
display: inline-block;
|
||||
width: 12px;
|
||||
color: #8b8782;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.mnote-vault-folder-name {
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.mnote-vault-folder-count {
|
||||
flex: 0 0 auto;
|
||||
color: #8b8782;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.mnote-vault-list-item-wrap {
|
||||
padding-left: calc(var(--vault-depth, 0) * 10px);
|
||||
}
|
||||
|
||||
.mnote-vault-list-item-wrap .mnote-vault-list-item {
|
||||
border-bottom-color: rgba(27, 28, 28, 0.04);
|
||||
}
|
||||
|
||||
.mnote-vault-hint {
|
||||
margin: 8px 0 0;
|
||||
color: #8b8782;
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
.mnote-vault-hint code {
|
||||
padding: 0 4px;
|
||||
border-radius: 3px;
|
||||
background: #f1f0ef;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.mnote-vault-sibling-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.mnote-vault-sibling {
|
||||
height: 26px;
|
||||
padding: 0 10px;
|
||||
border: 1px solid rgba(27, 28, 28, 0.12);
|
||||
border-radius: 4px;
|
||||
background: #ffffff;
|
||||
color: #37352f;
|
||||
font: inherit;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.mnote-vault-sibling:hover {
|
||||
background: #f7f7f6;
|
||||
}
|
||||
|
||||
/* Cipher book panel */
|
||||
.mnote-vault-cipher-add {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
margin: 12px 0 16px;
|
||||
}
|
||||
|
||||
.mnote-vault-cipher-add input {
|
||||
flex: 1 1 120px;
|
||||
min-width: 100px;
|
||||
height: 30px;
|
||||
padding: 0 10px;
|
||||
border: 1px solid rgba(27, 28, 28, 0.12);
|
||||
border-radius: 4px;
|
||||
font: inherit;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.mnote-vault-cipher-add button {
|
||||
height: 30px;
|
||||
padding: 0 12px;
|
||||
border: 1px solid rgba(27, 28, 28, 0.12);
|
||||
border-radius: 4px;
|
||||
background: #ffffff;
|
||||
font: inherit;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.mnote-vault-cipher-row {
|
||||
display: grid;
|
||||
grid-template-columns: 88px minmax(0, 1fr) auto;
|
||||
gap: 8px 12px;
|
||||
align-items: center;
|
||||
padding: 8px 0;
|
||||
border-bottom: 1px solid rgba(27, 28, 28, 0.06);
|
||||
}
|
||||
|
||||
.mnote-vault-cipher-key {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.mnote-vault-cipher-value {
|
||||
overflow: hidden;
|
||||
color: #37352f;
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
||||
font-size: 12px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.mnote-vault-cipher-actions {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.mnote-vault-cipher-actions button {
|
||||
height: 26px;
|
||||
padding: 0 8px;
|
||||
border: 1px solid rgba(27, 28, 28, 0.12);
|
||||
border-radius: 4px;
|
||||
background: #ffffff;
|
||||
font: inherit;
|
||||
font-size: 11px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.mnote-vault-cipher-actions button.is-danger {
|
||||
color: #c93a32;
|
||||
border-color: rgba(201, 58, 50, 0.28);
|
||||
}
|
||||
|
||||
@media (max-width: 820px) {
|
||||
.mnote-vault-cipher-row {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user