Settings and app lifetime
One settings object per app
app.use(createEditor()) (createEditor from @skaile/forge-editor/vue; the Nuxt module does it in a client plugin) provides the context; it wraps the framework-free createEditorContext() from /core, whose settings is a store (get, set, update, subscribe). The settings are one versioned JSON object (EditorSettings, version 2) that every editor, panel and tree of the app shares. A simple editor and a preset without a feature never prune the settings of features it does not install; unknown-to-this-bundle values stay in the stored JSON. createEditor() defaults the app's notifier to a queued Vue notifier; pass notify to replace it. The object has the sections editing, ai, blocks and appearance. Read and edit it with useEditorSettings() (/vue or /react); in features use the ctx.settings store. Validate with parseEditorSettings (drops unknown keys, replaces invalid values) or editorSettingsSchema.
React
<EditorContextProvider context={createEditorContext({ notify: reactNotifier })}> (EditorContextProvider, reactNotifier and Toaster from @skaile/forge-editor/react, createEditorContext from /core) provides the same context to a React tree. The host creates it and owns it: the provider never disposes it, call context.dispose() when it outlives the app. useEditorSettings() returns { settings, update, set, ready, synced }. Without notify the context drops app-level notices (silentNotifier). settings is the current EditorSettings, update(draft => { … }) mutates a copy and stores it, and set(value) replaces it. useStore(store) subscribes a component to any ReadonlyStore of the core.
Local storage and account sync
The settings are read from localStorage (tiptap-editor:settings) at install time and written back on every change; unreadable or blocked storage falls back to the defaults for the session. A record saved by an earlier version is migrated when it is read (migrateEditorSettings, applied by parseEditorSettings) and written back once: version 2 renamed keys (see the migration guide). The server migrates a stored older record on GET /api/editor/settings and stores it again, and PUT accepts a body of an older version (a tab that was open during the update).
A backend that offers settings (the default backend and createHttpBackend() do; createMemoryBackend() keeps settings in memory only) syncs per account. On the first useEditorSettings() the client calls backend.settings.load() (the HTTP backend: GET /api/editor/settings; only once an editor-side component needs the settings, not at install time): the stored settings replace the local values (synced), null (204: the adapter keeps none), a refusal (401, 501) and network errors keep the local ones; an account without stored settings uploads this browser's. Afterwards every change is sent with backend.settings.save() (debounced, 500 ms). The status codes are listed in Server routes and authentication. A plain-Vue or React app gets this from the default backend only when its server implements the same two endpoints (examples/react-advanced reaches the Nuxt host's through its proxy), otherwise it passes createMemoryBackend() and stays local.
View state of the tree
The tree's filters and expanded keys are the framework-free TreeViewModel of the app's context (treeView(context)); useTreeView() of /tree/vue and /tree/react returns them. Filters persist (tip-tree:node-filters), expansion lasts for the session. The model belongs to the app context (cached per context, nothing is global to the process), and a model that is dropped takes its subscription with it.
Settings UI
Tabs come from the features of a preset (composites editing, ai, authors, comments, themes appearance); the simple editor has none. EditorSettingsPanel (props editor, contentClass?, v-model:tab) places the tabs anywhere; EditorSettingsModal (props editor, v-model:open, v-model:tab) is the same panel in a fixed-width (42rem) popup with a blurred backdrop. Both render nothing when the editor's preset has no tab.
The settingsUi prop of EditorSurface and AdvancedEditor takes two values: 'internal' (default) mounts the modal inside the surface and the toolbar's settings entry opens it. 'external' mounts nothing and emits settingsOpen(tab?); the host then shows its own EditorSettingsPanel or EditorSettingsModal for the same editor:
<script setup lang="ts">
import { EditorSettingsModal } from '@skaile/forge-editor/vue'
import { AdvancedEditor } from '@skaile/forge-editor/vue/advanced'
import type { Editor } from '@tiptap/vue-3'
import { ref, shallowRef } from 'vue'
const editor = shallowRef<Editor>()
const open = ref(false)
const tab = ref<string>()
</script>
<template>
<AdvancedEditor settings-ui="external" @ready="editor = $event" @settings-open="(t) => { tab = t; open = true }" />
<EditorSettingsModal v-model:open="open" v-model:tab="tab" :editor="editor" />
</template>
Themes
themesFeature() (part of advancedEditor() on both surfaces): the setting appearance.theme (null = Default appearance, which applies no Theme) names an Obsidian theme served by GET /api/editor/themes/:id; users pick it in the settings tab „Theme“. The feature's setup hook starts one runtime per app as soon as the first surface mounts (reference-counted, released by the teardown: on Vue it lives until the Vue app unmounts, on React until the last surface that started it unmounts), injects the scoped CSS as <style data-tiptap-theme> and sets obsidian-theme theme-dark|theme-light on <html> (following html.dark/html.light, else the system preference). styles/themes.css bridges Obsidian variables onto every --ui-* token (React: react/styles/themes-bridge.css onto the shadcn tokens), so the whole site is themed, not only the editor. Needs an adapter with listThemes/loadTheme. Builtin themes in this repository (server/themes/<id>/): nord, things, minimal, catppuccin, border, blue-topaz, tokyo-night. Details, mapping table and limits: Themes.