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 emoji to its existing menu button. This preserves the established visual
identity without adding host-DOM manipulation. 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 ### Compose the native landing page around common tasks
The declarative root is a composition of native groups and catalogue pages, The declarative root is a composition of native groups and catalogue pages,
@@ -64,7 +64,9 @@ import {
createExtraMenuSettingDefinitions, createExtraMenuSettingDefinitions,
createGeneralSettingDefinitionGroups, createGeneralSettingDefinitionGroups,
createSettingsPageCatalogue, createSettingsPageCatalogue,
getSettingsRootGroupEntry,
type SettingsPageEntry, type SettingsPageEntry,
type SettingsRootGroupId,
} from "./SettingsPageCatalogue.ts"; } from "./SettingsPageCatalogue.ts";
import { createAdvancedSettingSpecGroups } from "./AdvancedSettingSpecs.ts"; import { createAdvancedSettingSpecGroups } from "./AdvancedSettingSpecs.ts";
import { isValidSettingSpecValue, type SettingSpec } from "./SettingSpec.ts"; import { isValidSettingSpecValue, type SettingSpec } from "./SettingSpec.ts";
@@ -73,6 +75,7 @@ import type {
SettingDefinitionGroup, SettingDefinitionGroup,
SettingDefinitionItem, SettingDefinitionItem,
SettingDefinitionPage, SettingDefinitionPage,
SettingGroupItem,
} from "obsidian"; } from "obsidian";
import { createExtraMenuSettingSpecGroup, createGeneralSettingSpecGroups } from "./GeneralSettingSpecs.ts"; import { createExtraMenuSettingSpecGroup, createGeneralSettingSpecGroups } from "./GeneralSettingSpecs.ts";
import { SetupManager } from "@/modules/features/SetupManager.ts"; import { SetupManager } from "@/modules/features/SetupManager.ts";
@@ -785,7 +788,7 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
if (!appearance || !logging) { if (!appearance || !logging) {
throw new Error("General settings must define Appearance and Logging groups"); throw new Error("General settings must define Appearance and Logging groups");
} }
return this.createPageGroup(`⚙️ ${$msg("obsidianLiveSyncSettingTab.panelGeneralSettings")}`, [ return this.createRootGroup("general-settings", [
{ {
type: "page", type: "page",
name: `🎨 ${appearance.heading}`, name: `🎨 ${appearance.heading}`,
@@ -809,53 +812,44 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
} }
private createQuickSetupGroup(): SettingDefinitionGroup { private createQuickSetupGroup(): SettingDefinitionGroup {
return { return this.createRootGroup("quick-setup", [
type: "group", {
heading: `🧙‍♂️ ${$msg("obsidianLiveSyncSettingTab.titleQuickSetup")}`, name: $msg("obsidianLiveSyncSettingTab.nameConnectSetupURI"),
items: [ desc: $msg("obsidianLiveSyncSettingTab.descConnectSetupURI"),
{ action: () => this.requestOpenSetupURI(),
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("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),
name: $msg("obsidianLiveSyncSettingTab.nameEnableLiveSync"), action: () => fireAndForget(async () => await this.enableLiveSyncFromSettings()),
desc: $msg("obsidianLiveSyncSettingTab.descEnableLiveSync"), },
visible: () => !this.isConfiguredAs("isConfigured", true), ]);
action: () => fireAndForget(async () => await this.enableLiveSyncFromSettings()),
},
],
};
} }
private createSynchronisationGroup(pages: SettingDefinitionPage[]): SettingDefinitionGroup { private createRootGroup(
return this.createPageGroup(`🔄 ${$msg("obsidianLiveSyncSettingTab.titleSynchronisation")}`, pages); id: SettingsRootGroupId,
} items: SettingGroupItem[],
private createPageGroup(
heading: string,
pages: SettingDefinitionPage[],
visible?: () => boolean visible?: () => boolean
): SettingDefinitionGroup { ): SettingDefinitionGroup {
const { icon, name } = getSettingsRootGroupEntry(id);
return { return {
type: "group", type: "group",
heading, heading: `${icon} ${name()}`,
items: pages, items,
...(visible ? { visible } : {}), ...(visible ? { visible } : {}),
}; };
} }
private createSetupOtherDevicesGroup(): SettingDefinitionGroup { private createSetupOtherDevicesGroup(): SettingDefinitionGroup {
return { return this.createRootGroup(
type: "group", "setup-other-devices",
heading: `📲 ${$msg("obsidianLiveSyncSettingTab.titleSetupOtherDevices")}`, [
visible: () => this.isConfiguredAs("isConfigured", true),
items: [
{ {
name: $msg("obsidianLiveSyncSettingTab.nameCopySetupURI"), name: $msg("obsidianLiveSyncSettingTab.nameCopySetupURI"),
desc: $msg("obsidianLiveSyncSettingTab.descCopySetupURI"), desc: $msg("obsidianLiveSyncSettingTab.descCopySetupURI"),
@@ -867,7 +861,8 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
action: () => this.requestShowSetupQRCode(), action: () => this.requestShowSetupQRCode(),
}, },
], ],
}; () => this.isConfiguredAs("isConfigured", true)
);
} }
override getSettingDefinitions(): SettingDefinitionItem[] { override getSettingDefinitions(): SettingDefinitionItem[] {
@@ -882,34 +877,34 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
} }
return this.createDeclarativePage(entry); return this.createDeclarativePage(entry);
}; };
const synchronisation = this.createSynchronisationGroup([ const synchronisation = this.createRootGroup("synchronisation", [
getPage("remote-configuration"), getPage("remote-configuration"),
getPage("synchronisation"), getPage("synchronisation"),
]); ]);
const generalSettings = this.createGeneralSettingsGroup(); const generalSettings = this.createGeneralSettingsGroup();
const quickSetup = this.createQuickSetupGroup(); const quickSetup = this.createQuickSetupGroup();
const setupOtherDevices = this.createSetupOtherDevicesGroup(); const setupOtherDevices = this.createSetupOtherDevicesGroup();
const maintenance = this.createPageGroup( const maintenance = this.createRootGroup("maintenance-and-recovery", [
`🛠️ ${$msg("obsidianLiveSyncSettingTab.titleMaintenanceAndRecovery")}`, getPage("maintenance"),
[getPage("maintenance"), getPage("hatch")] getPage("hatch"),
); ]);
const extraFeatures = this.createPageGroup( const extraFeatures = this.createRootGroup(
`🧩 ${$msg("obsidianLiveSyncSettingTab.titleExtraFeaturesGroup")}`, "extra-features",
[getPage("selector"), getPage("customisation-sync")], [getPage("selector"), getPage("customisation-sync")],
() => this.isPageVisible(LEVEL_ADVANCED) () => this.isPageVisible(LEVEL_ADVANCED)
); );
const advancedSettings = this.createPageGroup( const advancedSettings = this.createRootGroup(
`🔧 ${$msg("obsidianLiveSyncSettingTab.titleAdvancedSettings")}`, "advanced-settings",
[getPage("advanced"), getPage("power-users"), getPage("patches")], [getPage("advanced"), getPage("power-users"), getPage("patches")],
() => () =>
this.isPageVisible(LEVEL_ADVANCED) || this.isPageVisible(LEVEL_ADVANCED) ||
this.isPageVisible(LEVEL_POWER_USER) || this.isPageVisible(LEVEL_POWER_USER) ||
this.isPageVisible(LEVEL_EDGE_CASE) this.isPageVisible(LEVEL_EDGE_CASE)
); );
const helpAndInformation = this.createPageGroup( const helpAndInformation = this.createRootGroup("help-and-information", [
`${$msg("obsidianLiveSyncSettingTab.titleHelpAndInformation")}`, getPage("help"),
[getPage("help"), getPage("change-log")] getPage("change-log"),
); ]);
const laterGroups = [setupOtherDevices, maintenance, extraFeatures, advancedSettings, helpAndInformation]; const laterGroups = [setupOtherDevices, maintenance, extraFeatures, advancedSettings, helpAndInformation];
if (this.isAnySyncEnabled()) { if (this.isAnySyncEnabled()) {
@@ -50,6 +50,54 @@ export type SettingsPageEntry = {
legacy: SettingsPaneRenderer; 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. * 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("./PanePatches.ts", () => ({ panePatches: vi.fn() }));
vi.mock("./PaneMaintenance.ts", () => ({ paneMaintenance: 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", () => { 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", () => { it("registers every existing page once and keeps only Advanced native", () => {
const catalogue = createSettingsPageCatalogue(); const catalogue = createSettingsPageCatalogue();