Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Configuration and Settings

The configuration system provides typed, validated, reactive settings with automatic persistence.

ConfigurationSchema

ConfigurationSchema (packages/api/src/lib/configuration.svelte.ts) is the central schema registry for app and manifest-declared settings. It extends EventDispatcher and emits updated whenever the registered schema surface changes.

Schema Types

Settings are registered with schema-backed descriptors that validate values at write time and materialize defaults into persisted configuration on first load:

TypeShapeDescription
Stringtype: "string"Free-text value
Numbertype: "number"Numeric value; minimum and maximum render as a slider/range control
Booleantype: "boolean"Toggle
Enumtype: "string" + enum[]Fixed set of choices rendered as a select
Enum Arraytype: "array" of enumMulti-select list rendered from a multiple-select dropdown
Object Grouptype: "object"Nested settings section with inherited group title and description
Record Objecttype: "object" with schema-valued additionalPropertiesDynamic key/value map rendered as an editable table
Customtype: "custom"Arbitrary plugin-owned settings surface

Object groups are flattened into individual persisted keys, but the schema keeps group metadata so the renderer can preserve subsection titles and descriptions.

Methods

MethodDescription
register(key, schema)Register a configuration key with its schema and default.
getConfiguration(key)Returns a WorkspaceConfiguration proxy for the key.
validateConfigurationOptionValidates and normalizes a value against the registered key.
materializeSchemaDefaults()Persists missing defaults and restores invalid stored values.
load() / save()Persist to/from the data adapter.

WorkspaceConfiguration

WorkspaceConfiguration acts as a typed accessor for a configuration namespace:

MethodDescription
has(key)Check if a key exists.
get(key)Retrieve the current value.
update(key, value)Write a new value (validates against schema).
entries()Iterate all key-value pairs.

Invalid enum choices, out-of-range numeric values, and invalid enum-array entries are rejected before persistence. The workspace settings UI surfaces those failures through the normal settings interaction flow instead of silently accepting malformed values.

Persistence

Configuration is stored as JSON files via the DataAdapter:

FileContents
app.jsonCore application settings plus canonical plugin compatibility data under pluginData
appearance.jsonAppearance-specific settings
hotkeys.jsonVault-scoped command hotkey overrides, keyed by command ID
core-plugins.jsonDisabled bundled plugin IDs plus optional bundled-plugin enable overrides
community-plugins.jsonEnabled external plugin IDs, including installed official plugins for compatibility
installed-plugins.jsonVerified external install records and provenance
plugins/<id>/data.jsonExternal plugin compatibility mirror of canonical app.json plugin data
<id>.jsonBundled core/system plugin compatibility mirror of canonical app.json plugin data

The external official lapis-telemetry plugin stores its browser telemetry configuration through external plugin data under plugins/lapis-telemetry/data.json while canonical values remain under app.json pluginData. Its settings include enablement, web-vitals capture, debug logging, IndexedDB diagnostics persistence, sample rate, slow-span threshold, and optional OTLP endpoint.

Compatibility Plugin.loadData() and Plugin.saveData() now treat app.json as the source of truth for opaque plugin-owned settings payloads. The configuration layer stores those values under the top-level pluginData object, rewrites the legacy Obsidian plugin file on save, prefers canonical pluginData when both copies exist, and only falls back to the legacy file to backfill missing canonical entries during migration. After boot, external edits to app.json are reloaded through the configuration service and mirrored back into registered plugin legacy files so the on-disk compatibility layout stays in sync.

When boot finds an enabled external plugin whose manifest.json is missing, it removes that stale ID from community-plugins.json and notifies the user instead of repeatedly trying to activate a deleted plugin.

Manifest-declared plugin configuration stays namespaced under the contributing plugin ID, or an explicit contribution ID when one is provided. That keeps plugin-owned keys isolated from unrelated app settings and safe to ignore if a plugin is later removed.

Settings UI

The settings UI is built from a component library in packages/workspace.

When a registered schema contributes a single top-level section, the workspace configuration UI preserves the schema’s declarative title for the visible section heading instead of autogenerating a label from the schema or contribution id. That includes flat single-property contributions (for example csv-lint with title CSV Lint) and schemas that expand to exactly one nested subgroup (for example files.links.* under Files and Links).

The settings sidebar uses an Obsidian-style flat navigation model rather than exposing the raw schema tree. The Options, Core plugins, and Community plugins group labels are stable, even when a plugin group has no settings entries yet. The Options group contains curated app surfaces (Workspace, Editor, Files and Links, Appearance, Hotkeys, Plugin registry, Core plugins, and Community plugins) with Lucide-backed icons. Core plugins and Community plugins then list enabled plugins that expose settings through either an imperative SettingTab, manifest-declared configuration, or both. Selecting a nav item renders only that surface in the content pane; app schemas remain the implementation detail used to build the selected form.

The settings sidebar uses the shared sidebar provider’s resize state with an initial width of 14rem. Its resize rail updates the provider-owned --sidebar-width value directly, so dragging the handle changes the navigation width without hiding the selected settings surface or covering the content pane. Unlike the workspace dock rails, the settings panel keeps its visible resize separator at 2px on hover/focus so the modal chrome stays visually quiet. On small viewports, settings content exposes an icon-only top-left trigger that opens the shared sidebar provider’s mobile navigation sheet without changing the selected settings surface.

Schema-driven surfaces render each category as an unframed heading followed by a rounded settings panel. The panel owns the subtle background and row dividers; the category heading itself has no top border or sticky setting-row styling. Setting rows use their own setting title only because the surrounding category already provides the path context. This keeps declarative settings visually aligned with Obsidian’s settings model without changing the global Setting API used by imperative plugin tabs.

Imperative plugin tabs that call Setting.setHeading() emit the same section shape through API-owned hooks:

  • setting-section — section wrapper
  • setting-section-heading setting-control-heading — heading block outside the panel
  • setting-section-heading-title / setting-section-heading-description — title and optional description
  • setting-section-body — rounded panel that owns row dividers and receives subsequent new Setting(containerEl) rows until the next heading

The plugin tab root (.workspace-shell__settings-plugin-tab) is a form wrapper only; panel chrome lives on section bodies, matching .workspace-shell__settings-schema-body styling in the workspace shell.

The settings sidebar includes structured search above that navigation. Non-empty queries use the same shared Fuse.js helper as the command palette to rank schema-backed settings, app settings tabs, plugin rows, plugin manifest metadata, diagnostics/features, and declarative configuration metadata. The normal sidebar navigation stays visible while grouped result cards render in the main settings pane. Results highlight matching text and navigate to the canonical surface for the matched schema setting, settings tab, or plugin row. Imperative plugin settings tabs are not mounted off-screen for indexing; only structured metadata already owned by the settings shell is searched.

AppSettings Builder

AppSettings provides a declarative builder API for constructing settings panels:

const settings = new AppSettings(app).addGroup("Appearance", (group) => {
  group
    .addSetting("Theme", (setting) => {
      setting
        .setName("Color theme")
        .setDesc("Choose light or dark")
        .addDropdown((dropdown) => {
          dropdown
            .addOptions({ light: "Light", dark: "Dark", system: "System" })
            .setValue(config.get("theme"))
            .onChange((value) => config.update("theme", value));
        });
    })
    .addSetting("Font size", (setting) => {
      setting.setName("Font size").addSlider((slider) => {
        slider.setLimits(12, 24, 1).setValue(fontSize).onChange(updateFont);
      });
    });
});

Setting Components

ComponentPurpose
SettingGroupNamed group with collapsible header
SettingSingle setting row with name, description, and control
addToggleBoolean on/off switch
addTextText input
addTextAreaMulti-line text input
addDropdownSelect from options or enum-array multi-select
addSliderNumeric range
addColorPickerColor value
addButtonAction button
addExtraButtonSecondary action
addMomentFormatDate/time format string
addSearchSearchable selection
addObjectMapDynamic record-object editor with add/remove rows

Unsupported declarative shapes still stay visible in the schema-driven form, but they fall back to an Edit in app.json button that opens the vault’s app.json file in a centered in-app floating editor pane instead of rendering a misleading partial control.

Plugin Settings Sections

The workspace exposes two plugin-management sections in Settings:

SectionPurpose
Core pluginsLists bundled plugins plus installed official plugins, allows toggling non-required entries, and links their setting surfaces
Community pluginsLists installed community/manual plugins, allows toggling them, and links their setting surfaces

Required bundled plugins remain visible in the core list but cannot be disabled. Installed official plugins load from /.obsidian/plugins/<id>/, keep enablement in community-plugins.json for compatibility, and still appear under Core plugins when installed-plugins.json records provenance: "official".

Plugin rows that expose inline panel content now use an inline disclosure model. The row shows a trailing chevron affordance, starts collapsed, and expands the existing shared Details | Features panel in place when the non-control part of the row is activated. Rows without details metadata or feature sections stay non-expandable and keep their visible summary metadata. Settings-search navigation that targets a plugin row reveals a collapsed row before scrolling and highlighting it.

The Community Plugins section also owns the workspace-trust controls for hosted community plugins. It shows the current trust state for the active vault, offers explicit Trust and Revoke actions, and keeps trust policy visible next to plugin diagnostics and enablement controls instead of burying it in an unrelated app settings section.

The compatibility path still allows plugin-owned settings tabs backed by direct DOM construction. For new Lapis-native extensions, the preferred direction is declarative schemas plus app-owned controls for routine settings surfaces. Plugin-list settings affordances open the plugin’s canonical settings surface. If a plugin has both an imperative SettingTab and owned declarative configuration sections, the settings panel renders the imperative tab first and then the declarative form sections under the same plugin sidebar item.

When Settings is already open, plugin-list affordances and other settings-target navigation update the selected surface in place. They must not close and reopen the settings panel just to force a tab remount, because that creates a visible full-panel flash and breaks the Obsidian-style in-panel navigation model.

SettingTab

Plugins can add their own settings tabs:

class MySettingTab extends SettingTab {
  display(containerEl: HTMLElement): void {
    // Build settings UI using the builder API
  }
}

// In plugin onload:
this.addSettingTab(new MySettingTab(this.app, this));

Plugin setting surfaces are grouped under the settings section that matches the runtime source: bundled and official plugins under Core plugins, community/manual plugins under Community plugins. Core plugin manifest diagnostics can index the same plugin ID as the loaded runtime plugin; settings navigation and search de-duplicate those records so the loaded plugin surface appears once. Synthetic webview records created for compatibility by registerWebView() stay registered, but the settings sidebar filters those categoryId: "extensions" schema records out of the app-owned section model so .addSettingTab() tabs do not also appear as schema-derived sidebar groups.

The sidebar only renders plugin surfaces that have an imperative tab or declarative settings to show. The top-level plugin management entries under Options remain visible independently.

The Options section also includes the app-owned Hotkeys tab. It renders the command registry, filters by command text or pressed-hotkey tags, supports an assigned-only filter, and edits the CommandManager hotkey override API. The search input owns the pressed-hotkey capture affordance: clicking its keyboard button replaces that icon with Press hotkey, consumes keydown events until an idle gap commits the captured shortcut tags, and exits without adding a tag on Escape. Typed search text shows an in-input clear button. Rows can add multiple hotkeys, remove individual bindings, reset customized commands to defaults, and show conflict diagnostics without blocking the save. Changes persist to .obsidian/hotkeys.json and update command routing immediately.

Modal provides a dialog window with its own Scope for keyboard handling. Settings and confirmations commonly use modals.

Workspace Configuration Schemas

The workspace (packages/workspace) registers three main configuration groups.

Appearance

KeyTypeDefaultDescription
theme"light" | "dark" | "system""system"Color mode
accentColorstring""Custom accent hue in oklch
fontSizenumber16Base font size in pixels
fontFamilystring""Custom font family
lineWidthnumber700Max editor line width in pixels

Editor

KeyTypeDefaultDescription
lineNumbersbooleanfalseShow line numbers
indentSizenumber4Spaces per indent
vimModebooleanfalseEnable Vim keybindings
spellcheckbooleantrueBrowser spellcheck
foldHeadingsbooleantrueEnable heading folding
foldIndentbooleantrueEnable indent-based folding

The workspace also wires editor.behaviour.indentUsingTabs and editor.behaviour.indentVisualWidth into the shared CodeMirror setup: pressing Tab indents the current line or selection, Shift-Tab removes one indent level, and disabling tabs switches that indentation to spaces using the configured visual width.

editor.behaviour.indentVisualWidth defaults to 4 and drives both editor behavior and indentation layout. The shared editor uses it for EditorState.tabSize and space-based Tab insertion, while the workspace shell also projects the same value onto the root --indent-size CSS variable and the derived --list-indent value.

Files

KeyTypeDefaultDescription
files.links.newLinkFormat"shortest" | "relative" | "absolute""shortest"Default target-path style for generated internal links
files.links.useWikilinksbooleantruePrefer Obsidian-style wikilink syntax for generated internal links
files.links.omitMarkdownExtensionbooleantrueOmit .md and .markdown when formatting internal links where possible
files.links.useShortestUniqueSuffixbooleanfalseIn shortest mode, prefer the shortest unique path suffix instead of the full vault path

These settings are consumed by the shared API link formatter, so they affect completion-inserted note links, file-manager generated links, and rename-time link rewrites routed through FileManager.renameFile().

Workspace

KeyTypeDefaultDescription
workspace.mobile.mode"auto" | "always" | "never""auto"Choose whether the workspace shell follows the mobile breakpoint or forces a specific display mode
workspace.mobile.defaultPage"editor" | "tabs""editor"Choose which primary page the mobile shell opens first
workspace.mobile.showBottomNavbooleantrueShow the mobile shell’s bottom navigation bar
workspace.mobile.includeSidebarsInTabsbooleantrueInclude left and right sidebar leaves in the mobile tabs page
workspace.mobile.includeFloatingInTabsbooleantrueInclude floating and popout leaves in the mobile tabs page
workspace.mobile.breakpointPxnumber768Viewport width below which workspace.mobile.mode = auto uses mobile mode
workspace.editorAssociationsRecord<string, string>{}Glob-pattern to editor-view ID overrides for file-backed view routing
workspace.fileExplorer.autoRevealCurrentFilebooleanfalseAutomatically expand and scroll the file explorer to reveal the active file

The settings UI renders workspace.editorAssociations as a key/value table. Keys are VS Code-style glob patterns and values use a dropdown resolved through App.configurationOptionSources from the registered workspace.editorViews source. Unknown saved values are preserved and shown as custom options when allowUnknownOptions is true, so disabling or uninstalling a plugin does not destroy user associations.

The file explorer toolbar toggle and the Workspace settings toggle both read and write workspace.fileExplorer.autoRevealCurrentFile.

The mobile workspace settings are normal schema-backed workspace settings stored in app.json. Enum settings (workspace.mobile.mode, workspace.mobile.defaultPage) render through the standard select control, the booleans render as toggles, and workspace.mobile.breakpointPx uses the normal number-range control with the schema’s minimum and maximum bounds.

Declarative manifest configuration

Manifest-only and hybrid Lapis extensions can declare settings through lapis.contributes.configuration in manifest.json. The runtime path is:

  1. ContributionLapisConfigurationContribution is parsed from the manifest and installed by PluginManager.installManifestConfiguration.
  2. SchemaConfigurationSchema.register flattens nested object groups into namespaced leaf keys, validates writes with zod-backed SchemaType descriptors, and materializes defaults into app.json (or the plugin namespace).
  3. Control mappinggetSettingControlKind in packages/workspace/src/lib/components/configuration/configuration.ts maps each leaf schema to a SettingControlKind.
  4. RenderingcreatePropertySetting mounts Obsidian-compat Setting.add* helpers from packages/api/src/lib/settings.svelte.ts, which wrap @lapis-notes/ui primitives via mountComponent.

The programmatic AppSettings builder (addButton, addSearch, addMomentFormat, and similar) is not manifest-declarative; it remains the Obsidian-compat path for code-built setting tabs.

Supported schema shapes (declarative)

JSON schema shapeSettingControlKindSetting API@lapis-notes/ui
booleantoggleaddToggleSwitch
string (plain)textaddTextInput
string + enumselectaddDropdownbits-ui Select (api wrapper)
string + optionsSource (closed set)selectaddDropdownbits-ui Select
string + optionsSource + allowUnknownOptions: truecomboboxaddOptionsComboboxsearchable Input + suggestion list
string + editPresentation: "multilineText" or format: "textarea"textareaaddTextAreaTextarea
string + format: "color"coloraddColorPickerInput type="color"
string + format: "icon"iconaddIconPickerapi icon list (Popover + Button)
string + format: "date" / "time"dateaddDatePickerDateTimePickerDialog + trigger Button
string + format: "email" / "uri" / "ipv4"text (typed Input)addTextInput (email / url / text)
number or integer + minimum + maximumrangeaddSliderSlider
number or integer (no min/max)numberaddText (type="number")Input
array + enum itemsmultiselectaddDropdown (multiple)Select
array + string items + optionsSource (closed set)multiselectaddDropdown (multiple)Select
array + string items + optionsSource + allowUnknownOptions: truelistaddListSelect / Input rows with suggestions
array + primitive items (string / number / integer / boolean)listaddListInput / Switch / Button per row
array + flat object items (fixed primitive/enum/optionsSource columns)object-arrayaddObjectArrayTable + row grip reorder + add/remove + cell editors
flat primitive object value (leaf grid)object-gridaddObjectGridTable + cell editors
record object value with schema-valued additionalPropertiesobject-mapaddObjectMapTable + key editor and value control
nested object (manifest grouping)(section only)group headerslayout chrome
type: "custom"customSettingTab mountworkspace shell
anything elseunsupportedaddButton (“Edit in app.json”)ghost Button

CSS hooks

Schema-driven rows from createPropertySetting expose stable CSS classes derived from SettingControlKind via getSettingControlClassName:

  • Leaf controls: setting-control-{kind} on the root .setting-item (for example setting-control-toggle, setting-control-object-map).
  • Table-style leaf controls (object-grid, object-array): full-width column layout — title and description span the panel width with the table control underneath (not the two-column label/control row used by scalar settings).
  • Group section headers: setting-control-heading on the header row (declarative schema headings and imperative Setting.setHeading() sections).
  • Imperative section layout: setting-section, setting-section-heading, setting-section-heading-title, setting-section-heading-description, setting-section-heading-actions, setting-section-body.
  • Plugin list tabs (Core plugins / Community plugins): use Setting.setHeading() like other imperative plugin tabs; plugin rows render inside setting-section-body and span the panel via grid-column: 1 / -1. Heading actions such as reload buttons chain through Setting.setHeading().addButton(...). Heading descriptions use Setting.setDesc() before or after setHeading(), matching other setting rows.
  • Custom plugin tabs (type: "custom"): setting-control-custom on the external wrapper alongside setting-item-external.

These hooks are for workspace styling only; plugin-owned SettingTab surfaces built through AppSettings do not receive them automatically.

Dynamic option sources (optionsSource)

String schemas may declare optionsSource with an optional allowUnknownOptions flag. The settings UI resolves option lists through App.configurationOptionSources (ConfigurationOptionSourceRegistry in packages/api/src/lib/configuration-option-source-registry.ts):

  • Core sources — registered during app bootstrap (for example workspace.editorViews, wired to EditorViewRegistry invalidation).
  • Plugin sources — registered at runtime via Plugin.registerConfigurationOptionSource(id, provider). Relative IDs are prefixed with the plugin manifest id (ruleIdsmarkdown-lint.ruleIds). Registration is cleaned up on plugin unload.
  • Schema references — use fully qualified source ids in manifest JSON ("optionsSource": "markdown-lint.ruleIds").
  • optionsSourceParams — optional Record<string, unknown> on string schemas for parameterized providers (for example metadata.fieldValues with { "field": "status" }).
  • Searchable combobox — top-level string settings with allowUnknownOptions: true render as a combobox that passes query and limit to the resolver. The same combobox control is used for optionsSource columns in object-map and object-array table cells.
  • Session cache — providers may set cache: "session" to reuse resolved option lists (including queried results keyed by query and limit) until invalidation.

Dynamic sources are UI hints only; validation remains schema-local (static enum, etc.). When a source is unavailable, static enum options still render, current values are preserved, and the setting description includes a quiet hint such as Options source "plugin-id.source" is not currently available.. Mounted object-map, object-array, select, multiselect, combobox, and option-backed list controls refresh when a source invalidates.

Restore-default affordance: ghost Button + rotate-ccw on each supported row (existing behavior).

Metadata: implemented vs planned

FieldSchemaSettings UI
title, description, markdownDescriptionyesyes
defaultyesyes (+ restore default)
enumDescriptions, enumMarkdownDescriptionsyesdropdown labels
enumItemLabelsyesdropdown labels (short label; value unchanged)
orderyessort within category / group
minimum, maximum, stepyesslider / validation
minItems, maxItemsyesvalidation + object-array add/remove disable
deprecationMessage, markdownDeprecationMessageyeshide unless non-default; warning chrome when visible
format (color, icon, textarea, date, time, email, uri, ipv4)yesmapped controls above
editPresentationpartial (multilineText)textarea
optionsSource, allowUnknownOptions, optionsSourceParamsyesdynamic option lists for string, object-map, object-array, select, multiselect, and option-backed lists
pattern, minLength, maxLengthplanned
VS Code scope, tags, keywordsN/AN/A

VS Code comparison

See VS Code configuration contribution points. Summary:

CapabilityLapis declarativeVS Code
Boolean, string, enum, multilineyesyes
enumItemLabelsyesyes
number / integeryesyes
Slider (min + max)yesyes
format: color / iconyes (Lapis extension)not in public VS Code schema
Primitive arrays (editable list)yesyes
Flat primitive object gridyesyes
Array of flat object rows (table editor)yespartial / extension-specific
Dynamic record-object mapyesyes
Complex arrays/objects (nested cells, free-form rows)JSON fallbackJSON fallback
order in UIyesyes
Deprecation UXyesyes
Standard format strings (date, email, uri, …)yesyes
JSON Schema pattern / lengthplanned (TASK-PLUGIN-053)documented

Gaps and Task IDs

Prior open work is tracked by task id:

task_idScope
TASK-PLUGIN-047Spec matrix + VS Code gap documentation (this section)
TASK-PLUGIN-048order, enumItemLabels, integer + e2e-vault Phase A
TASK-PLUGIN-049Primitive array list control + e2e-vault Phase B
TASK-PLUGIN-050Leaf object-grid + e2e-vault Phase C
TASK-PLUGIN-051String format + deprecation UX + e2e-vault Phase D
TASK-PLUGIN-052e2e-vault INDEX.md + fixture checklist alignment
TASK-PLUGIN-066Array-of-flat-object object-array control + e2e Phase E
TASK-PLUGIN-067Object-array row reorder via shared @lapis-notes/ui/table-dnd
TASK-PLUGIN-073Extensible optionsSource registry for declarative settings UI
TASK-PLUGIN-074Top-level/optionsSourceParams follow-up for declarative settings UI
TASK-PLUGIN-075Searchable combobox and metadata.fieldValues option source
TASK-PLUGIN-076optionsSource combobox in table cells, query cache, developer docs

Unsupported fallback policy

Arrays whose items are objects with nested object/array columns, free-form additionalProperties, specialized string formats inside cells (color, date, textarea, and similar), or other shapes that do not map to a supported SettingControlKind render only an Edit in app.json ghost button that opens the vault configuration file in a centered in-app floating editor pane. The UI does not render partial or misleading controls for those keys.

Supported object-array rows require fixed items.properties where every column passes isTableCellSchema (string plain/enum/optionsSource, boolean, number, integer). Recommend additionalProperties: false in plugin schemas.

UI implementation policy

Declarative controls must not import shadcn-svelte directly from packages/workspace. Use Setting.add* in packages/api/src/lib/settings.svelte.ts, which mounts Svelte trees from @lapis-notes/ui. New shared editors belong under packages/api/src/lib/components/configuration/ unless promoted into packages/ui for reuse outside settings.

E2E vault contract (plugin-test)

The tracked vault e2e-vault/ includes manifest-only community plugin plugin-test (.obsidian/plugins/plugin-test/manifest.json). It is the manual regression surface for declarative settings widgets (not only commands or status bar).

PhaseManifest keys (examples)UI under test
Adisplay.mode (enumItemLabels, order), behavior.retryCount (integer)dropdown labels, sort order, integer/slider
Bdisplay.tags, behavior.flagsprimitive string/boolean lists
Cbehavior.limits (flat object)object grid
Ddisplay.contactEmail, display.docsUrl, display.scheduledAt, display.legacyModetyped formats; deprecation
Ebehavior.rules (object array), behavior.unsupportedComplexRowsobject-array table; JSON fallback

Persisted defaults live in e2e-vault/.obsidian/app.json under flattened plugin-test.* keys. The fixture note e2e-vault/plugin-test/Declarative Plugin Fixture.md lists controls and manual verification steps. Run pnpm e2e-vault:prepare before manual QA; do not edit generated e2e-vault-temp.

Reactivity

Configuration values are Svelte 5 $state under the hood. Components that read configuration via getConfiguration() automatically re-render when values change. The updated event provides an imperative hook for non-Svelte consumers.

Nested object-group metadata is preserved when schemas are flattened into persisted keys, so the settings UI can keep meaningful subsection headings and descriptions even though persisted configuration remains key-value based.