mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2026-08-26 05:17:06 +00:00
Design incremental declarative settings migration
This commit is contained in:
@@ -0,0 +1,494 @@
|
||||
---
|
||||
date: 2026-08-24
|
||||
commonlib-version: "0.1.19"
|
||||
self-hosted-livesync-version: "1.0.18"
|
||||
status: proposed
|
||||
---
|
||||
|
||||
# Architectural Decision Record: Adapt Standard Settings to Obsidian's Declarative API
|
||||
|
||||
## Status
|
||||
|
||||
Proposed. The first implementation is deliberately limited to one-key,
|
||||
immediately persisted controls and one proof page. It does not attempt to
|
||||
describe every existing settings interaction through a new abstraction.
|
||||
|
||||
## Context
|
||||
|
||||
Obsidian 1.13 introduced declarative plug-in settings through
|
||||
`PluginSettingTab.getSettingDefinitions()`. Declarative definitions are used for
|
||||
native rendering, validation, navigation, and global settings search. When the
|
||||
method returns a non-empty array, Obsidian does not call the existing
|
||||
`display()` implementation.
|
||||
|
||||
Self-hosted LiveSync still supports Obsidian versions before 1.13 through its
|
||||
`minAppVersion` of 1.7.2. It must therefore retain an imperative `display()`
|
||||
fallback unless the minimum supported Obsidian version is raised separately.
|
||||
Maintaining an unrelated declarative definition and imperative implementation
|
||||
for each setting would allow the two interfaces to drift.
|
||||
|
||||
The current `LiveSyncSetting` AutoWire implementation combines several
|
||||
responsibilities:
|
||||
|
||||
- Commonlib setting metadata supplies setting names, descriptions, maturity, and
|
||||
configuration level;
|
||||
- pane functions decide page and group membership, control type, options, and
|
||||
conditional visibility;
|
||||
- `LiveSyncSetting` creates and updates Obsidian DOM components;
|
||||
- `ObsidianLiveSyncSettingTab` owns an editing buffer, dirty state, local and
|
||||
persisted settings, and save operations; and
|
||||
- selected controls add staged Apply behaviour, derived values, or effects
|
||||
which run after a successful save.
|
||||
|
||||
These responsibilities are not all declarative setting data. In particular,
|
||||
Remote Configuration, Hatch, Maintenance, Setup, and Selector contain dynamic
|
||||
lists, Svelte components, diagnostic results, multi-step actions, and
|
||||
destructive confirmations. Encoding those interactions in a general settings
|
||||
DSL would increase the abstraction before a second renderer had proved which
|
||||
parts are genuinely shared.
|
||||
|
||||
Commonlib's `SettingInformation` setting metadata contains more entries than
|
||||
the current settings interface exposes. Generating definitions from all of it
|
||||
would therefore make compatibility or internal settings searchable merely
|
||||
because they have labels. Page membership must remain an explicit LiveSync
|
||||
decision.
|
||||
|
||||
The existing legacy settings wizard also changes DOM classes and selects panes
|
||||
through `enableMinimalSetup()`. The maintained onboarding path now uses
|
||||
`SetupManager`. The current call graph has no caller for
|
||||
`askAgainForSetupURI()`: it is the only emitter of
|
||||
`EVENT_REQUEST_OPEN_SETTING_WIZARD`, and that event's only handler calls
|
||||
`enableMinimalSetup()`. The old route is therefore obsolete rather than a
|
||||
second onboarding interface which the declarative renderer must preserve.
|
||||
Historically, it was the second prompt after a user reported having no Setup
|
||||
URI. It offered the in-settings wizard, P2P setup, manual settings, or a reminder
|
||||
at the next launch, then stopped initialisation while the selected interface
|
||||
took over. `SetupManager` now owns that decision and continuation.
|
||||
|
||||
## Decision
|
||||
|
||||
### Retire the obsolete in-settings wizard first
|
||||
|
||||
The old in-settings wizard will be removed as a focused prerequisite. This is
|
||||
cleanup of an unreachable interface, not part of the declarative settings
|
||||
model. The cleanup will remove:
|
||||
|
||||
- `askAgainForSetupURI()`, `EVENT_REQUEST_OPEN_SETTING_WIZARD`, its handler, and
|
||||
`enableMinimalSetup()`;
|
||||
- the `inWizard` completion branch in Sync Settings;
|
||||
- the `isWizard`, `wizardHidden`, and `wizardOnly` styling contract;
|
||||
- the General-page `Next` control and the already commented Remote
|
||||
Configuration `Next` control;
|
||||
- the now-unnecessary `wizardHidden` argument on the old pane builder; and
|
||||
- message keys whose final production consumer is the removed route, followed
|
||||
by the normal catalogue regeneration.
|
||||
|
||||
This cleanup does not affect `SetupManager`, Setup URI onboarding, QR-code
|
||||
navigation, document-history navigation, or any other control which happens to
|
||||
use the word 'Next'. The existing onboarding and ordinary settings E2E paths
|
||||
must pass before the declarative work begins.
|
||||
|
||||
The English quick-setup documentation already describes the maintained
|
||||
onboarding. Older localised quick-setup pages which still describe the removed
|
||||
interface are documentation maintenance rather than a prerequisite for this
|
||||
runtime cleanup.
|
||||
|
||||
### Use one explicit page catalogue
|
||||
|
||||
LiveSync will define one ordered page catalogue. It will be the sole source for
|
||||
page identity, name, configuration level, visibility, and content ownership.
|
||||
Each entry keeps the existing pane renderer for the legacy path and selects one
|
||||
of two native content forms:
|
||||
|
||||
```typescript
|
||||
type SettingsPageEntry = {
|
||||
id: string;
|
||||
name: () => string;
|
||||
legacy: PaneRenderer;
|
||||
native: { items: () => SettingDefinitionItem[] } | { page: () => SettingPage };
|
||||
};
|
||||
```
|
||||
|
||||
A native `items` page may mix groups of `SettingSpec` controls with Obsidian's
|
||||
direct action, render, list, and nested-page definitions. A native custom
|
||||
`SettingPage` is the final escape hatch when the page cannot yet be divided
|
||||
safely. The imperative and declarative interfaces consume the same catalogue:
|
||||
|
||||
| Catalogue content | Obsidian before 1.13 | Obsidian 1.13 and later |
|
||||
| ------------------------------------- | -------------------------------- | ------------------------------------------ |
|
||||
| Standard `SettingSpec` | Render through `LiveSyncSetting` | Convert to a control definition |
|
||||
| Native group, action, or rendered row | Use the existing pane renderer | Use `SettingDefinitionPage.items` |
|
||||
| Full custom page | Use the existing pane renderer | Open a lazily created custom `SettingPage` |
|
||||
|
||||
This makes page names and visibility consistent without requiring every page
|
||||
to migrate at once. Page names must be unique because Obsidian uses them for
|
||||
nested navigation.
|
||||
|
||||
The custom `SettingPage` adapter will be created lazily from the 1.13-or-later
|
||||
path. It must feature-detect the runtime API and must not instantiate or
|
||||
subclass `SettingPage` while the module is loading on an older supported
|
||||
Obsidian version. The adapter sets `title` from the catalogue and renders pane
|
||||
content into the host-provided `containerEl`. Its `hide()` boundary will unload
|
||||
the page-owned `Component`, unmount Svelte and markdown content, and remove
|
||||
page-owned update handlers. The parent tab's `hide()` remains a final cleanup
|
||||
boundary because Obsidian does not guarantee a page-level `hide()` call when
|
||||
the host window is destroyed.
|
||||
|
||||
Custom pages receive only the current page's `containerEl` and the existing
|
||||
`addPanel` helper. They do not recreate the old top-level tab menu inside each
|
||||
native page.
|
||||
|
||||
### Prefer native groups and searchable rows to full custom pages
|
||||
|
||||
An existing `addPanel` section maps naturally to a
|
||||
`SettingDefinitionGroup`. Within that group:
|
||||
|
||||
- ordinary value controls use `SettingSpec`;
|
||||
- a simple button operation may use `SettingDefinitionAction` directly;
|
||||
- a Svelte control or a specialised Obsidian row uses
|
||||
`SettingDefinitionRender` and returns its cleanup callback; and
|
||||
- a truly dynamic collection may use `SettingDefinitionList` when its existing
|
||||
behaviour already matches the list contract.
|
||||
|
||||
These Obsidian-specific definitions are written directly in the page adapter.
|
||||
They are not added to the shared `SettingSpec` vocabulary. This keeps the shared
|
||||
model small while allowing page, panel, and row names, descriptions, and aliases
|
||||
to participate in settings search.
|
||||
|
||||
Search indexes the metadata on a definition; it does not infer searchable
|
||||
entries from arbitrary DOM created inside a `render` callback. A whole panel
|
||||
wrapped in one rendered row therefore provides panel-level search only.
|
||||
Individual controls or actions require individual standard, action, or render
|
||||
definitions when control-level search is worthwhile.
|
||||
|
||||
A full custom `SettingPage` remains acceptable for a workflow which cannot yet
|
||||
be split without nesting several existing setting rows inside one synthetic
|
||||
row. It is a compatibility escape hatch, not the default representation for
|
||||
every complex pane.
|
||||
|
||||
### Keep `SettingSpec` intentionally small
|
||||
|
||||
`SettingSpec` describes only controls which have all of the following
|
||||
properties:
|
||||
|
||||
- one explicit persisted setting key, excluding keys from `OnDialogSettings`;
|
||||
- one standard toggle, text, textarea, number, or dropdown control;
|
||||
- a value which is read from the current editing buffer;
|
||||
- a change which can be persisted immediately through the existing
|
||||
`saveSettings([key])` path; and
|
||||
- no additional operation which must run after saving.
|
||||
|
||||
A representative, key-safe shape is:
|
||||
|
||||
```typescript
|
||||
type PersistedBooleanSettingKey = Exclude<AllBooleanItemKey, keyof OnDialogSettings>;
|
||||
type PersistedStringSettingKey = Exclude<AllStringItemKey, keyof OnDialogSettings>;
|
||||
type PersistedNumericSettingKey = Exclude<AllNumericItemKey, keyof OnDialogSettings>;
|
||||
type PersistedSettingKey = PersistedBooleanSettingKey | PersistedStringSettingKey | PersistedNumericSettingKey;
|
||||
|
||||
type SettingSpecBase<K extends PersistedSettingKey, C> = {
|
||||
key: K;
|
||||
control: C;
|
||||
visible?: () => boolean;
|
||||
disabled?: () => boolean;
|
||||
aliases?: string[];
|
||||
};
|
||||
|
||||
type SettingSpec =
|
||||
| SettingSpecBase<PersistedBooleanSettingKey, { type: "toggle"; defaultValue?: boolean }>
|
||||
| SettingSpecBase<PersistedStringSettingKey, { type: "text"; placeholder?: string }>
|
||||
| SettingSpecBase<PersistedStringSettingKey, { type: "textarea"; placeholder?: string; rows?: number }>
|
||||
| SettingSpecBase<
|
||||
PersistedNumericSettingKey,
|
||||
{
|
||||
type: "number";
|
||||
min?: number;
|
||||
max?: number;
|
||||
validate?: (value: number) => string | void;
|
||||
}
|
||||
>
|
||||
| SettingSpecBase<
|
||||
PersistedStringSettingKey,
|
||||
{
|
||||
type: "dropdown";
|
||||
options: () => Record<string, string>;
|
||||
}
|
||||
>;
|
||||
```
|
||||
|
||||
Names, descriptions, maturity labels, placeholders, and configuration levels
|
||||
come from the translated Commonlib setting metadata by default. The native
|
||||
renderer appends the existing maturity marker to `name`, maps the description
|
||||
and placeholder directly, and combines the metadata level with the
|
||||
specification's `visible` predicate. The specification may override a label
|
||||
only where the current interface already uses a deliberate product-specific
|
||||
label. Options remain LiveSync owned because they can depend on the active
|
||||
remote, platform, or language. A control which needs the current obsolete-row
|
||||
styling remains custom because the native definition does not provide an
|
||||
equivalent per-row class contract.
|
||||
|
||||
The catalogue explicitly lists each exposed key. It does not enumerate
|
||||
`SettingInformation` automatically.
|
||||
|
||||
The following behaviours are outside the standard specification and remain a
|
||||
custom row or custom page:
|
||||
|
||||
- `holdValue` and Apply buttons;
|
||||
- `invert` bindings;
|
||||
- password inputs;
|
||||
- a control which maps one displayed value to several stored keys, such as
|
||||
`syncMode`;
|
||||
- a control with an `onSaved` service, event, restart, rebuild, or re-render
|
||||
effect;
|
||||
- button clusters, dynamic lists, Svelte components, rich diagnostic output,
|
||||
or destructive actions; and
|
||||
- styling which exists only to support the old wizard or tab menu.
|
||||
|
||||
This is a migration boundary, not a permanent prohibition. A second concrete
|
||||
use may justify a focused extension, but the first implementation will not add
|
||||
a generic action language, transaction language, or lifecycle hook system.
|
||||
|
||||
### Retain the existing editing and persistence owner
|
||||
|
||||
`ObsidianLiveSyncSettingTab` remains the owner of editing values and saves. The
|
||||
first implementation will expose a small adapter over its existing methods
|
||||
rather than move settings persistence into a new service.
|
||||
|
||||
For declarative controls:
|
||||
|
||||
- `getControlValue(key)` reads `editingSettings[key]`;
|
||||
- `setControlValue(key, value)` resolves an explicitly registered standard
|
||||
specification, updates the editing value, and calls `saveSettings([key])`;
|
||||
- successful saves continue to pass through `saveLocalSetting()` or
|
||||
`services.setting.saveSettingData()` as appropriate; and
|
||||
- the tab calls `refreshDomState()` after a value changes when another
|
||||
definition's `visible` or `disabled` predicate can depend on it.
|
||||
|
||||
An unknown key is an implementation error. The adapter must not fall through
|
||||
to `plugin.settings`, because LiveSync does not use that conventional storage
|
||||
shape.
|
||||
|
||||
Specification construction and `getSettingDefinitions()` must remain cheap
|
||||
and side-effect free. Obsidian calls the method during search indexing and
|
||||
again on updates; it must perform no file, database, network, or settings
|
||||
write.
|
||||
|
||||
### Give imperative pages an explicit lifetime and refresh boundary
|
||||
|
||||
The present `display()` renders every pane together, so arrays of
|
||||
`settingComponents`, controlled DOM updates, and `onSavedHandlers` can be
|
||||
cleared and rebuilt as one unit. Native page navigation mounts one custom page
|
||||
at a time. Reusing those arrays without a page boundary would leak updates from
|
||||
a hidden page or remove effects which a staged edit still needs.
|
||||
|
||||
Each imperative render will therefore receive a small page scope containing:
|
||||
|
||||
- its `Component` lifetime;
|
||||
- its `LiveSyncSetting` instances;
|
||||
- its controlled DOM update functions; and
|
||||
- its explicit cleanup callbacks for Svelte, markdown, and other mounted
|
||||
content.
|
||||
|
||||
The legacy `display()` fallback uses one scope for the complete old tab. A
|
||||
custom declarative page creates one scope when opened and disposes it when
|
||||
hidden. This scope is renderer state and is not part of `SettingSpec`.
|
||||
|
||||
Saved-setting effects remain owned by the tab session, not by a DOM page. The
|
||||
existing handlers are unique by setting key, so `addOnSaved()` will replace the
|
||||
handler for that key instead of appending duplicate closures when a page is
|
||||
reopened. A later migration may declare those effects in a separate catalogue,
|
||||
but they will not be added to the standard control specification merely to
|
||||
support page navigation.
|
||||
|
||||
Direct calls to `this.display()` from pane code and the tab's own reload path
|
||||
will be replaced by an explicit refresh request with one of two scopes:
|
||||
|
||||
- `page` re-renders the active custom page, or the legacy tab; and
|
||||
- `catalogue` calls the declarative tab's `update()` so translated page names,
|
||||
page visibility, and search definitions are rebuilt, or re-renders the
|
||||
legacy tab.
|
||||
|
||||
Changing the display language or the Advanced, Power User, or Edge Case mode
|
||||
uses a catalogue refresh. Dynamic Selector rows, Maintenance status, and
|
||||
Hidden File Sync status use a page refresh. This keeps the renderer choice out
|
||||
of pane actions and prevents a direct `display()` call from replacing native
|
||||
declarative navigation.
|
||||
|
||||
### Preserve imperative rendering as a renderer
|
||||
|
||||
The existing AutoWire calls are not the shared model. Instead,
|
||||
`LiveSyncSetting` becomes the legacy renderer for `SettingSpec` where a pane
|
||||
has migrated. It continues to own DOM classes, dirty-value decoration, and
|
||||
component updates for older Obsidian versions.
|
||||
|
||||
Unmigrated pane functions continue to call `LiveSyncSetting` directly inside a
|
||||
custom page. This allows incremental migration without first rewriting their
|
||||
behaviour.
|
||||
|
||||
The initial implementation must not modify Commonlib's setting metadata.
|
||||
Commonlib owns setting identity and shared labels; LiveSync owns page placement,
|
||||
Obsidian controls, persistence routing, and side effects.
|
||||
|
||||
### Use Advanced as the first proof page
|
||||
|
||||
The Advanced page is the first page whose groups contain only standard
|
||||
`SettingSpec` controls. It provides a useful proof without introducing
|
||||
unrelated workflows:
|
||||
|
||||
- number, dropdown, and toggle controls;
|
||||
- translated Commonlib labels;
|
||||
- minimum-value validation and a default value;
|
||||
- CouchDB-dependent visibility; and
|
||||
- configuration-level page visibility.
|
||||
|
||||
It has no current `onSaved` handler, Svelte component, staged Apply group, or
|
||||
destructive action. General is not the first proof because changing the display
|
||||
language re-renders the interface and other controls emit status events after
|
||||
saving. Those effects should remain imperative until the standard binding has
|
||||
been proven.
|
||||
|
||||
At Stage C, other pages use native groups and searchable rows where their
|
||||
existing panels divide cleanly. Only the remaining full custom pages are limited
|
||||
to page-level search. A later, focused migration can split those workflows into
|
||||
standard, action, or rendered rows without changing the page catalogue.
|
||||
|
||||
## Implementation Stages and Checkpoint
|
||||
|
||||
### Stage A: remove the old wizard
|
||||
|
||||
Complete the focused prerequisite described above. This removes a DOM contract
|
||||
which would otherwise distort both renderers.
|
||||
|
||||
### Stage B: prove the shared standard-control model
|
||||
|
||||
The first declarative-settings change remains deliberately small. It will:
|
||||
|
||||
1. add the minimal `SettingSpec` type and pure conversion functions;
|
||||
2. describe only the Advanced controls as specifications;
|
||||
3. render those specifications through the existing `LiveSyncSetting` path;
|
||||
4. prove conversion to Obsidian setting definitions with focused tests; and
|
||||
5. retain the current `display()` behaviour, page menu, persistence owner, and
|
||||
`minAppVersion`, without returning non-empty setting definitions.
|
||||
|
||||
Stage B does not enable the native declarative renderer. It proves that the
|
||||
shared model can express a real page without first taking ownership of every
|
||||
page's lifetime. Returning an empty definition array merely to silence review
|
||||
output is not an outcome of this stage.
|
||||
|
||||
### Stage C: activate native pages
|
||||
|
||||
Activation is a separate checkpoint because it is the first cross-cutting
|
||||
change. It will add the page catalogue, custom `SettingPage` adapter, scoped
|
||||
imperative lifetime, renderer-neutral refresh operation, declarative control
|
||||
read and write overrides, and non-empty definitions on Obsidian 1.13 or later.
|
||||
It will expose each remaining pane through native groups and rendered rows where
|
||||
the existing panels divide cleanly, use a full custom native page only as a
|
||||
fallback, and retain the imperative renderer for older Obsidian versions.
|
||||
|
||||
This stage necessarily touches direct `display()` callers, saved-handler
|
||||
ownership, and cleanup for Svelte and markdown content. Review its measured
|
||||
patch and focused test plan with the maintainer before implementation. Do not
|
||||
expand `SettingSpec` to absorb those concerns merely to make activation appear
|
||||
smaller.
|
||||
|
||||
## Verification
|
||||
|
||||
Stage A will run the maintained onboarding E2E scenario and an ordinary
|
||||
settings navigation scenario. A source check will confirm that no old wizard
|
||||
event, state, class, or message consumer remains.
|
||||
|
||||
Stage B focused unit tests will verify:
|
||||
|
||||
- only explicitly listed Advanced controls become specifications;
|
||||
- synthetic `OnDialogSettings` keys cannot be standard specifications;
|
||||
- control type, options, defaults, validation, metadata, and visibility map
|
||||
consistently to the legacy and native representations; and
|
||||
- rendering the Advanced specifications through `LiveSyncSetting` preserves
|
||||
the current save behaviour.
|
||||
|
||||
Stage C focused unit tests will verify:
|
||||
|
||||
- the page catalogue has stable, unique identifiers and names;
|
||||
- every standard setting key is registered once;
|
||||
- reads use the editing buffer;
|
||||
- writes use `saveSettings([key])` and never `plugin.settings`; and
|
||||
- custom pages remain custom rather than being flattened into incomplete
|
||||
definitions.
|
||||
|
||||
Real-Obsidian verification on 1.13 or later will confirm:
|
||||
|
||||
- native page navigation opens every page;
|
||||
- Advanced controls appear in global settings search;
|
||||
- Advanced values persist and are restored after reopening settings;
|
||||
- CouchDB-dependent controls and Advanced-mode visibility update correctly;
|
||||
- a representative custom page, including its cleanup, still works;
|
||||
- page and catalogue refreshes preserve native navigation; and
|
||||
- no duplicate save, update handler, or saved-setting effect occurs after
|
||||
leaving and reopening a page.
|
||||
|
||||
Because the manifest continues to support earlier Obsidian versions, a
|
||||
pre-1.13 real-runtime smoke test must confirm that the imperative fallback
|
||||
still opens, navigates, saves one Advanced value, and opens one custom page. If
|
||||
the maintained E2E runner cannot install that runtime, the exact manual version
|
||||
and procedure must be recorded before the implementation is merged.
|
||||
|
||||
The current real-Obsidian runner defaults to Obsidian 1.12.7, so it already
|
||||
owns the fallback smoke path. The declarative path requires a separate 1.13-or-
|
||||
later AppImage selected through `E2E_OBSIDIAN_VERSION`. If a reviewable 1.13
|
||||
runtime is not available, the implementation may remain a branch proof but the
|
||||
new runtime path must not be merged on type-level evidence alone.
|
||||
|
||||
Existing E2E scenarios must use one shared settings-page navigation helper.
|
||||
That helper uses the current `.sls-setting-menu-btn` contract on the legacy
|
||||
runtime and accessible native page names on 1.13 or later. Individual scenarios
|
||||
must not duplicate version checks or retain selectors for a menu which the
|
||||
declarative renderer does not create.
|
||||
|
||||
## Expansion Checkpoints
|
||||
|
||||
Review the scope with the maintainer before any implementation adds one of the
|
||||
following:
|
||||
|
||||
- a generic representation of actions, confirmations, rebuilds, or service
|
||||
lifecycles;
|
||||
- staged multi-setting transactions in `SettingSpec`;
|
||||
- a replacement for the current onboarding workflow;
|
||||
- a Commonlib setting metadata contract change;
|
||||
- a minimum Obsidian version increase; or
|
||||
- conversion of Remote Configuration, Hatch, Maintenance, Setup, or the
|
||||
Svelte-based Selector controls.
|
||||
|
||||
These may become worthwhile after the first proof, but none is required to
|
||||
establish a shared standard-control model and native settings search.
|
||||
|
||||
## Alternatives Rejected
|
||||
|
||||
### Return an empty definition array
|
||||
|
||||
This can satisfy a syntactic lint check while retaining `display()`, but it
|
||||
does not add native settings search or prove a migration path.
|
||||
|
||||
### Generate every setting from Commonlib setting metadata
|
||||
|
||||
Metadata does not define current page membership, control type, options,
|
||||
visibility, save policy, or whether a compatibility key should be exposed.
|
||||
Automatic generation would expose settings which the current interface omits.
|
||||
|
||||
### Teach `LiveSyncSetting` to run against a simulated DOM
|
||||
|
||||
The existing class is a renderer with direct component and element access.
|
||||
Making it emulate declarative output would preserve its mixed responsibilities
|
||||
and make the new API depend on implementation details of the old one.
|
||||
|
||||
### Model every pane before adopting the API
|
||||
|
||||
This would require a general action and lifecycle language for approximately
|
||||
50 buttons, several dynamic lists, five Svelte-based regular-expression
|
||||
controls, and multiple recovery workflows. The resulting framework would be
|
||||
larger than the standard-control problem it is intended to solve.
|
||||
|
||||
## References
|
||||
|
||||
- [Migrate to declarative settings](https://docs.obsidian.md/plugins/guides/migrate-declarative-settings)
|
||||
- Obsidian `PluginSettingTab`, `SettingDefinitionItem`, and `SettingPage` type
|
||||
declarations from the dependency version locked by this repository
|
||||
Reference in New Issue
Block a user