Persisting settings ​
Remember folder states, dock sizes and tool options across reloads, through one StorageAdapter port that tests replace with memory.
1. Let elements remember their state ​
<jolly-dock-layout storage-key="voxel-editor">
<jolly-dock key="right" side="right">
<jolly-pane key="inspector" heading="Inspector">
<jolly-folder key="shadows" label="Shadows" storage-key="voxel-editor:shadows">
</jolly-folder>
</jolly-pane>
</jolly-dock>
</jolly-dock-layout>- Elements with a
storage-keystore under<storage-key>:<name>. - The dock layout stores the arrangement, sizes and floating windows. A folder stores whether it is open. A pane stores its folder order.
- Elements without a
storageproperty share oneLocalStorageAdapterfor the page. - Prefix every key with the editor name, so two editors on one origin do not read each other's state.
2. Store application settings ​
Give a settings store the same port. Default it to memory, and inject localStorage at boot:
import { MemoryStorageAdapter, type StorageAdapter } from "@jolly-pixel/ui";
const kStorageKey = "terrain:sculpt";
export class SculptSettings {
#storage: StorageAdapter;
#options: Readonly<SculptOptions>;
constructor(
storage: StorageAdapter = new MemoryStorageAdapter()
) {
this.#storage = storage;
this.#options = parseOptions(storage.get(kStorageKey));
}
get options(): Readonly<SculptOptions> {
return this.#options;
}
update(
changes: Partial<SculptOptions>
): void {
this.#options = parseOptions(JSON.stringify({ ...this.#options, ...changes }));
this.#storage.set(kStorageKey, JSON.stringify(this.#options));
}
}import { LocalStorageAdapter } from "@jolly-pixel/ui";
const settings = new SculptSettings(new LocalStorageAdapter());parseOptionsturns whatever is stored, ornull, into valid options: defaults for missing keys, numbers clamped, unknown choices dropped. A value written by an older version then never breaks the editor.- Emit a change event from
update, so panels can subscribe to the store. See Keeping an element in sync. - Fields bound to the store read through it with
FieldBinding, and write only on commit. See Preview and commit.
3. Scope storage to a project ​
Implement the port to put every key under a prefix:
class ProjectStorage implements StorageAdapter {
#inner: StorageAdapter;
#project: string;
constructor(
inner: StorageAdapter,
project: string
) {
this.#inner = inner;
this.#project = project;
}
get(
key: string
): string | null {
return this.#inner.get(`${this.#project}:${key}`);
}
set(
key: string,
value: string
): void {
this.#inner.set(`${this.#project}:${key}`, value);
}
}
const storage = new ProjectStorage(new LocalStorageAdapter(), projectId);
html`
<jolly-dock-layout storage-key="voxel-editor" .storage=${storage}>
<jolly-dock key="right" side="right">
<jolly-pane key="inspector" heading="Inspector" .storage=${storage}>
<jolly-folder key="shadows" label="Shadows" .storage=${storage}></jolly-folder>
</jolly-pane>
</jolly-dock>
</jolly-dock-layout>
`;Each element reads its own storage, so pass the adapter to panes and folders as well. Inside a dock layout, the layout stores dock sizes itself.
storage is a property, with no attribute. Set it before the element first renders, as a Lit .storage=${storage} binding does: the dock layout reads the stored arrangement once, on its first update.
4. Test without a browser ​
const storage = new MemoryStorageAdapter();
storage.set("terrain:sculpt", JSON.stringify({ size: 999 }));
const settings = new SculptSettings(storage);
assert.equal(settings.options.size, BRUSH_SIZE.max);MemoryStorageAdapter keeps values for the lifetime of the instance, so each test starts from its own state.
5. When storage fails ​
LocalStorageAdapterfalls back to memory after the first failed access, such as a full quota or blocked site data, and stays there.persistenttells whether writes still reachlocalStorage. Show it when losing settings at reload would surprise the user.new LocalStorageAdapter({ resolve: () => sessionStorage })keeps state for the tab only.
6. Ask once, then remember ​
resolveStoredPrompt returns a stored answer, or prompts for it once:
import { resolveStoredPrompt } from "@jolly-pixel/ui/elements/dialog";
const author = await resolveStoredPrompt({
title: "Your name",
label: "Name shown to collaborators",
storageKey: "editor:author",
fallbackValue: "Anonymous"
});An empty answer stores fallbackValue, so the prompt does not come back on every load.