Release 0.2.2

This commit is contained in:
Kur0mi 2026-06-24 22:31:25 +02:00
parent 53cc944d25
commit f6cdeb16c6
14 changed files with 372 additions and 353 deletions

View file

@ -1,5 +1,12 @@
# Changelog
## 0.2.2
- Address Obsidian community plugin review findings around settings APIs, command names, deprecated slider tooltips, direct static styles, and config folder handling.
- Raise the minimum supported Obsidian version to 1.13.0 for the current settings definitions API.
- Remove reveal CSS that relied on partially supported browser features in older Obsidian builds.
- Tighten 2D neighbor list typing and add regression coverage for configured vault folders.
## 0.2.1
- Add default day/night map background fallbacks when the active Obsidian theme cannot provide a matching background color.

348
main.js

File diff suppressed because one or more lines are too long

View file

@ -1,10 +1,10 @@
{
"id": "mini-world-map",
"name": "Mini World Map",
"version": "0.2.1",
"minAppVersion": "1.5.0",
"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",
"authorUrl": "https://github.com/Miro0o",
"isDesktopOnly": false
}
}

4
package-lock.json generated
View file

@ -1,12 +1,12 @@
{
"name": "mini-world-map",
"version": "0.2.1",
"version": "0.2.2",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "mini-world-map",
"version": "0.2.1",
"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.1",
"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

@ -1,4 +1,4 @@
import { Notice, Plugin, PluginSettingTab, Setting } from 'obsidian';
import { Notice, Plugin, PluginSettingTab, Setting, type App, type SettingDefinitionItem, type SettingDefinitionRender } from 'obsidian';
import { VIEW_TYPE_MINI_WORLD_MAP } from './constants';
import type { Language, MiniWorldMapSettings, ViewMode } from './settings';
import {
@ -10,6 +10,7 @@ import {
MAX_LINK_LIMIT,
MAX_RENDER_NODE_LIMIT,
MAX_SWIRL_STRENGTH,
applyVaultConfigDirDefault,
clampNumber,
mergeSettings,
normalizeHoverHighlightMode,
@ -26,7 +27,8 @@ export default class MiniWorldMapPlugin extends Plugin {
async onload(): Promise<void> {
console.info('[Mini World Map] loading');
this.settings = mergeSettings(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));
this.addRibbonIcon('network', 'Open Mini World Map', () => {
@ -35,13 +37,13 @@ export default class MiniWorldMapPlugin extends Plugin {
this.addCommand({
id: 'open-map',
name: 'Open Mini World Map',
name: 'Open map',
callback: () => void this.activateView(),
});
this.addCommand({
id: 'toggle-render-mode',
name: 'Toggle Mini World Map render mode',
name: 'Toggle render mode',
callback: () => {
this.setViewMode(this.settings.viewMode === 'radial2d' ? 'map3d' : 'radial2d');
void this.activateView();
@ -50,7 +52,7 @@ export default class MiniWorldMapPlugin extends Plugin {
this.addCommand({
id: 'rebuild-index',
name: 'Rebuild Mini World Map index',
name: 'Rebuild index',
callback: () => {
let rebuilt = false;
for (const leaf of this.app.workspace.getLeavesOfType(VIEW_TYPE_MINI_WORLD_MAP)) {
@ -66,7 +68,7 @@ export default class MiniWorldMapPlugin extends Plugin {
this.addCommand({
id: 'search-3d',
name: 'Search Mini World Map',
name: 'Search map',
callback: () => {
void this.activateView().then((view) => {
const controller = view?.controller;
@ -119,167 +121,155 @@ export default class MiniWorldMapPlugin extends Plugin {
class MiniWorldMapSettingTab extends PluginSettingTab {
constructor(
app: import('obsidian').App,
app: App,
private plugin: MiniWorldMapPlugin,
) {
super(app, plugin);
}
display(): void {
const { containerEl } = this;
getSettingDefinitions(): SettingDefinitionItem[] {
const radial = this.plugin.settings.radial;
const language = this.plugin.settings.language;
containerEl.empty();
containerEl.createEl('h2', { text: t(language, 'settings.title') });
new Setting(containerEl)
.setName(t(language, 'language'))
.setDesc(t(language, 'settings.languageDesc'))
.addDropdown((dropdown) => {
for (const [value, label] of languageOptions(language)) dropdown.addOption(value, label);
dropdown.setValue(this.plugin.settings.language).onChange(async (value) => {
this.plugin.settings.language = normalizeLanguage(value);
await this.plugin.saveSettings();
this.display();
});
});
new Setting(containerEl)
.setName(t(language, 'settings.defaultMode'))
.setDesc(t(language, 'settings.defaultModeDesc'))
.addDropdown((dropdown) =>
dropdown
.addOption('radial2d', viewModeLabel(language, 'radial2d'))
.addOption('map3d', viewModeLabel(language, 'map3d'))
.setValue(this.plugin.settings.viewMode)
.onChange(async (value) => {
this.plugin.settings.viewMode = normalizeViewMode(value);
await this.plugin.saveSettings();
return [
{
type: 'group',
heading: t(language, 'settings.title'),
items: [
this.renderSetting(t(language, 'language'), t(language, 'settings.languageDesc'), (setting) => {
setting.addDropdown((dropdown) => {
for (const [value, label] of languageOptions(language)) dropdown.addOption(value, label);
dropdown.setValue(this.plugin.settings.language).onChange(async (value) => {
this.plugin.settings.language = normalizeLanguage(value);
await this.plugin.saveSettings();
this.update();
});
});
}),
);
new Setting(containerEl)
.setName(t(language, 'control.defaultDepth'))
.setDesc(t(language, 'settings.depthDesc'))
.addSlider((slider) =>
slider
.setLimits(1, 20, 1)
.setValue(radial.atlasDepth)
.setDynamicTooltip()
.onChange(async (value) => {
radial.atlasDepth = clampNumber(value, 1, MAX_ATLAS_DEPTH, DEFAULT_RADIAL_SETTINGS.atlasDepth);
await this.plugin.saveSettings();
this.renderSetting(t(language, 'settings.defaultMode'), t(language, 'settings.defaultModeDesc'), (setting) => {
setting.addDropdown((dropdown) =>
dropdown
.addOption('radial2d', viewModeLabel(language, 'radial2d'))
.addOption('map3d', viewModeLabel(language, 'map3d'))
.setValue(this.plugin.settings.viewMode)
.onChange(async (value) => {
this.plugin.settings.viewMode = normalizeViewMode(value);
await this.plugin.saveSettings();
}),
);
}),
);
new Setting(containerEl)
.setName(t(language, 'control.defaultNodes'))
.setDesc(t(language, 'settings.nodeLimitDesc'))
.addText((text) =>
text
.setPlaceholder(String(DEFAULT_RADIAL_SETTINGS.renderNodeLimit))
.setValue(String(radial.renderNodeLimit))
.onChange(async (value) => {
radial.renderNodeLimit = clampNumber(value, 200, MAX_RENDER_NODE_LIMIT, DEFAULT_RADIAL_SETTINGS.renderNodeLimit);
await this.plugin.saveSettings();
this.renderSetting(t(language, 'control.defaultDepth'), t(language, 'settings.depthDesc'), (setting) => {
setting.addSlider((slider) =>
slider
.setLimits(1, 20, 1)
.setValue(radial.atlasDepth)
.onChange(async (value) => {
radial.atlasDepth = clampNumber(value, 1, MAX_ATLAS_DEPTH, DEFAULT_RADIAL_SETTINGS.atlasDepth);
await this.plugin.saveSettings();
}),
);
}),
);
new Setting(containerEl)
.setName(t(language, 'control.defaultNoteLinks'))
.setDesc(t(language, 'settings.linkLimitDesc'))
.addText((text) =>
text
.setPlaceholder(String(DEFAULT_RADIAL_SETTINGS.linkLimit))
.setValue(String(radial.linkLimit))
.onChange(async (value) => {
radial.linkLimit = clampNumber(value, 0, MAX_LINK_LIMIT, DEFAULT_RADIAL_SETTINGS.linkLimit);
await this.plugin.saveSettings();
this.renderSetting(t(language, 'control.defaultNodes'), t(language, 'settings.nodeLimitDesc'), (setting) => {
setting.addText((text) =>
text
.setPlaceholder(String(DEFAULT_RADIAL_SETTINGS.renderNodeLimit))
.setValue(String(radial.renderNodeLimit))
.onChange(async (value) => {
radial.renderNodeLimit = clampNumber(value, 200, MAX_RENDER_NODE_LIMIT, DEFAULT_RADIAL_SETTINGS.renderNodeLimit);
await this.plugin.saveSettings();
}),
);
}),
);
new Setting(containerEl)
.setName(t(language, 'control.showNoteLinks'))
.setDesc(t(language, 'settings.showLinksDesc'))
.addToggle((toggle) =>
toggle.setValue(radial.showLinkOverlay && !radial.hiddenLegendItems.includes('link')).onChange(async (value) => {
radial.showLinkOverlay = value;
const hidden = new Set(radial.hiddenLegendItems);
if (value) hidden.delete('link');
else hidden.add('link');
radial.hiddenLegendItems = [...hidden];
await this.plugin.saveSettings();
}),
);
new Setting(containerEl)
.setName(t(language, 'control.hover'))
.setDesc(t(language, 'settings.hoverDesc'))
.addDropdown((dropdown) => {
for (const [value, label] of hoverModeOptions(language, HOVER_HIGHLIGHT_MODE_OPTIONS)) dropdown.addOption(value, label);
dropdown.setValue(radial.hoverHighlightMode).onChange(async (value) => {
radial.hoverHighlightMode = normalizeHoverHighlightMode(value);
await this.plugin.saveSettings();
});
});
new Setting(containerEl)
.setName(t(language, 'control.hoverTargets'))
.setDesc(t(language, 'settings.hoverTargetsDesc'))
.addDropdown((dropdown) => {
for (const [value, label] of hoverTargetOptions(language, HOVER_TARGET_MODE_OPTIONS)) dropdown.addOption(value, label);
dropdown.setValue(radial.hoverTargetMode).onChange(async (value) => {
radial.hoverTargetMode = normalizeHoverTargetMode(value);
await this.plugin.saveSettings();
});
});
new Setting(containerEl)
.setName(t(language, 'control.labels'))
.setDesc(t(language, 'settings.labelsDesc'))
.addDropdown((dropdown) => {
for (const [value, label] of labelVisibilityOptions(language, LABEL_VISIBILITY_OPTIONS)) dropdown.addOption(value, label);
dropdown.setValue(radial.labelVisibility).onChange(async (value) => {
radial.labelVisibility = normalizeLabelVisibility(value);
await this.plugin.saveSettings();
});
});
new Setting(containerEl)
.setName(t(language, 'control.spin'))
.setDesc(t(language, 'settings.spinDesc'))
.addSlider((slider) =>
slider
.setLimits(0, MAX_SWIRL_STRENGTH, 1)
.setValue(radial.swirlStrength)
.setDynamicTooltip()
.onChange(async (value) => {
radial.swirlStrength = clampNumber(value, 0, MAX_SWIRL_STRENGTH, DEFAULT_RADIAL_SETTINGS.swirlStrength);
await this.plugin.saveSettings();
this.renderSetting(t(language, 'control.defaultNoteLinks'), t(language, 'settings.linkLimitDesc'), (setting) => {
setting.addText((text) =>
text
.setPlaceholder(String(DEFAULT_RADIAL_SETTINGS.linkLimit))
.setValue(String(radial.linkLimit))
.onChange(async (value) => {
radial.linkLimit = clampNumber(value, 0, MAX_LINK_LIMIT, DEFAULT_RADIAL_SETTINGS.linkLimit);
await this.plugin.saveSettings();
}),
);
}),
);
this.renderSetting(t(language, 'control.showNoteLinks'), t(language, 'settings.showLinksDesc'), (setting) => {
setting.addToggle((toggle) =>
toggle.setValue(radial.showLinkOverlay && !radial.hiddenLegendItems.includes('link')).onChange(async (value) => {
radial.showLinkOverlay = value;
const hidden = new Set(radial.hiddenLegendItems);
if (value) hidden.delete('link');
else hidden.add('link');
radial.hiddenLegendItems = [...hidden];
await this.plugin.saveSettings();
}),
);
}),
this.renderSetting(t(language, 'control.hover'), t(language, 'settings.hoverDesc'), (setting) => {
setting.addDropdown((dropdown) => {
for (const [value, label] of hoverModeOptions(language, HOVER_HIGHLIGHT_MODE_OPTIONS)) dropdown.addOption(value, label);
dropdown.setValue(radial.hoverHighlightMode).onChange(async (value) => {
radial.hoverHighlightMode = normalizeHoverHighlightMode(value);
await this.plugin.saveSettings();
});
});
}),
this.renderSetting(t(language, 'control.hoverTargets'), t(language, 'settings.hoverTargetsDesc'), (setting) => {
setting.addDropdown((dropdown) => {
for (const [value, label] of hoverTargetOptions(language, HOVER_TARGET_MODE_OPTIONS)) dropdown.addOption(value, label);
dropdown.setValue(radial.hoverTargetMode).onChange(async (value) => {
radial.hoverTargetMode = normalizeHoverTargetMode(value);
await this.plugin.saveSettings();
});
});
}),
this.renderSetting(t(language, 'control.labels'), t(language, 'settings.labelsDesc'), (setting) => {
setting.addDropdown((dropdown) => {
for (const [value, label] of labelVisibilityOptions(language, LABEL_VISIBILITY_OPTIONS)) dropdown.addOption(value, label);
dropdown.setValue(radial.labelVisibility).onChange(async (value) => {
radial.labelVisibility = normalizeLabelVisibility(value);
await this.plugin.saveSettings();
});
});
}),
this.renderSetting(t(language, 'control.spin'), t(language, 'settings.spinDesc'), (setting) => {
setting.addSlider((slider) =>
slider
.setLimits(0, MAX_SWIRL_STRENGTH, 1)
.setValue(radial.swirlStrength)
.onChange(async (value) => {
radial.swirlStrength = clampNumber(value, 0, MAX_SWIRL_STRENGTH, DEFAULT_RADIAL_SETTINGS.swirlStrength);
await this.plugin.saveSettings();
}),
);
}),
this.renderSetting(t(language, 'control.unresolvedLinks'), t(language, 'settings.unresolvedDesc'), (setting) => {
setting.addToggle((toggle) =>
toggle.setValue(radial.includeUnresolvedLinks).onChange(async (value) => {
radial.includeUnresolvedLinks = value;
await this.plugin.saveSettings();
}),
);
}),
this.renderSetting(t(language, 'control.ignoredFolders'), t(language, 'settings.ignoredDesc'), (setting) => {
setting.addTextArea((text) =>
text.setValue(radial.ignoreFolders.join('\n')).onChange(async (value) => {
radial.ignoreFolders = value
.split('\n')
.map((line) => line.trim())
.filter(Boolean);
await this.plugin.saveSettings();
}),
);
}),
],
},
];
}
new Setting(containerEl)
.setName(t(language, 'control.unresolvedLinks'))
.setDesc(t(language, 'settings.unresolvedDesc'))
.addToggle((toggle) =>
toggle.setValue(radial.includeUnresolvedLinks).onChange(async (value) => {
radial.includeUnresolvedLinks = value;
await this.plugin.saveSettings();
}),
);
new Setting(containerEl)
.setName(t(language, 'control.ignoredFolders'))
.setDesc(t(language, 'settings.ignoredDesc'))
.addTextArea((text) =>
text.setValue(radial.ignoreFolders.join('\n')).onChange(async (value) => {
radial.ignoreFolders = value
.split('\n')
.map((line) => line.trim())
.filter(Boolean);
await this.plugin.saveSettings();
}),
);
private renderSetting(name: string, desc: string, render: (setting: Setting) => void): SettingDefinitionRender {
return {
name,
desc,
render,
};
}
}

View file

@ -236,9 +236,6 @@ export class RadialRenderer {
showLoadingMask(text: string): void {
cancelAnimationFrame(this.revealFrame);
this.revealDepthLimit = Number.POSITIVE_INFINITY;
this.container.style.setProperty('--mwm-reveal-x', '50%');
this.container.style.setProperty('--mwm-reveal-y', '50%');
this.container.style.setProperty('--mwm-reveal-radius', '0px');
this.container.removeClass('is-radial-revealing');
this.container.removeClass('is-radial-depth-revealing');
this.container.addClass('is-radial-preparing');

View file

@ -35,9 +35,8 @@ function normalizeCssColor(value: string): string | null {
const doc = activeDocument;
const body = doc.body;
const probe = doc.createElement('span');
probe.style.color = value;
probe.setCssStyles({ color: value, display: 'none' });
if (!probe.style.color) return null;
probe.style.display = 'none';
body.appendChild(probe);
try {
const computed = doc.defaultView?.getComputedStyle(probe).color ?? getComputedStyle(probe).color;

View file

@ -141,7 +141,7 @@ export const DEFAULT_RADIAL_SETTINGS: RadialSettings = {
swirlStrength: 0,
showRingGuides: false,
hiddenLegendItems: [],
ignoreFolders: ['.git', '.obsidian'],
ignoreFolders: ['.git'],
};
export const DEFAULT_GALAXY_SETTINGS: GalaxySettings = {
@ -181,6 +181,15 @@ export function mergeSettings(saved: unknown): MiniWorldMapSettings {
};
}
export function applyVaultConfigDirDefault(settings: MiniWorldMapSettings, saved: unknown, configDir: string): MiniWorldMapSettings {
if (hasSavedIgnoreFolders(saved)) return settings;
const normalized = normalizeIgnoredFolder(configDir);
if (normalized && !settings.radial.ignoreFolders.includes(normalized)) {
settings.radial.ignoreFolders = [...settings.radial.ignoreFolders, normalized];
}
return settings;
}
export function mergeRadialSettings(saved: unknown): RadialSettings {
const s = isRecord(saved) ? saved : {};
const d = DEFAULT_RADIAL_SETTINGS;
@ -348,6 +357,19 @@ function finiteNumber(value: unknown, fallback: number): number {
return typeof value === 'number' && Number.isFinite(value) ? value : Number.parseFloat(String(value)) || fallback;
}
function hasSavedIgnoreFolders(saved: unknown): boolean {
const raw = isRecord(saved) ? saved : {};
const radial = isRecord(raw['radial']) ? raw['radial'] : raw;
return Array.isArray(radial['ignoreFolders']);
}
function normalizeIgnoredFolder(path: string): string {
return path
.trim()
.replace(/\\/g, '/')
.replace(/^\/+|\/+$/g, '');
}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null;
}

View file

@ -2,7 +2,7 @@ import type { App } from 'obsidian';
import { TFile, TFolder } from 'obsidian';
import type { RadialSettings } from '../settings';
import { buildWorldMap, normalizeVaultPath } from './buildWorldMap';
import type { LinkTable, VisibleGraphState, VisibleWorldGraph, WorldFileRecord, WorldModel, WorldNode } from './types';
import type { LinkTable, VisibleGraphState, VisibleWorldGraph, WorldEdge, WorldFileRecord, WorldModel, WorldNode } from './types';
import { ROOT_ID } from './types';
import { buildVisibleWorldGraph, visualNodeId } from './visibleGraph';
@ -35,12 +35,12 @@ export class WorldMapIndex {
};
}
get linkEdgesBySource() {
return this.model?.linkEdgesBySource ?? new Map();
get linkEdgesBySource(): Map<string, WorldEdge[]> {
return this.model?.linkEdgesBySource ?? new Map<string, WorldEdge[]>();
}
get linkEdgesByTarget() {
return this.model?.linkEdgesByTarget ?? new Map();
get linkEdgesByTarget(): Map<string, WorldEdge[]> {
return this.model?.linkEdgesByTarget ?? new Map<string, WorldEdge[]>();
}
rebuild(settings: RadialSettings = this.settings): void {

View file

@ -1147,11 +1147,6 @@
height: 16px;
}
.mwm-radial-host.is-radial-revealing .mwm-radial-canvas,
.mwm-radial-host.is-radial-revealing .mwm-radial-labels {
clip-path: circle(var(--mwm-reveal-radius, 0px) at var(--mwm-reveal-x, 50%) var(--mwm-reveal-y, 50%));
}
.mwm-radial-host.is-radial-preparing .mwm-radial-canvas,
.mwm-radial-host.is-radial-preparing .mwm-radial-labels {
opacity: 0;

View file

@ -1,5 +1,5 @@
import { describe, expect, it } from 'vitest';
import { mergeSettings } from '../src/settings';
import { applyVaultConfigDirDefault, mergeSettings } from '../src/settings';
describe('Mini World Map settings migration', () => {
it('defaults to 2D radial mode and preserves legacy radial keys', () => {
@ -33,6 +33,14 @@ describe('Mini World Map settings migration', () => {
expect(mergeSettings({ radial: { hoverTargetMode: 'anything' } }).radial.hoverTargetMode).toBe('nodes');
});
it('adds the current vault config folder to default ignored folders', () => {
const settings = applyVaultConfigDirDefault(mergeSettings({}), {}, '.custom-config');
expect(settings.radial.ignoreFolders).toContain('.custom-config');
const explicit = applyVaultConfigDirDefault(mergeSettings({ radial: { ignoreFolders: ['Archive'] } }), { radial: { ignoreFolders: ['Archive'] } }, '.custom-config');
expect(explicit.radial.ignoreFolders).toEqual(['Archive']);
});
it('preserves intentional nested 2D note-link visibility', () => {
expect(mergeSettings({ radial: { showLinkOverlay: false } }).radial.showLinkOverlay).toBe(false);
});

View file

@ -16,7 +16,7 @@ const records = [
];
function model(overrides: Partial<typeof DEFAULT_RADIAL_SETTINGS> = {}) {
const settings = { ...DEFAULT_RADIAL_SETTINGS, ...overrides };
const settings = { ...DEFAULT_RADIAL_SETTINGS, ignoreFolders: [...DEFAULT_RADIAL_SETTINGS.ignoreFolders, '.obsidian'], ...overrides };
return buildWorldMap(
records,
{

View file

@ -4,5 +4,6 @@
"0.1.2": "1.5.0",
"0.1.3": "1.5.0",
"0.2.0": "1.5.0",
"0.2.1": "1.5.0"
"0.2.1": "1.5.0",
"0.2.2": "1.13.0"
}