Separate declarative root group identity from display

This commit is contained in:
vorotamoroz
2026-08-25 07:29:56 +00:00
parent 070ce0e307
commit 131ffeb0ef
4 changed files with 126 additions and 52 deletions
@@ -140,6 +140,12 @@ by the catalogue, while the imperative renderer continues to pass the same
emoji to its existing menu button. This preserves the established visual
identity without adding host-DOM manipulation.
`SettingDefinitionGroup` likewise exposes only a string heading. Root groups
therefore have semantic identifiers whose catalogue entries keep their emoji
and late-translated names separate. The adapter combines those fields only
when constructing the Obsidian definition, so callers select a group by its
identifier instead of repeating presentation strings.
### Compose the native landing page around common tasks
The declarative root is a composition of native groups and catalogue pages,
@@ -64,7 +64,9 @@ import {
createExtraMenuSettingDefinitions,
createGeneralSettingDefinitionGroups,
createSettingsPageCatalogue,
getSettingsRootGroupEntry,
type SettingsPageEntry,
type SettingsRootGroupId,
} from "./SettingsPageCatalogue.ts";
import { createAdvancedSettingSpecGroups } from "./AdvancedSettingSpecs.ts";
import { isValidSettingSpecValue, type SettingSpec } from "./SettingSpec.ts";
@@ -73,6 +75,7 @@ import type {
SettingDefinitionGroup,
SettingDefinitionItem,
SettingDefinitionPage,
SettingGroupItem,
} from "obsidian";
import { createExtraMenuSettingSpecGroup, createGeneralSettingSpecGroups } from "./GeneralSettingSpecs.ts";
import { SetupManager } from "@/modules/features/SetupManager.ts";
@@ -785,7 +788,7 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
if (!appearance || !logging) {
throw new Error("General settings must define Appearance and Logging groups");
}
return this.createPageGroup(`⚙️ ${$msg("obsidianLiveSyncSettingTab.panelGeneralSettings")}`, [
return this.createRootGroup("general-settings", [
{
type: "page",
name: `🎨 ${appearance.heading}`,
@@ -809,53 +812,44 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
}
private createQuickSetupGroup(): SettingDefinitionGroup {
return {
type: "group",
heading: `🧙‍♂️ ${$msg("obsidianLiveSyncSettingTab.titleQuickSetup")}`,
items: [
{
name: $msg("obsidianLiveSyncSettingTab.nameConnectSetupURI"),
desc: $msg("obsidianLiveSyncSettingTab.descConnectSetupURI"),
action: () => this.requestOpenSetupURI(),
},
{
name: $msg("Rerun Onboarding Wizard"),
desc: $msg("Rerun the onboarding wizard to set up Self-hosted LiveSync again."),
action: () => fireAndForget(async () => await this.rerunOnboardingWizard()),
},
{
name: $msg("obsidianLiveSyncSettingTab.nameEnableLiveSync"),
desc: $msg("obsidianLiveSyncSettingTab.descEnableLiveSync"),
visible: () => !this.isConfiguredAs("isConfigured", true),
action: () => fireAndForget(async () => await this.enableLiveSyncFromSettings()),
},
],
};
return this.createRootGroup("quick-setup", [
{
name: $msg("obsidianLiveSyncSettingTab.nameConnectSetupURI"),
desc: $msg("obsidianLiveSyncSettingTab.descConnectSetupURI"),
action: () => this.requestOpenSetupURI(),
},
{
name: $msg("Rerun Onboarding Wizard"),
desc: $msg("Rerun the onboarding wizard to set up Self-hosted LiveSync again."),
action: () => fireAndForget(async () => await this.rerunOnboardingWizard()),
},
{
name: $msg("obsidianLiveSyncSettingTab.nameEnableLiveSync"),
desc: $msg("obsidianLiveSyncSettingTab.descEnableLiveSync"),
visible: () => !this.isConfiguredAs("isConfigured", true),
action: () => fireAndForget(async () => await this.enableLiveSyncFromSettings()),
},
]);
}
private createSynchronisationGroup(pages: SettingDefinitionPage[]): SettingDefinitionGroup {
return this.createPageGroup(`🔄 ${$msg("obsidianLiveSyncSettingTab.titleSynchronisation")}`, pages);
}
private createPageGroup(
heading: string,
pages: SettingDefinitionPage[],
private createRootGroup(
id: SettingsRootGroupId,
items: SettingGroupItem[],
visible?: () => boolean
): SettingDefinitionGroup {
const { icon, name } = getSettingsRootGroupEntry(id);
return {
type: "group",
heading,
items: pages,
heading: `${icon} ${name()}`,
items,
...(visible ? { visible } : {}),
};
}
private createSetupOtherDevicesGroup(): SettingDefinitionGroup {
return {
type: "group",
heading: `📲 ${$msg("obsidianLiveSyncSettingTab.titleSetupOtherDevices")}`,
visible: () => this.isConfiguredAs("isConfigured", true),
items: [
return this.createRootGroup(
"setup-other-devices",
[
{
name: $msg("obsidianLiveSyncSettingTab.nameCopySetupURI"),
desc: $msg("obsidianLiveSyncSettingTab.descCopySetupURI"),
@@ -867,7 +861,8 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
action: () => this.requestShowSetupQRCode(),
},
],
};
() => this.isConfiguredAs("isConfigured", true)
);
}
override getSettingDefinitions(): SettingDefinitionItem[] {
@@ -882,34 +877,34 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
}
return this.createDeclarativePage(entry);
};
const synchronisation = this.createSynchronisationGroup([
const synchronisation = this.createRootGroup("synchronisation", [
getPage("remote-configuration"),
getPage("synchronisation"),
]);
const generalSettings = this.createGeneralSettingsGroup();
const quickSetup = this.createQuickSetupGroup();
const setupOtherDevices = this.createSetupOtherDevicesGroup();
const maintenance = this.createPageGroup(
`🛠️ ${$msg("obsidianLiveSyncSettingTab.titleMaintenanceAndRecovery")}`,
[getPage("maintenance"), getPage("hatch")]
);
const extraFeatures = this.createPageGroup(
`🧩 ${$msg("obsidianLiveSyncSettingTab.titleExtraFeaturesGroup")}`,
const maintenance = this.createRootGroup("maintenance-and-recovery", [
getPage("maintenance"),
getPage("hatch"),
]);
const extraFeatures = this.createRootGroup(
"extra-features",
[getPage("selector"), getPage("customisation-sync")],
() => this.isPageVisible(LEVEL_ADVANCED)
);
const advancedSettings = this.createPageGroup(
`🔧 ${$msg("obsidianLiveSyncSettingTab.titleAdvancedSettings")}`,
const advancedSettings = this.createRootGroup(
"advanced-settings",
[getPage("advanced"), getPage("power-users"), getPage("patches")],
() =>
this.isPageVisible(LEVEL_ADVANCED) ||
this.isPageVisible(LEVEL_POWER_USER) ||
this.isPageVisible(LEVEL_EDGE_CASE)
);
const helpAndInformation = this.createPageGroup(
`${$msg("obsidianLiveSyncSettingTab.titleHelpAndInformation")}`,
[getPage("help"), getPage("change-log")]
);
const helpAndInformation = this.createRootGroup("help-and-information", [
getPage("help"),
getPage("change-log"),
]);
const laterGroups = [setupOtherDevices, maintenance, extraFeatures, advancedSettings, helpAndInformation];
if (this.isAnySyncEnabled()) {
@@ -50,6 +50,54 @@ export type SettingsPageEntry = {
legacy: SettingsPaneRenderer;
};
export type SettingsRootGroupEntry = {
icon: string;
name: () => string;
};
/** Root groups used only by Obsidian's declarative settings landing page. */
const SETTINGS_ROOT_GROUP_CATALOGUE = {
"quick-setup": {
icon: "🧙‍♂️",
name: () => $msg("obsidianLiveSyncSettingTab.titleQuickSetup"),
},
synchronisation: {
icon: "🔄",
name: () => $msg("obsidianLiveSyncSettingTab.titleSynchronisation"),
},
"general-settings": {
icon: "⚙️",
name: () => $msg("obsidianLiveSyncSettingTab.panelGeneralSettings"),
},
"setup-other-devices": {
icon: "📲",
name: () => $msg("obsidianLiveSyncSettingTab.titleSetupOtherDevices"),
},
"maintenance-and-recovery": {
icon: "🛠️",
name: () => $msg("obsidianLiveSyncSettingTab.titleMaintenanceAndRecovery"),
},
"extra-features": {
icon: "🧩",
name: () => $msg("obsidianLiveSyncSettingTab.titleExtraFeaturesGroup"),
},
"advanced-settings": {
icon: "🔧",
name: () => $msg("obsidianLiveSyncSettingTab.titleAdvancedSettings"),
},
"help-and-information": {
icon: "️",
name: () => $msg("obsidianLiveSyncSettingTab.titleHelpAndInformation"),
},
} as const satisfies Record<string, SettingsRootGroupEntry>;
export type SettingsRootGroupId = keyof typeof SETTINGS_ROOT_GROUP_CATALOGUE;
/** Resolve a late-translated root-group label while keeping its icon separate until the Obsidian API boundary. */
export function getSettingsRootGroupEntry(id: SettingsRootGroupId): SettingsRootGroupEntry {
return SETTINGS_ROOT_GROUP_CATALOGUE[id];
}
/**
* Build the explicit page list in the same order as the existing settings tab.
*
@@ -18,9 +18,34 @@ vi.mock("./PanePowerUsers.ts", () => ({ panePowerUsers: vi.fn() }));
vi.mock("./PanePatches.ts", () => ({ panePatches: vi.fn() }));
vi.mock("./PaneMaintenance.ts", () => ({ paneMaintenance: vi.fn() }));
import { createAdvancedSettingDefinitionGroups, createSettingsPageCatalogue } from "./SettingsPageCatalogue.ts";
import {
createAdvancedSettingDefinitionGroups,
createSettingsPageCatalogue,
getSettingsRootGroupEntry,
type SettingsRootGroupId,
} from "./SettingsPageCatalogue.ts";
const EXPECTED_ROOT_GROUPS = [
["quick-setup", "🧙‍♂️", "obsidianLiveSyncSettingTab.titleQuickSetup"],
["synchronisation", "🔄", "obsidianLiveSyncSettingTab.titleSynchronisation"],
["general-settings", "⚙️", "obsidianLiveSyncSettingTab.panelGeneralSettings"],
["setup-other-devices", "📲", "obsidianLiveSyncSettingTab.titleSetupOtherDevices"],
["maintenance-and-recovery", "🛠️", "obsidianLiveSyncSettingTab.titleMaintenanceAndRecovery"],
["extra-features", "🧩", "obsidianLiveSyncSettingTab.titleExtraFeaturesGroup"],
["advanced-settings", "🔧", "obsidianLiveSyncSettingTab.titleAdvancedSettings"],
["help-and-information", "️", "obsidianLiveSyncSettingTab.titleHelpAndInformation"],
] as const satisfies readonly (readonly [SettingsRootGroupId, string, string])[];
describe("settings page catalogue", () => {
it("keeps each declarative root-group icon separate from its late-translated name", () => {
expect(
EXPECTED_ROOT_GROUPS.map(([id]) => {
const { icon, name } = getSettingsRootGroupEntry(id);
return [id, icon, name()];
})
).toEqual(EXPECTED_ROOT_GROUPS);
});
it("registers every existing page once and keeps only Advanced native", () => {
const catalogue = createSettingsPageCatalogue();