# State Two small state helpers. Neither is required — they remove boilerplate, they don't impose an architecture. --- ## createEditorStore A reactive source of truth for your editor's settings: a Solid `createStore` under the hood, with optional debounced `localStorage` persistence and a typed `bind` helper that produces `{ value, onChange }` pairs for fields. ```tsx interface Settings { name: string; transform: { x: number; y: number }; snap: boolean; } const [state, setState, bind] = createEditorStore({ defaults: { name: 'Untitled', transform: { x: 0, y: 0 }, snap: true }, persist: { key: 'my-editor', version: 1 }, }); ``` It returns a tuple `[state, setState, bind]`: - **`state`** — a Solid store. Read reactively: `state.name`, `state.transform.x`. - **`setState`** — the store setter (Solid's `SetStoreFunction`): `setState('name', 'New')`, `setState('transform', 'x', 10)`. - **`bind`** — a typed path binder (1–4 levels deep) returning a `FieldBinding`: ```tsx bind('name') // → { value: () => state.name, onChange: (v) => setState('name', v) } bind('transform', 'x') // nested path, fully typed ``` Spread a binding straight into any field — it fills `value` + `onChange`: ```tsx ``` ### Options | Option | Type | Notes | |---|---|---| | `defaults` | `T` | Initial shape. You own `T`. | | `persist?` | `{ key, version, debounceMs? }` | Opt into localStorage. | `persist`: - **`key`** — localStorage key. - **`version`** — bump it to **discard** a stored blob whose shape changed (no migration — discard-on-mismatch). - **`debounceMs`** — write debounce (default 150ms). On load, a stored blob is **deep-merged over `defaults`**, so adding a new field to `defaults` later fills in for old stored blobs without a version bump (the merge supplies the missing key). Bump `version` only when an *existing* field's shape changes incompatibly. ### Notifying your world There's no subscribe API — it's Solid. React to changes with an effect: ```tsx createEffect(() => engine.setSnap(state.snap)); createEffect(() => engine.setPosition(state.transform.x, state.transform.y)); ``` This is the whole point: one fact lives in the store, the field reads/writes it via `bind`, and your world reads it via `createEffect` — no hand fan-out. > **It is a store + helper, not a schema or registry.** Fields stay > hand-composed; you own `T`'s shape. `bind` only removes wiring boilerplate. --- ## createPersistedToggle A `createSignal`-shaped `[value, setValue]` whose writes mirror to localStorage. For **chrome/layout flags the library doesn't render itself** — chiefly split-view stage visibility, where your app owns the mount decision but the *state* belongs with the other `stage-*` flags (not the versioned editor-content store). ```tsx const [secondaryShown, setSecondaryShown] = createPersistedToggle('split:secondary', false); ``` The returned pair can also feed a controlled `collapsed`/`onToggle` slot directly: ```tsx !secondaryShown()} onRightToggle={(next) => setSecondaryShown(!next)} ... /> ``` It shares the same localStorage kernel as `Section`/panel collapse, and it never traps the user — a read/parse failure is swallowed and falls back to the default. --- ## Why the split A **panel** collapse is library-owned (the panel stays mounted, shrinks to a strip), so the library persists it for you via `persistKey`. A **stage**'s visibility is app-owned structure (a canvas tears down its GL context on unmount, so only your app can own that mount decision) — but the *flag* still belongs with the other layout state, which is what `createPersistedToggle` is for. Editor *content* (the settings the user edits) goes in `createEditorStore` (it's versioned). Chrome/layout flags go in `createPersistedToggle` or the components' own `persistKey`.