Compare commits

..

No commits in common. "main" and "0.2.2" have entirely different histories.
main ... 0.2.2

17 changed files with 341 additions and 444 deletions

View file

@ -1,18 +1,5 @@
# Changelog
## 0.2.4
- Move 3D node inspection details into the side panel.
- Improve long label, pin, neighbor, and button wrapping in the map panels.
- Type Obsidian saved plugin data as `unknown` before settings migration to avoid unsafe assignment warnings.
## 0.2.3
- Add a 2D Complete map action that shows the vault root with expanded node, link, depth, and outside-link detail budgets.
- Allow farther 2D zoom-out for large complete maps.
- Improve 2D panel wrapping and alignment so long labels remain readable.
- Soften the 2D outside-link road color and legend swatch.
## 0.2.2
- Address Obsidian community plugin review findings around settings APIs, command names, deprecated slider tooltips, direct static styles, and config folder handling.

View file

@ -86,7 +86,7 @@ Global plugin settings include:
- 2D unresolved link handling.
- Ignored folders.
The 2D view panel also includes per-view controls for atlas/focus mode, vault root, complete map display, theme, depth, visible nodes, note-link budget, outside-link detail, exact outside-note limit, ring guides, and legend visibility.
The 2D view panel also includes per-view controls for atlas/focus mode, vault root, theme, depth, visible nodes, note-link budget, outside-link detail, exact outside-note limit, ring guides, and legend visibility.
The 3D view panel includes controls for search, recentering, reveal animation, visual style presets, theme presets, color themes, imported Obsidian graph colors, node sizing, bloom, physics, cruise motion, unresolved links, orphan nodes, quality tier, and default reset.

66
main.js

File diff suppressed because one or more lines are too long

View file

@ -1,7 +1,7 @@
{
"id": "mini-world-map",
"name": "Mini World Map",
"version": "0.2.4",
"version": "0.2.2",
"minAppVersion": "1.13.0",
"description": "Visualize your vault as a hierarchy-first world map with internal links layered on top.",
"author": "Miro0o and Codex",

4
package-lock.json generated
View file

@ -1,12 +1,12 @@
{
"name": "mini-world-map",
"version": "0.2.4",
"version": "0.2.2",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "mini-world-map",
"version": "0.2.4",
"version": "0.2.2",
"license": "MIT",
"dependencies": {
"3d-force-graph": "1.80.0",

View file

@ -1,6 +1,6 @@
{
"name": "mini-world-map",
"version": "0.2.4",
"version": "0.2.2",
"description": "Hierarchy-first 2D radial rings and 3D map views for your Obsidian vault",
"main": "main.js",
"type": "module",

View file

@ -59,7 +59,6 @@ const STRINGS: Record<string, Record<Language, string>> = {
'view.atlas': { en: 'Atlas', zh: '图谱' },
'view.focus': { en: 'Focus', zh: '聚焦' },
'view.vaultRoot': { en: 'Vault root', zh: '库根目录' },
'view.completeMap': { en: 'Full map', zh: '完整地图' },
'view.mode': { en: 'Map mode', zh: '地图模式' },
'view.theme': { en: 'Theme', zh: '主题' },
'theme.auto': { en: 'System', zh: '跟随系统' },
@ -71,8 +70,6 @@ const STRINGS: Record<string, Record<Language, string>> = {
'inspect.type': { en: 'Type', zh: '类型' },
'inspect.depth': { en: 'Depth', zh: '深度' },
'inspect.notes': { en: 'Notes', zh: '笔记' },
'inspect.folder': { en: 'Folder', zh: '文件夹' },
'inspect.modified': { en: 'Modified', zh: '修改时间' },
'inspect.out': { en: 'Out', zh: '出链' },
'inspect.in': { en: 'In', zh: '入链' },
'inspect.linkOverlay': { en: 'Note link', zh: '笔记链接' },
@ -230,8 +227,6 @@ const STRINGS: Record<string, Record<Language, string>> = {
'3d.searchPlaceholder': { en: 'Search notes, press Enter to fly…', zh: '搜索笔记,回车飞过去…' },
'3d.searchUnresolved': { en: 'Unresolved', zh: '未解析' },
'3d.searchLinks': { en: '{count} links', zh: '{count} 链接' },
'3d.inspect.none': { en: 'Select a node to inspect it.', zh: '选择一个节点查看详情。' },
'3d.inspect.note': { en: 'Note', zh: '笔记' },
'3d.card.unresolved': { en: 'Unresolved link (note does not exist)', zh: '未解析链接(笔记尚不存在)' },
'3d.card.root': { en: 'Vault root', zh: '根目录' },
'3d.card.stats': { en: '↩ {in} backlinks · → {out} outgoing', zh: '↩ {in} 反链 · → {out} 出链' },

View file

@ -27,7 +27,7 @@ export default class MiniWorldMapPlugin extends Plugin {
async onload(): Promise<void> {
console.info('[Mini World Map] loading');
const saved: unknown = await this.loadData();
const saved = await this.loadData();
this.settings = applyVaultConfigDirDefault(mergeSettings(saved), saved, this.app.vault.configDir);
this.registerView(VIEW_TYPE_MINI_WORLD_MAP, (leaf) => new MiniWorldMapView(leaf, this));

View file

@ -8,18 +8,7 @@ import type { VisualTokens } from '../render/presets';
import { t, viewModeLabel } from '../i18n';
import { Slider } from './Slider';
type PanelPage = 'inspect' | 'view' | 'appearance' | 'physics' | 'motion' | 'advanced';
export interface PanelInspectNode {
title: string;
path: string;
folder: string;
type: string;
inDegree: number;
outDegree: number;
modified: string | null;
canOpen: boolean;
}
type PanelPage = 'view' | 'appearance' | 'physics' | 'motion' | 'advanced';
export interface ControlPanelCallbacks {
onViewMode?: (mode: ViewMode) => void;
@ -40,8 +29,6 @@ export interface ControlPanelCallbacks {
onSizeBy: () => void;
onQuality: () => void;
onSearch: () => void;
onOpenSelected: () => void;
onFocusSelected: () => void;
onReset: () => void;
runScenario: (s: 'S1' | 'S2' | 'S3') => void;
}
@ -59,7 +46,6 @@ export class ControlPanel {
private sizeBySelect: HTMLSelectElement | null = null;
private qualityBtn: HTMLButtonElement | null = null;
private styleChips: HTMLButtonElement[] = [];
private inspectNode: PanelInspectNode | null = null;
constructor(
parent: HTMLElement,
@ -101,7 +87,6 @@ export class ControlPanel {
const settings = this.body.createDiv({ cls: 'mwm-panel-settings mwm-3d-settings' });
const tabs = settings.createDiv({ cls: 'mwm-panel-tabs' });
for (const [id, label] of [
['inspect', this.tt('tab.inspect')],
['view', this.tt('tab.view')],
['appearance', this.tt('tab.appearance')],
['physics', this.tt('tab.physics')],
@ -116,43 +101,13 @@ export class ControlPanel {
}
const pageEl = settings.createDiv({ cls: 'mwm-panel-page' });
if (this.page === 'inspect') this.renderInspectPage(pageEl);
else if (this.page === 'appearance') this.renderAppearancePage(pageEl);
if (this.page === 'appearance') this.renderAppearancePage(pageEl);
else if (this.page === 'physics') this.renderPhysicsPage(pageEl);
else if (this.page === 'motion') this.renderMotionPage(pageEl);
else if (this.page === 'advanced') this.renderAdvancedPage(pageEl);
else this.renderViewPage(pageEl);
}
private renderInspectPage(parent: HTMLElement): void {
const node = this.inspectNode;
if (!node) {
parent.createDiv({ cls: 'mwm-side-muted', text: this.tt('3d.inspect.none') });
return;
}
parent.createDiv({ cls: 'mwm-side-title', text: node.title });
const path = parent.createDiv({ cls: 'mwm-side-path mwm-scroll-path', text: node.path || this.tt('3d.card.root') });
path.setAttr('title', node.path || this.tt('3d.card.root'));
const facts = parent.createDiv({ cls: 'mwm-facts' });
for (const [label, value] of [
[this.tt('inspect.type'), node.type],
[this.tt('inspect.folder'), node.folder || this.tt('3d.card.root')],
[this.tt('inspect.in'), String(node.inDegree)],
[this.tt('inspect.out'), String(node.outDegree)],
...(node.modified ? ([[this.tt('inspect.modified'), node.modified]] as const) : []),
] as const) {
facts.createSpan({ text: label });
facts.createSpan({ text: value });
}
const actions = parent.createDiv({ cls: 'galaxy-panel-row' });
if (node.canOpen) {
const openBtn = actions.createEl('button', { text: this.tt('context.openNote') });
openBtn.addEventListener('click', this.cb.onOpenSelected);
}
const focusBtn = actions.createEl('button', { text: this.tt('common.focus') });
focusBtn.addEventListener('click', this.cb.onFocusSelected);
}
private renderViewPage(parent: HTMLElement): void {
const row = parent.createDiv({ cls: 'galaxy-panel-row' });
const searchBtn = row.createEl('button', { text: this.tt('common.search') });
@ -389,12 +344,6 @@ export class ControlPanel {
this.render();
}
setInspectNode(node: PanelInspectNode | null, activate = false): void {
this.inspectNode = node;
if (activate) this.page = 'inspect';
if (activate || this.page === 'inspect') this.render();
}
setPanelTheme(cls: VisualTokens['panelClass']): void {
this.root.removeClass('gx-theme-space');
this.root.removeClass('gx-theme-night');

View file

@ -1,8 +1,18 @@
import type { GraphData } from '../types';
import type { App } from 'obsidian';
import { TFile, getAllTags } from 'obsidian';
import type { Language } from '../settings';
import type { GraphData, GraphNode } from '../types';
import type { AggregateRenderer } from '../render/AggregateRenderer';
import { t } from '../i18n';
export interface OverlayCallbacks {
openNote: (id: string) => void;
focusNode: (index: number) => void;
}
/**
* DOM labels for hub, hover, and selected-neighbor names.
* DOM NASA
* 14 + hover 1 + 20 + 1 36
*/
export class OverlayManager {
private root: HTMLElement;
@ -10,23 +20,42 @@ export class OverlayManager {
private neighborEls: { index: number; el: HTMLElement }[] = [];
private hoverEl: HTMLElement;
private hoverIndex = -1;
private card: HTMLElement;
private cardIndex = -1;
private data: GraphData = { nodes: [], links: [] };
private graphRadius = 200;
private snippetToken = 0;
private hubBudget = 14;
private neighborBudget = 20;
private mobileCard = false;
constructor(
parent: HTMLElement,
private app: App,
private renderer: AggregateRenderer,
private cb: OverlayCallbacks,
private language: Language = 'en',
) {
this.root = parent.createDiv({ cls: 'gx-overlay' });
this.hoverEl = this.root.createDiv({ cls: 'gx-label gx-label-hover' });
this.hoverEl.hide();
this.card = this.root.createDiv({ cls: 'gx-card' });
this.card.hide();
}
setBudgets(hub: number, neighbor: number): void {
setLanguage(language: Language): void {
this.language = language;
if (this.cardIndex >= 0) {
const node = this.data.nodes[this.cardIndex];
if (node) this.buildCard(node, this.cardIndex);
}
}
/** 质量档位预算;卡片切底部抽屉模式(移动端) */
setBudgets(hub: number, neighbor: number, mobileCard: boolean): void {
this.hubBudget = hub;
this.neighborBudget = neighbor;
this.mobileCard = mobileCard;
this.setData(this.data, this.graphRadius);
}
@ -59,11 +88,31 @@ export class OverlayManager {
this.hoverEl.show();
}
/** Selected node: neighbor labels only; details render in the left panel. */
/**
* M4.1 .mobile-navbar
* navbar //
* navbar 0
*/
private refreshBottomInset(): void {
let inset = 0;
const navbar = activeDocument.querySelector('.mobile-navbar');
if (navbar) {
const nb = navbar.getBoundingClientRect();
const ce = this.root.getBoundingClientRect();
inset = Math.max(0, Math.round(ce.bottom - nb.top));
}
this.root.setCssProps({ '--gx-bottom-inset': `${inset}px` });
}
/** 选中:邻居标签 + 卡片index<0 清空 */
setSelection(index: number, neighbors: Set<number>): void {
for (const e of this.neighborEls) e.el.remove();
this.neighborEls = [];
if (index < 0) return;
this.cardIndex = index;
if (index < 0) {
this.card.hide();
return;
}
const byDegree = [...neighbors]
.filter((i) => i !== index)
.sort((a, b) => (this.data.nodes[b]?.degree ?? 0) - (this.data.nodes[a]?.degree ?? 0))
@ -72,6 +121,63 @@ export class OverlayManager {
index: i,
el: this.root.createDiv({ cls: 'gx-label gx-label-neighbor', text: this.data.nodes[i]?.name ?? '' }),
}));
const node = this.data.nodes[index];
if (node) {
if (this.mobileCard) {
this.refreshBottomInset();
// 移除桌面定位残留的内联 transform → CSS 底部抽屉定位靠选择器特异性接管(免 !important
this.card.style.removeProperty('transform');
}
this.buildCard(node, index);
}
}
private buildCard(node: GraphNode, index: number): void {
this.card.empty();
this.card.show();
this.card.createDiv({ cls: 'gx-card-title', text: node.name });
const meta = this.card.createDiv({ cls: 'gx-card-meta' });
const dot = meta.createSpan({ cls: 'gx-card-dot' });
dot.style.background = this.renderer.nodeColorHex(index);
meta.createSpan({
text: node.unresolved ? this.tt('3d.card.unresolved') : node.id.includes('/') ? node.id.slice(0, node.id.lastIndexOf('/')) : this.tt('3d.card.root'),
});
const file = node.unresolved ? null : this.app.vault.getAbstractFileByPath(node.id);
const tfile = file instanceof TFile ? file : null;
if (tfile) {
const cache = this.app.metadataCache.getFileCache(tfile);
const tags = cache ? (getAllTags(cache) ?? []) : [];
if (tags.length > 0) {
const tagRow = this.card.createDiv({ cls: 'gx-card-tags' });
for (const t of tags.slice(0, 5)) tagRow.createSpan({ cls: 'gx-card-tag', text: t });
}
}
const stats = this.card.createDiv({ cls: 'gx-card-stats' });
stats.setText(
this.tt('3d.card.stats', { in: node.inDegree, out: node.outDegree }) +
(tfile ? this.tt('3d.card.modified', { date: new Date(tfile.stat.mtime).toLocaleDateString(this.language === 'zh' ? 'zh-CN' : 'en-US') }) : ''),
);
if (tfile) {
const snippetEl = this.card.createDiv({ cls: 'gx-card-snippet', text: '…' });
const token = ++this.snippetToken;
void this.app.vault.cachedRead(tfile).then((text) => {
if (token !== this.snippetToken) return; // 已切换选中,丢弃过期结果
snippetEl.setText(stripMarkdown(text).slice(0, 120) || this.tt('3d.card.empty'));
});
}
const actions = this.card.createDiv({ cls: 'gx-card-actions' });
if (!node.unresolved) {
const openBtn = actions.createEl('button', { text: this.tt('context.openNote') });
openBtn.addEventListener('click', () => this.cb.openNote(node.id));
}
const focusBtn = actions.createEl('button', { text: this.tt('common.focus') });
focusBtn.addEventListener('click', () => this.cb.focusNode(index));
}
/** 每帧投影所有被追踪节点translate3d 定位GPU 合成,无重排) */
@ -98,6 +204,15 @@ export class OverlayManager {
const p = this.renderer.projectNode(this.hoverIndex, w, h);
if (!p.behind) this.hoverEl.style.transform = `translate3d(${p.x.toFixed(1)}px, ${(p.y - 18).toFixed(1)}px, 0)`;
}
if (this.cardIndex >= 0 && !this.mobileCard) {
const p = this.renderer.projectNode(this.cardIndex, w, h);
if (!p.behind) {
const flip = p.x + 296 > w;
const x = flip ? p.x - 296 : p.x + 16;
const y = Math.min(Math.max(p.y - 40, 12), Math.max(h - this.card.clientHeight - 12, 12));
this.card.style.transform = `translate3d(${x.toFixed(1)}px, ${y.toFixed(1)}px, 0)`;
}
}
}
dispose(): void {
@ -105,4 +220,18 @@ export class OverlayManager {
this.hubEls = [];
this.neighborEls = [];
}
private tt(key: string, vars: Record<string, string | number> = {}): string {
return t(this.language, key, vars);
}
}
function stripMarkdown(text: string): string {
return text
.replace(/^---\n[\s\S]*?\n---\n?/, '') // frontmatter
.replace(/!?\[\[([^\]|]+)(\|[^\]]+)?\]\]/g, '$1')
.replace(/\[([^\]]*)\]\([^)]*\)/g, '$1')
.replace(/[#*`>~_]|---/g, '')
.replace(/\s+/g, ' ')
.trim();
}

View file

@ -18,8 +18,7 @@ import type { RadialLayout, RadialPoint, RadialRoute } from '../layout/radial/la
import { ROOT_ID, type VisibleWorldGraph, type WorldEdge, type WorldNode } from '../world/types';
import { NODE_FRAGMENT_SHADER, NODE_VERTEX_SHADER } from './shaders';
// Hard floor keeps wheel math and hit-tests finite while still allowing huge complete maps to fit.
export const MIN_RADIAL_ZOOM = 0.00001;
export const MIN_RADIAL_ZOOM = 0.0001;
export const MAX_RADIAL_ZOOM = 6;
const NODE_BASE_POINT = 4.8;
@ -71,7 +70,7 @@ const PALETTES: Record<RadialResolvedScheme, RadialPalette> = {
tree: '#93a3b8',
link: '#a2acba',
external: '#7b6fd6',
externalLink: '#b8752e',
externalLink: '#c98135',
unresolved: '#dc4a4a',
focus: '#3f7fe8',
folder: '#7f92a8',
@ -81,7 +80,7 @@ const PALETTES: Record<RadialResolvedScheme, RadialPalette> = {
ringOpacity: 0.22,
treeOpacity: 0.26,
linkOpacity: 0.1,
externalLinkOpacity: 0.14,
externalLinkOpacity: 0.16,
highlightOpacity: 0.92,
nodeScale: 0.32,
maxLabels: 170,
@ -92,7 +91,7 @@ const PALETTES: Record<RadialResolvedScheme, RadialPalette> = {
tree: '#8a8f9c',
link: '#8b8f99',
external: '#a78bfa',
externalLink: '#d97706',
externalLink: '#f59e0b',
unresolved: '#fb7185',
focus: '#8b7cf6',
folder: '#d5d8de',
@ -102,7 +101,7 @@ const PALETTES: Record<RadialResolvedScheme, RadialPalette> = {
ringOpacity: 0.22,
treeOpacity: 0.28,
linkOpacity: 0.12,
externalLinkOpacity: 0.14,
externalLinkOpacity: 0.16,
highlightOpacity: 0.98,
nodeScale: 0.32,
maxLabels: 300,

View file

@ -1,5 +1,5 @@
import type { App } from 'obsidian';
import { Menu, Notice, Platform, TFile, debounce, setIcon } from 'obsidian';
import { Menu, Notice, Platform, debounce, setIcon } from 'obsidian';
import { Spherical, Vector3 } from 'three';
import type { BenchResult } from '../types';
import type { GalaxySettings, Language, ViewMode } from '../settings';
@ -18,7 +18,7 @@ import { DAYLIGHT, DEEP_SPACE, NIGHT } from '../render/presets';
import type { VisualTokens } from '../render/presets';
import { resolveObsidianBackground } from '../render/obsidianTheme';
import { CameraDirector } from '../interactions/CameraDirector';
import { ControlPanel, type PanelInspectNode } from '../overlay/ControlPanel';
import { ControlPanel } from '../overlay/ControlPanel';
import { OverlayManager } from '../overlay/OverlayManager';
import { NodeSearchModal } from './SearchModal';
import { collectFrames, observeLongTasks, writeBenchResult, sleep } from '../bench/bench';
@ -104,7 +104,10 @@ export class GraphController {
onResetView: () => this.recenter(),
});
this.overlay = new OverlayManager(this.contentEl, renderer);
this.overlay = new OverlayManager(this.contentEl, this.app, renderer, {
openNote: (id) => void this.app.workspace.openLinkText(id, '', true),
focusNode: (i) => this.selectNode(i, true),
}, this.language);
this.overlay.setData(this.store.data, this.graphRadius);
this.applySettings();
@ -272,7 +275,7 @@ export class GraphController {
const prev = this.tier.id;
this.tier = this.pickTier();
this.renderer?.applyTier(this.tier, this.settings.bloom.strength);
this.overlay?.setBudgets(this.tier.hubLabels, this.tier.neighborLabels);
this.overlay?.setBudgets(this.tier.hubLabels, this.tier.neighborLabels, this.tier.id === 'mobile');
this.contentEl.toggleClass('gx-mobile', this.tier.id === 'mobile');
const total = this.app.vault.getMarkdownFiles().length;
this.store.setCaps(this.tier.nodeCap, this.tier.linkCap); // 变化时触发重建
@ -451,7 +454,6 @@ export class GraphController {
renderer.setFocus(index, neighbors);
renderer.setSelectedLinks(linkIdx);
this.overlay?.setSelection(index, neighbors);
this.panel?.setInspectNode(this.panelInspectNode(index), true);
if (fly) {
const pos = renderer.nodePosition(index, new Vector3());
// 邻居质心方向:到达后环绕优先扫过链接密集的一侧
@ -473,31 +475,6 @@ export class GraphController {
this.renderer?.setFocus(-1, null);
this.renderer?.setSelectedLinks([]);
this.overlay?.setSelection(-1, new Set());
this.panel?.setInspectNode(null);
}
private panelInspectNode(index: number): PanelInspectNode | null {
const node = this.store.data.nodes[index];
if (!node) return null;
const file = node.unresolved ? null : this.app.vault.getAbstractFileByPath(node.id);
const tfile = file instanceof TFile ? file : null;
const folder = node.unresolved ? this.tt('3d.searchUnresolved') : node.id.includes('/') ? node.id.slice(0, node.id.lastIndexOf('/')) : this.tt('3d.card.root');
return {
title: node.name,
path: node.unresolved ? node.id : node.id || this.tt('3d.card.root'),
folder,
type: node.unresolved ? this.tt('3d.searchUnresolved') : this.tt('3d.inspect.note'),
inDegree: node.inDegree,
outDegree: node.outDegree,
modified: tfile ? new Date(tfile.stat.mtime).toLocaleDateString(this.language === 'zh' ? 'zh-CN' : 'en-US') : null,
canOpen: !node.unresolved,
};
}
private openSelectedNode(): void {
const node = this.store.data.nodes[this.selected];
if (!node || node.unresolved) return;
void this.app.workspace.openLinkText(node.id, '', true);
}
private flyToSelected(): void {
@ -628,8 +605,6 @@ export class GraphController {
this.saveSoon();
},
onSearch: () => this.openSearch(),
onOpenSelected: () => this.openSelectedNode(),
onFocusSelected: () => this.flyToSelected(),
onReset: () => {
Object.assign(this.settings.bloom, DEFAULT_GALAXY_SETTINGS.bloom);
Object.assign(this.settings.physics, DEFAULT_GALAXY_SETTINGS.physics);
@ -669,6 +644,7 @@ export class GraphController {
item.onClick(() => {
if (value === this.language) return;
this.language = value;
this.overlay?.setLanguage(value);
this.panel?.setLanguage(value);
this.onLanguage?.(value);
});

View file

@ -165,22 +165,18 @@ export class Radial2DController extends Component {
private async rebuildNow(reason: string, token: number): Promise<void> {
const radial = this.radial();
const shouldReveal = ['start', 'manual', 'root', 'focus', 'atlas', 'complete'].includes(reason);
const shouldReveal = ['start', 'manual', 'root', 'focus', 'atlas'].includes(reason);
const loadingText = this.t('loading.radial');
if (shouldReveal) {
this.renderer?.showLoadingMask(loadingText);
await animationFrames(2);
if (this.disposed || token !== this.rebuildToken) return;
}
const indexSettings = this.state.showCompleteRoot ? { ...radial, includeUnresolvedLinks: true } : radial;
this.index.rebuild(indexSettings);
this.index.rebuild(radial);
this.state.hoverHighlightMode = radial.hoverHighlightMode;
this.state.labelVisibility = radial.labelVisibility;
if (this.state.showCompleteRoot) this.applyCompleteMapState();
else {
this.state.hiddenLegendItems = radial.hiddenLegendItems.slice();
this.state.showLinkOverlay = radial.showLinkOverlay || !this.state.hiddenLegendItems.includes('link');
}
this.state.hiddenLegendItems = radial.hiddenLegendItems.slice();
this.state.showLinkOverlay = radial.showLinkOverlay || !this.state.hiddenLegendItems.includes('link');
this.state.pinNeedsHoverLinks = this.pinnedPathsNeedHoverLinks();
this.state.selectedNodeId = this.selectedNodeId;
this.state.selectedLink = this.selectedLink;
@ -492,7 +488,7 @@ export class Radial2DController extends Component {
['controls', this.t('tab.controls')],
['defaults', this.t('tab.defaults')],
] as const) {
const button = tabs.createEl('button', { cls: this.activePanelPage === id ? 'is-active' : '', text: label, attr: { title: label } });
const button = tabs.createEl('button', { cls: this.activePanelPage === id ? 'is-active' : '', text: label });
button.addEventListener('click', () => {
this.activePanelPage = id;
this.renderPanel();
@ -508,7 +504,7 @@ export class Radial2DController extends Component {
private modeButton(parent: HTMLElement, label: string, mode: ViewMode): void {
const active = this.settings.viewMode === mode;
const button = parent.createEl('button', { cls: active ? 'is-active' : '', text: label, attr: { title: label } });
const button = parent.createEl('button', { cls: active ? 'is-active' : '', text: label });
button.addEventListener('click', () => this.onViewMode(mode));
}
@ -572,16 +568,12 @@ export class Radial2DController extends Component {
[this.t('inspect.outgoing', { count: outgoing.length }), outgoing, 'target'],
[this.t('inspect.backlinks', { count: incoming.length }), incoming, 'source'],
] as const) {
const section = parent.createDiv({ cls: 'mwm-neighbor-section' });
section.createDiv({ cls: 'mwm-side-heading', text: title });
if (edges.length === 0) section.createDiv({ cls: 'mwm-side-muted', text: this.t('common.none') });
const list = section.createDiv({ cls: 'mwm-neighbor-list' });
parent.createDiv({ cls: 'mwm-side-heading', text: title });
if (edges.length === 0) parent.createDiv({ cls: 'mwm-side-muted', text: this.t('common.none') });
for (const edge of edges) {
const neighbor = this.index.nodes.get(edge[side]);
if (!neighbor) continue;
const label = `${neighbor.title} (${edge.weight})`;
const button = list.createEl('button', { cls: 'mwm-link-row', attr: { title: label } });
button.createSpan({ cls: 'mwm-link-row-label', text: label });
const button = parent.createEl('button', { cls: 'mwm-link-row', text: `${neighbor.title} (${edge.weight})` });
button.addEventListener('click', () => this.inspectNode(neighbor.id));
}
}
@ -639,18 +631,14 @@ export class Radial2DController extends Component {
else this.selectedPinIds.delete(pin.id);
this.renderPanel();
});
const main = row.createEl('button', { cls: 'mwm-pin-main', attr: { type: 'button' } });
const main = row.createEl('button', { cls: 'mwm-pin-main', attr: { type: 'button', title: pin.path || pin.title } });
main.addEventListener('click', () => this.locatePin(pin));
const displayTitle = this.pinDisplayTitle(pin);
const displayPath = pin.path || '/';
main.setAttr('title', `${displayTitle}\n${this.pinKindLabel(pin)} - ${displayPath}`);
main.createDiv({ cls: 'mwm-pin-title', text: displayTitle });
main.createDiv({ cls: 'mwm-pin-meta', text: displayPath });
const actions = row.createDiv({ cls: 'mwm-pin-actions' });
this.button(actions, pin.active ? this.t('pins.hideHighlight') : this.t('pins.showHighlight'), () => this.togglePinHighlight(pin.id));
this.button(actions, this.t('common.inspect'), () => this.inspectPin(pin));
if (pin.groupId) this.button(actions, this.t('common.ungroup'), () => this.ungroupPin(pin.id));
this.button(actions, 'X', () => this.removePin(pin.id));
main.createDiv({ cls: 'mwm-pin-title', text: pin.title });
main.createDiv({ cls: 'mwm-pin-meta', text: `${this.pinKindLabel(pin)} - ${pin.path || '/'}` });
this.button(row, pin.active ? this.t('pins.hideHighlight') : this.t('pins.showHighlight'), () => this.togglePinHighlight(pin.id));
this.button(row, this.t('common.inspect'), () => this.inspectPin(pin));
if (pin.groupId) this.button(row, this.t('common.ungroup'), () => this.ungroupPin(pin.id));
this.button(row, 'X', () => this.removePin(pin.id));
}
private renderViewPage(parent: HTMLElement): void {
@ -659,10 +647,9 @@ export class Radial2DController extends Component {
this.button(row, this.t('common.recenter'), () => this.centerCurrentView());
this.button(row, this.t('common.rebuild'), () => this.rebuild('manual'));
const row2 = parent.createDiv({ cls: 'galaxy-panel-row' });
this.button(row2, this.t('view.atlas'), () => this.resetToAtlas(), this.state.mode === 'atlas' && !this.state.showCompleteRoot);
this.button(row2, this.t('view.atlas'), () => this.resetToAtlas(), this.state.mode === 'atlas');
this.button(row2, this.t('view.focus'), () => this.focusActiveNote(), this.state.mode === 'focus');
this.button(row2, this.t('view.vaultRoot'), () => this.resetToRoot());
this.button(row2, this.t('view.completeMap'), () => this.showCompleteMap(), this.state.showCompleteRoot);
this.select(parent, this.t('view.theme'), this.radial().colorScheme, colorSchemeOptions(this.settings.language), (value) => {
this.radial().colorScheme = normalizeColorScheme(value);
this.saveSoon();
@ -720,7 +707,6 @@ export class Radial2DController extends Component {
this.state.mode = 'atlas';
this.state.focusPath = null;
if (node.type === 'folder') {
this.leaveCompleteMap();
this.state.rootPath = visualId;
await this.queueRebuild('root');
return;
@ -730,7 +716,6 @@ export class Radial2DController extends Component {
this.renderPanel();
return;
}
this.leaveCompleteMap();
this.state.rootPath = this.searchAtlasRoot(node, visualId);
await this.queueRebuild('root');
}
@ -747,42 +732,6 @@ export class Radial2DController extends Component {
if (this.renderer?.fitToLayout(rootId)) this.needsFit = false;
}
private showCompleteMap(): void {
this.state.showCompleteRoot = true;
this.applyCompleteMapState();
this.needsFit = true;
this.rebuild('complete');
}
private applyCompleteMapState(): void {
this.state.mode = 'atlas';
this.state.rootPath = ROOT_ID;
this.state.focusPath = null;
this.state.search = '';
this.state.atlasDepth = MAX_ATLAS_DEPTH;
this.state.nodeLimit = MAX_RENDER_NODE_LIMIT;
this.state.linkLimit = MAX_LINK_LIMIT;
this.state.externalLinkAnchorLimit = MAX_EXTERNAL_LINK_ANCHOR_LIMIT;
this.state.showLinkOverlay = true;
this.state.showExternalLinks = true;
this.state.externalDetailMode = 'exact';
this.state.hiddenLegendItems = [];
}
private leaveCompleteMap(): void {
if (!this.state.showCompleteRoot) return;
const radial = this.radial();
this.state.showCompleteRoot = false;
this.state.atlasDepth = radial.atlasDepth;
this.state.nodeLimit = radial.renderNodeLimit;
this.state.linkLimit = radial.linkLimit;
this.state.externalLinkAnchorLimit = radial.externalLinkAnchorLimit;
this.state.showExternalLinks = radial.showExternalLinks;
this.state.externalDetailMode = radial.externalDetailMode;
this.state.hiddenLegendItems = radial.hiddenLegendItems.slice();
this.state.showLinkOverlay = radial.showLinkOverlay || !this.state.hiddenLegendItems.includes('link');
}
private centerNode(nodeId: string): void {
const point = this.renderer?.nodePoint(nodeId);
const view = this.renderer?.getView();
@ -801,24 +750,20 @@ export class Radial2DController extends Component {
private renderControlsPage(parent: HTMLElement): void {
const radial = this.radial();
this.numberInput(parent, this.t('control.depth'), this.state.atlasDepth, 1, MAX_ATLAS_DEPTH, 1, (value) => {
this.leaveCompleteMap();
this.state.atlasDepth = value;
this.rebuild('depth');
});
this.numberInput(parent, this.t('control.nodes'), this.state.nodeLimit, 200, MAX_RENDER_NODE_LIMIT, 100, (value) => {
this.leaveCompleteMap();
this.state.nodeLimit = value;
this.rebuild('nodes');
});
this.numberInput(parent, this.t('control.noteLinks'), this.state.linkLimit, 0, MAX_LINK_LIMIT, 50, (value) => {
this.leaveCompleteMap();
this.state.linkLimit = value;
this.rebuild('links');
});
const hidden = new Set(this.state.hiddenLegendItems);
const hidden = new Set(radial.hiddenLegendItems);
const noteLinksVisible = this.state.showLinkOverlay && !hidden.has('link');
this.toggle(parent, this.t('control.showNoteLinks'), noteLinksVisible, (value) => {
this.leaveCompleteMap();
radial.showLinkOverlay = value;
this.state.showLinkOverlay = value;
this.setLegendHidden('link', !value);
@ -861,7 +806,6 @@ export class Radial2DController extends Component {
this.renderPanel();
});
this.toggle(parent, this.t('control.outsideLinks'), this.state.showExternalLinks, (value) => {
this.leaveCompleteMap();
this.state.showExternalLinks = value;
radial.showExternalLinks = value;
this.saveSoon();
@ -873,7 +817,6 @@ export class Radial2DController extends Component {
this.state.externalDetailMode,
outsideDetailOptions(this.settings.language),
(value) => {
this.leaveCompleteMap();
this.state.externalDetailMode = normalizeExternalDetailMode(value);
radial.externalDetailMode = this.state.externalDetailMode;
this.saveSoon();
@ -881,7 +824,6 @@ export class Radial2DController extends Component {
},
);
this.numberInput(parent, this.t('control.exactOutsideFiles'), this.state.externalLinkAnchorLimit, 0, MAX_EXTERNAL_LINK_ANCHOR_LIMIT, 50, (value) => {
this.leaveCompleteMap();
this.state.externalLinkAnchorLimit = value;
radial.externalLinkAnchorLimit = value;
this.saveSoon();
@ -924,7 +866,7 @@ export class Radial2DController extends Component {
private renderLegend(parent: HTMLElement): void {
parent.createDiv({ cls: 'mwm-side-heading', text: this.t('control.legend') });
const hidden = new Set(this.state.hiddenLegendItems);
const hidden = new Set(this.radial().hiddenLegendItems);
for (const [id, labelKey, titleKey, markerClass] of LEGEND_ITEM_DEFINITIONS) {
const row = parent.createEl('label', {
cls: hidden.has(id) ? 'mwm-legend-item is-muted' : 'mwm-legend-item',
@ -937,7 +879,6 @@ export class Radial2DController extends Component {
text.createSpan({ cls: 'mwm-legend-label', text: this.t(labelKey) });
text.createSpan({ cls: 'mwm-legend-desc', text: this.t(titleKey) });
checkbox.addEventListener('change', () => {
this.leaveCompleteMap();
const value = checkbox.checked;
if (value) hidden.delete(id);
else hidden.add(id);
@ -997,8 +938,7 @@ export class Radial2DController extends Component {
}
private button(parent: HTMLElement, label: string, onClick: () => void, active = false): HTMLButtonElement {
const button = parent.createEl('button', { cls: active ? 'is-active' : '', attr: { title: label } });
button.createSpan({ cls: 'mwm-button-label', text: label });
const button = parent.createEl('button', { cls: active ? 'is-active' : '', text: label });
button.addEventListener('click', onClick);
return button;
}
@ -1011,7 +951,7 @@ export class Radial2DController extends Component {
}
private textArea(parent: HTMLElement, label: string, value: string, onChange: (value: string) => void): void {
const field = parent.createEl('label', { cls: 'mwm-panel-field mwm-panel-field-stack' });
const field = parent.createEl('label', { cls: 'mwm-panel-field' });
field.createSpan({ text: label });
const input = field.createEl('textarea');
input.value = value;
@ -1170,23 +1110,6 @@ export class Radial2DController extends Component {
return pin.kind === 'link' ? this.t('inspect.linkOverlay') : this.t(`hover.${normalizeHoverHighlightMode(pin.mode)}`);
}
private pinDisplayTitle(pin: PinPath): string {
if (pin.kind === 'link') {
const [source = pin.source ?? '', target = pin.target ?? ''] = pin.path.split(' -> ');
const sourceName = this.pathLeaf(source) || this.pathLeaf(pin.source ?? '') || this.t('common.source');
const targetName = this.pathLeaf(target) || this.pathLeaf(pin.target ?? '') || this.t('common.target');
return `${sourceName} -> ${targetName}`;
}
return this.pathLeaf(pin.path) || pin.title;
}
private pathLeaf(path: string): string {
const clean = path.trim().replace(/[/\\]+$/, '');
if (!clean || clean === '/') return '';
const leaf = clean.split(/[/\\]/).filter(Boolean).pop() ?? clean;
return leaf.endsWith('.md') ? leaf.slice(0, -3) : leaf;
}
private clearDisallowedHoverTargets(): void {
const targets = this.hoverTargets();
if (!targets.nodes) this.hoverNodeId = null;
@ -1202,7 +1125,6 @@ export class Radial2DController extends Component {
}
private resetToAtlas(): void {
this.leaveCompleteMap();
this.state.mode = 'atlas';
this.state.rootPath = this.state.rootPath || ROOT_ID;
this.state.focusPath = null;
@ -1211,7 +1133,6 @@ export class Radial2DController extends Component {
}
private resetToRoot(): void {
this.leaveCompleteMap();
this.state.mode = 'atlas';
this.state.rootPath = ROOT_ID;
this.state.focusPath = null;
@ -1229,7 +1150,6 @@ export class Radial2DController extends Component {
}
private useAsRoot(nodeId: string): void {
this.leaveCompleteMap();
this.state.mode = 'atlas';
this.state.rootPath = nodeId;
this.state.focusPath = null;
@ -1244,7 +1164,6 @@ export class Radial2DController extends Component {
}
private focusNote(nodeId: string): void {
this.leaveCompleteMap();
this.state.mode = 'focus';
this.state.focusPath = nodeId;
this.state.rootPath = ROOT_ID;

View file

@ -1,6 +1,5 @@
import {
DEFAULT_RADIAL_SETTINGS,
MAX_ATLAS_DEPTH,
MAX_EXTERNAL_LINK_ANCHOR_LIMIT,
MAX_LINK_LIMIT,
MAX_RENDER_NODE_LIMIT,
@ -58,9 +57,7 @@ export function visualNodeId(model: WorldModel, id: string | null | undefined):
function buildAtlasGraph(model: WorldModel, state: VisibleGraphState, settings: RadialSettings): VisibleWorldGraph {
const rootId = model.nodes.has(state.rootPath) ? state.rootPath : ROOT_ID;
const rootDepth = model.nodes.get(rootId)?.depth ?? 0;
const maxDepth = state.showCompleteRoot
? Math.max(MAX_ATLAS_DEPTH, model.stats.maxDepth - rootDepth)
: clampNumber(state.atlasDepth, 1, MAX_ATLAS_DEPTH, settings.atlasDepth);
const maxDepth = clampNumber(state.atlasDepth, 1, 80, settings.atlasDepth);
const query = normalizedQuery(state.search);
const visible = new Set<string>();
@ -189,14 +186,12 @@ function aggregateVisibleLinkEdges(
const aggregate = new Map<string, WorldEdge>();
const externalNodes = new Map<string, WorldNode>();
const externalHierarchyEdges: WorldEdge[] = [];
const externalLimit = state.showCompleteRoot
? MAX_EXTERNAL_LINK_ANCHOR_LIMIT
: clampNumber(
state.externalLinkAnchorLimit,
0,
MAX_EXTERNAL_LINK_ANCHOR_LIMIT,
settings.externalLinkAnchorLimit,
);
const externalLimit = clampNumber(
state.externalLinkAnchorLimit,
0,
MAX_EXTERNAL_LINK_ANCHOR_LIMIT,
settings.externalLinkAnchorLimit,
);
const externalContext = { fileCount: 0, overflowId: null as string | null };
for (const raw of model.linkEdges) {
@ -274,9 +269,7 @@ function applyNodeBudget(
focusId: string | null,
): { nodes: WorldNode[]; hiddenNodeCount: number } {
if (state.showCompleteRoot && nodes.length <= MAX_RENDER_NODE_LIMIT) return { nodes, hiddenNodeCount: 0 };
const limit = state.showCompleteRoot
? MAX_RENDER_NODE_LIMIT
: clampNumber(state.nodeLimit, 200, MAX_RENDER_NODE_LIMIT, settings.renderNodeLimit);
const limit = clampNumber(state.nodeLimit, 200, MAX_RENDER_NODE_LIMIT, settings.renderNodeLimit);
if (nodes.length <= limit) return { nodes, hiddenNodeCount: 0 };
const candidates = nodes

View file

@ -18,8 +18,6 @@
/* ---------- 控制面板 ---------- */
.galaxy-panel {
--mwm-action-button-min-height: 40px;
--mwm-link-row-min-height: 46px;
position: absolute;
top: 10px;
left: 10px;
@ -140,7 +138,6 @@
.galaxy-panel-body {
padding: 0 10px 10px;
overflow-y: auto;
overflow-x: hidden;
min-height: 0;
scrollbar-gutter: stable;
}
@ -166,15 +163,11 @@
flex-wrap: wrap;
gap: 6px;
margin-top: 10px;
align-items: stretch;
}
.galaxy-panel-row button {
display: inline-flex;
align-items: center;
justify-content: center;
flex: 1 1 132px;
min-height: var(--mwm-action-button-min-height);
flex: 1 1 104px;
min-height: 28px;
font-size: 11px;
line-height: 1.2;
padding: 5px 8px;
@ -185,19 +178,8 @@
cursor: pointer;
box-shadow: none;
min-width: 0;
max-width: 100%;
white-space: normal;
overflow-wrap: anywhere;
word-break: break-word;
box-sizing: border-box;
}
.galaxy-panel-row button .mwm-button-label {
display: block;
max-width: 100%;
min-width: 0;
overflow-wrap: anywhere;
word-break: break-word;
}
.galaxy-panel-row button:hover {
@ -357,6 +339,98 @@
font-weight: 600;
}
.gx-card {
position: absolute;
top: 0;
left: 0;
z-index: 6; /* 卡片永远盖住浮层标签G2 bug 修复) */
width: 280px;
transform: translate3d(-1000px, -1000px, 0);
pointer-events: auto;
border-radius: 10px;
padding: 12px 14px;
font-size: 12px;
background: rgba(10, 14, 24, 0.78);
backdrop-filter: blur(16px);
border: 1px solid rgba(255, 255, 255, 0.08);
color: #e8ecf6;
}
.gx-daylight .gx-card {
background: rgba(252, 250, 245, 0.88);
border: 1px solid rgba(31, 41, 51, 0.14);
color: #1f2933;
}
.gx-card-title {
font-size: 15px;
font-weight: 600;
margin-bottom: 4px;
}
.gx-card-meta {
display: flex;
align-items: center;
gap: 6px;
font-size: 11px;
opacity: 0.7;
}
.gx-card-dot {
width: 8px;
height: 8px;
border-radius: 50%;
flex-shrink: 0;
}
.gx-card-tags {
display: flex;
flex-wrap: wrap;
gap: 4px;
margin-top: 8px;
}
.gx-card-tag {
font-size: 10px;
padding: 1px 7px;
border-radius: 8px;
background: rgba(127, 155, 255, 0.18);
border: 1px solid rgba(127, 155, 255, 0.3);
}
.gx-card-stats {
margin-top: 8px;
font-size: 11px;
opacity: 0.75;
}
.gx-card-snippet {
margin-top: 6px;
font-size: 11px;
line-height: 1.5;
opacity: 0.65;
max-height: 50px;
overflow: hidden;
}
.gx-card-actions {
display: flex;
gap: 6px;
margin-top: 10px;
}
.gx-card-actions button {
flex: 1;
font-size: 11px;
padding: 3px 8px;
background: rgba(127, 127, 127, 0.12);
border: 1px solid rgba(127, 127, 127, 0.25);
color: inherit;
border-radius: 6px;
cursor: pointer;
box-shadow: none;
}
.gx-search-path {
font-size: 11px;
color: var(--text-muted);
@ -482,6 +556,20 @@
/* ---------- M4 移动端 ---------- */
/* .gx-mobile .gx-card .gx-card 更具体 靠选择器特异性覆盖 left/top/transform
JS 已在移动端清掉内联 transform OverlayManager.setSelection */
.gx-mobile .gx-card {
left: 8px;
right: 8px;
top: auto;
/* 自适应:实测 navbar 重叠(--gx-bottom-inset与 iPhone 横条安全区取大者 */
bottom: calc(10px + max(var(--gx-bottom-inset, 0px), env(safe-area-inset-bottom, 0px)));
width: auto;
max-height: 40vh;
overflow-y: auto;
transform: none;
}
.gx-mask-btn {
font-size: 13px;
padding: 8px 18px;
@ -516,7 +604,7 @@
--mwm-legend-unresolved-color: #dc4a4a;
--mwm-legend-tree-color: #93a3b8;
--mwm-legend-link-color: #a2acba;
--mwm-legend-external-link-color: #b8752e;
--mwm-legend-external-link-color: #c98135;
--mwm-floating-bg: rgba(255, 255, 255, 0.82);
--mwm-floating-border: rgba(23, 32, 51, 0.14);
--mwm-floating-color: #172033;
@ -537,7 +625,7 @@
--mwm-legend-unresolved-color: #fb7185;
--mwm-legend-tree-color: #818cf8;
--mwm-legend-link-color: #94a3b8;
--mwm-legend-external-link-color: #d97706;
--mwm-legend-external-link-color: #fb923c;
--mwm-floating-bg: rgba(10, 14, 24, 0.64);
--mwm-floating-border: rgba(255, 255, 255, 0.12);
--mwm-floating-color: #e8ecf6;
@ -700,11 +788,10 @@
.mwm-panel-tabs {
gap: 4px;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(58px, 1fr));
}
.mwm-panel-tabs button {
flex: 1 1 86px;
min-height: 28px;
font-size: 11px;
line-height: 1.2;
@ -716,9 +803,8 @@
cursor: pointer;
box-shadow: none;
min-width: 0;
max-width: 100%;
white-space: normal;
overflow-wrap: break-word;
overflow-wrap: anywhere;
}
.mwm-panel-tabs button.is-active,
@ -744,8 +830,8 @@
.mwm-panel-field,
.mwm-panel-toggle {
display: grid;
grid-template-columns: minmax(88px, 0.42fr) minmax(0, 1fr);
align-items: center;
grid-template-columns: minmax(0, 1fr);
align-items: stretch;
gap: 4px;
font-size: 11px;
}
@ -753,17 +839,11 @@
.mwm-panel-toggle {
grid-template-columns: auto minmax(0, 1fr);
justify-content: start;
align-items: start;
}
.mwm-panel-field-stack {
grid-template-columns: minmax(0, 1fr);
}
.mwm-panel-field > span,
.mwm-panel-toggle > span {
min-width: 0;
line-height: 1.2;
overflow-wrap: anywhere;
}
@ -935,24 +1015,10 @@
opacity: 0.72;
}
.mwm-neighbor-section {
display: grid;
gap: 6px;
min-width: 0;
}
.mwm-neighbor-list {
display: grid;
grid-auto-rows: minmax(var(--mwm-link-row-min-height), 1fr);
gap: 6px;
min-width: 0;
}
.mwm-pin-group {
display: grid;
gap: 6px;
margin-top: 8px;
min-width: 0;
}
.mwm-pin-group-header {
@ -1002,7 +1068,6 @@
.mwm-link-row,
.mwm-pin-main {
width: 100%;
max-width: 100%;
min-height: 28px;
padding: 5px 6px;
border: 1px solid var(--background-modifier-border);
@ -1016,91 +1081,33 @@
box-shadow: none;
white-space: normal;
overflow-wrap: anywhere;
word-break: break-word;
box-sizing: border-box;
}
.mwm-link-row {
display: flex;
align-items: center;
min-height: var(--mwm-link-row-min-height);
padding: 6px 8px;
}
.mwm-link-row-label {
display: block;
width: 100%;
min-width: 0;
overflow-wrap: anywhere;
word-break: break-word;
hyphens: auto;
}
.mwm-pin-row,
.mwm-pin-group-row {
gap: 6px;
min-width: 0;
}
.mwm-pin-row {
display: grid;
grid-template-columns: auto minmax(0, 1fr);
align-items: start;
gap: 6px 8px;
}
.mwm-pin-group-row {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
display: flex;
flex-wrap: wrap;
align-items: center;
}
.mwm-pin-group-row input {
width: 100%;
min-width: 0;
max-width: 100%;
box-sizing: border-box;
gap: 6px;
}
.mwm-pin-row.is-muted {
opacity: 0.58;
}
.mwm-pin-row > button:not(.mwm-pin-main),
.mwm-pin-group-row > button,
.mwm-pin-group-header > button,
.mwm-pin-actions > button {
.mwm-pin-group-header > button {
flex: 0 0 auto;
font-size: 10px;
padding: 3px 5px;
}
.mwm-pin-check {
margin-top: 14px;
}
.mwm-pin-main {
display: block;
grid-column: 2;
min-width: 0;
overflow-x: hidden;
flex: 1;
flex-basis: 150px;
white-space: normal;
text-align: left;
}
.mwm-pin-actions {
grid-column: 2;
display: flex;
flex-wrap: wrap;
gap: 5px;
min-width: 0;
}
.mwm-pin-actions > button {
flex: 1 1 72px;
min-width: 0;
max-width: 100%;
white-space: normal;
overflow-wrap: anywhere;
word-break: break-word;
overflow: hidden;
}
.mwm-pin-title {
@ -1110,23 +1117,6 @@
white-space: nowrap;
}
.mwm-pin-meta {
min-width: 0;
overflow-x: hidden;
overflow-y: visible;
text-overflow: clip;
white-space: normal;
overflow-wrap: break-word;
word-break: normal;
}
.mwm-scroll-path {
max-height: 56px;
overflow-x: hidden;
overflow-y: auto;
scrollbar-gutter: stable;
}
.mwm-floating-controls {
position: absolute;
top: 10px;

View file

@ -155,44 +155,6 @@ describe('visible world graph', () => {
expect(graph.focusId).toBe('Atlas/Topic A.md');
expect(graph.nodesById.has('Atlas/Sub/Topic B.md')).toBe(true);
});
it('expands the complete root atlas across depth and link budgets', () => {
const deepRecords: { path: string; basename: string; kind: 'folder' | 'note' }[] = [
{ path: 'Deep', basename: 'Deep', kind: 'folder' },
];
let current = 'Deep';
for (let index = 0; index < 8; index++) {
current = `${current}/Layer ${index}`;
deepRecords.push({ path: current, basename: `Layer ${index}`, kind: 'folder' });
}
const deepNote = `${current}/Deep.md`;
deepRecords.push({ path: deepNote, basename: 'Deep', kind: 'note' });
for (let index = 0; index < 4; index++) {
deepRecords.push({ path: `Note ${index}.md`, basename: `Note ${index}`, kind: 'note' });
}
const resolvedLinks = {
[deepNote]: Object.fromEntries(Array.from({ length: 4 }, (_, index) => [`Note ${index}.md`, 1])),
};
const m = buildWorldMap(deepRecords, resolvedLinks, {}, DEFAULT_RADIAL_SETTINGS);
const limitedState = defaultVisibleGraphState(DEFAULT_RADIAL_SETTINGS);
limitedState.atlasDepth = 1;
limitedState.linkLimit = 1;
limitedState.showLinkOverlay = true;
const limited = buildVisibleWorldGraph(m, limitedState, DEFAULT_RADIAL_SETTINGS);
const completeState = defaultVisibleGraphState(DEFAULT_RADIAL_SETTINGS);
completeState.atlasDepth = 1;
completeState.linkLimit = 1;
completeState.showLinkOverlay = true;
completeState.showCompleteRoot = true;
const complete = buildVisibleWorldGraph(m, completeState, DEFAULT_RADIAL_SETTINGS);
expect(limited.nodesById.has(deepNote)).toBe(false);
expect(limited.linkEdges).toHaveLength(1);
expect(complete.nodesById.has(deepNote)).toBe(true);
expect(complete.hiddenNodeCount).toBe(0);
expect(complete.linkEdges).toHaveLength(4);
});
});
describe('radial layout', () => {

View file

@ -5,7 +5,5 @@
"0.1.3": "1.5.0",
"0.2.0": "1.5.0",
"0.2.1": "1.5.0",
"0.2.2": "1.13.0",
"0.2.3": "1.13.0",
"0.2.4": "1.13.0"
"0.2.2": "1.13.0"
}