diff --git a/docs/adr/2026_08_declarative_settings_adapter.md b/docs/adr/2026_08_declarative_settings_adapter.md index 6ee9cfe0..ea0aff46 100644 --- a/docs/adr/2026_08_declarative_settings_adapter.md +++ b/docs/adr/2026_08_declarative_settings_adapter.md @@ -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, diff --git a/src/modules/features/SettingDialogue/ObsidianLiveSyncSettingTab.ts b/src/modules/features/SettingDialogue/ObsidianLiveSyncSettingTab.ts index 7acf83bb..48c23b47 100644 --- a/src/modules/features/SettingDialogue/ObsidianLiveSyncSettingTab.ts +++ b/src/modules/features/SettingDialogue/ObsidianLiveSyncSettingTab.ts @@ -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()) { diff --git a/src/modules/features/SettingDialogue/SettingsPageCatalogue.ts b/src/modules/features/SettingDialogue/SettingsPageCatalogue.ts index 80353da5..c25fa19e 100644 --- a/src/modules/features/SettingDialogue/SettingsPageCatalogue.ts +++ b/src/modules/features/SettingDialogue/SettingsPageCatalogue.ts @@ -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; + +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. * diff --git a/src/modules/features/SettingDialogue/SettingsPageCatalogue.unit.spec.ts b/src/modules/features/SettingDialogue/SettingsPageCatalogue.unit.spec.ts index eacfdd27..9de8ff93 100644 --- a/src/modules/features/SettingDialogue/SettingsPageCatalogue.unit.spec.ts +++ b/src/modules/features/SettingDialogue/SettingsPageCatalogue.unit.spec.ts @@ -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();