@weasel-js/prefs 1.9.3 → 1.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/react.d.ts CHANGED
@@ -1,4 +1,5 @@
1
- import { a as PrefGroup, w as PrefPath, c as PrefsStore, z as PrefValueAt } from './store-BU2tRX5H.js';
1
+ import { a as PrefGroup, L as PrefPath, Y as PrefsStore, U as PrefValueAt, X as PrefsOptions } from './open-BEY-aOGe.js';
2
+ import { SyncStorageAdapter } from '@weasel-js/storage';
2
3
  import '@weasel-js/quantity';
3
4
 
4
5
  /** One leaf of `store`, as React state. */
@@ -17,4 +18,27 @@ declare function usePrefsValues<S extends PrefGroup>(store: PrefsStore<S>): {
17
18
  reset: (path?: string) => void;
18
19
  };
19
20
 
20
- export { usePref, usePrefsValues };
21
+ type SyncOptions = PrefsOptions & {
22
+ storage: SyncStorageAdapter;
23
+ };
24
+ /**
25
+ * A store for `schema`, open while the component is mounted: opened during
26
+ * the first render, so that render has values, and closed, with its queued
27
+ * writes sent, when the component unmounts.
28
+ *
29
+ * Components naming the same `storage`, `prefix`, and `schema` share one
30
+ * store, which closes when the last of them unmounts. Components naming the
31
+ * same `storage` and `prefix` with different schemas each get a store of
32
+ * their own over the same records, so a write through one is read by the
33
+ * others at once, and each hears it as a `local` change. They must pass the
34
+ * same `migrations`: the records are migrated once, when the first opens.
35
+ *
36
+ * `storage` and `schema` are compared by identity and `prefix` by value, so
37
+ * keep the first two out of the render body. A change to any of the three
38
+ * closes the store and returns the one for the new three. The `options`
39
+ * object itself may be new every render: `migrations` and `validators` are
40
+ * read when a store opens, and a change to either alone opens nothing.
41
+ */
42
+ declare function usePrefsStore<S extends PrefGroup>(schema: S, options: SyncOptions): PrefsStore<S>;
43
+
44
+ export { usePref, usePrefsStore, usePrefsValues };
package/dist/react.js CHANGED
@@ -1,6 +1,6 @@
1
+ import { openPrefsCacheSync, createPrefsStore } from './chunk-5KB3WBJZ.js';
1
2
  import { useCallback, useSyncExternalStore } from 'react';
2
3
 
3
- // src/hooks.ts
4
4
  function usePref(store, path) {
5
5
  const subscribe = useCallback((onChange) => store.subscribe(onChange), [store]);
6
6
  const getSnapshot = () => store.get(path);
@@ -28,7 +28,71 @@ function usePrefsValues(store) {
28
28
  const reset = useCallback((path) => store.reset(path), [store]);
29
29
  return { values, set, unset, stored, reset };
30
30
  }
31
+ var shared = /* @__PURE__ */ new WeakMap();
32
+ function sharedAt(storage, prefix) {
33
+ let byPrefix = shared.get(storage);
34
+ if (!byPrefix) shared.set(storage, byPrefix = /* @__PURE__ */ new Map());
35
+ let entry = byPrefix.get(prefix);
36
+ if (!entry) byPrefix.set(prefix, entry = { open: null, bySchema: /* @__PURE__ */ new Map() });
37
+ return entry;
38
+ }
39
+ function heldAt(at, schema) {
40
+ let entry = at.bySchema.get(schema);
41
+ if (!entry) at.bySchema.set(schema, entry = { store: null, holders: 0 });
42
+ return entry;
43
+ }
44
+ var lent = (cache) => Object.create(cache, {
45
+ close: {
46
+ value: async () => {
47
+ await cache.flush();
48
+ }
49
+ }
50
+ });
51
+ function closeUnheldSoon(storage, prefix, schema, entry) {
52
+ queueMicrotask(() => {
53
+ if (entry.holders > 0) return;
54
+ void entry.store?.close();
55
+ entry.store = null;
56
+ const byPrefix = shared.get(storage);
57
+ const at = byPrefix?.get(prefix);
58
+ if (!at || !byPrefix) return;
59
+ if (at.bySchema.get(schema) === entry) at.bySchema.delete(schema);
60
+ if (at.bySchema.size > 0) return;
61
+ at.open?.stop();
62
+ void at.open?.cache.close();
63
+ byPrefix.delete(prefix);
64
+ });
65
+ }
66
+ function acquire(schema, options) {
67
+ const { storage, prefix } = options;
68
+ const at = sharedAt(storage, prefix);
69
+ const entry = heldAt(at, schema);
70
+ if (!entry.store) {
71
+ at.open ??= openPrefsCacheSync(options);
72
+ entry.store = createPrefsStore(
73
+ schema,
74
+ lent(at.open.cache),
75
+ options.validators,
76
+ void 0,
77
+ options.migrations?.length ?? 0
78
+ );
79
+ if (entry.holders === 0) closeUnheldSoon(storage, prefix, schema, entry);
80
+ }
81
+ return entry.store;
82
+ }
83
+ function usePrefsStore(schema, options) {
84
+ const { storage, prefix } = options;
85
+ const hold = useCallback(() => {
86
+ const entry = heldAt(sharedAt(storage, prefix), schema);
87
+ entry.holders++;
88
+ return () => {
89
+ if (--entry.holders === 0) closeUnheldSoon(storage, prefix, schema, entry);
90
+ };
91
+ }, [storage, prefix, schema]);
92
+ const current = () => acquire(schema, options);
93
+ return useSyncExternalStore(hold, current, current);
94
+ }
31
95
 
32
- export { usePref, usePrefsValues };
96
+ export { usePref, usePrefsStore, usePrefsValues };
33
97
  //# sourceMappingURL=react.js.map
34
98
  //# sourceMappingURL=react.js.map
package/dist/react.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/hooks.ts"],"names":[],"mappings":";;;AAMO,SAAS,OAAA,CACd,OACA,IAAA,EAIA;AACA,EAAA,MAAM,SAAA,GAAY,WAAA,CAAY,CAAC,QAAA,KAAyB,KAAA,CAAM,UAAU,QAAQ,CAAA,EAAG,CAAC,KAAK,CAAC,CAAA;AAC1F,EAAA,MAAM,WAAA,GAAc,MAAM,KAAA,CAAM,GAAA,CAAI,IAAI,CAAA;AACxC,EAAA,MAAM,KAAA,GAAQ,oBAAA,CAAqB,SAAA,EAAW,WAAA,EAAa,WAAW,CAAA;AACtE,EAAA,MAAM,GAAA,GAAM,WAAA;AAAA,IACV,CAAC,IAAA,KAA+E;AAC9E,MAAA,KAAA,CAAM,GAAA;AAAA,QACJ,IAAA;AAAA,QACA,OAAO,SAAS,UAAA,GACX,IAAA,CAAwD,MAAM,GAAA,CAAI,IAAI,CAAC,CAAA,GACxE;AAAA,OACN;AAAA,IACF,CAAA;AAAA,IACA,CAAC,OAAO,IAAI;AAAA,GACd;AACA,EAAA,OAAO,CAAC,OAAO,GAAG,CAAA;AACpB;AAKO,SAAS,eAAoC,KAAA,EAMlD;AACA,EAAA,MAAM,SAAA,GAAY,WAAA,CAAY,CAAC,QAAA,KAAyB,KAAA,CAAM,UAAU,QAAQ,CAAA,EAAG,CAAC,KAAK,CAAC,CAAA;AAC1F,EAAA,MAAM,SAAS,oBAAA,CAAqB,SAAA,EAAW,KAAA,CAAM,MAAA,EAAQ,MAAM,MAAM,CAAA;AACzE,EAAA,MAAM,QAAQ,oBAAA,CAAqB,SAAA,EAAW,KAAA,CAAM,KAAA,EAAO,MAAM,KAAK,CAAA;AACtE,EAAA,MAAM,SAAS,oBAAA,CAAqB,SAAA,EAAW,KAAA,CAAM,MAAA,EAAQ,MAAM,MAAM,CAAA;AACzE,EAAA,MAAM,GAAA,GAAM,WAAA;AAAA,IACV,CAAC,IAAA,EAAc,KAAA,KAAmB,KAAA,CAAM,GAAA,CAAI,MAAqB,KAAc,CAAA;AAAA,IAC/E,CAAC,KAAK;AAAA,GACR;AACA,EAAA,MAAM,KAAA,GAAQ,WAAA,CAAY,CAAC,IAAA,KAAkB,KAAA,CAAM,MAAM,IAAI,CAAA,EAAG,CAAC,KAAK,CAAC,CAAA;AACvE,EAAA,OAAO,EAAE,MAAA,EAAQ,GAAA,EAAK,KAAA,EAAO,QAAQ,KAAA,EAAM;AAC7C","file":"react.js","sourcesContent":["import { useCallback, useSyncExternalStore } from 'react';\nimport type { PrefPath, PrefValueAt } from './paths';\nimport type { PrefGroup } from './schema';\nimport type { PrefsStore } from './store';\n\n/** One leaf of `store`, as React state. */\nexport function usePref<S extends PrefGroup, P extends PrefPath<S>>(\n store: PrefsStore<S>,\n path: P,\n): [\n PrefValueAt<S, P>,\n (next: PrefValueAt<S, P> | ((prev: PrefValueAt<S, P>) => PrefValueAt<S, P>)) => void,\n] {\n const subscribe = useCallback((onChange: () => void) => store.subscribe(onChange), [store]);\n const getSnapshot = () => store.get(path);\n const value = useSyncExternalStore(subscribe, getSnapshot, getSnapshot);\n const set = useCallback(\n (next: PrefValueAt<S, P> | ((prev: PrefValueAt<S, P>) => PrefValueAt<S, P>)) => {\n store.set(\n path,\n typeof next === 'function'\n ? (next as (prev: PrefValueAt<S, P>) => PrefValueAt<S, P>)(store.get(path))\n : next,\n );\n },\n [store, path],\n );\n return [value, set];\n}\n\n/** The whole of `store` as React state, shaped for `PrefsForm`: `values`,\n * `onChange={set}`, `auto={unset}`, and `reset` for `onAutoChange`. `stored`\n * is `store.stored()`. */\nexport function usePrefsValues<S extends PrefGroup>(store: PrefsStore<S>): {\n values: Record<string, unknown>;\n set: (path: string, value: unknown) => void;\n unset: ReadonlySet<string>;\n stored: Record<string, unknown>;\n reset: (path?: string) => void;\n} {\n const subscribe = useCallback((onChange: () => void) => store.subscribe(onChange), [store]);\n const values = useSyncExternalStore(subscribe, store.values, store.values);\n const unset = useSyncExternalStore(subscribe, store.unset, store.unset);\n const stored = useSyncExternalStore(subscribe, store.stored, store.stored);\n const set = useCallback(\n (path: string, value: unknown) => store.set(path as PrefPath<S>, value as never),\n [store],\n );\n const reset = useCallback((path?: string) => store.reset(path), [store]);\n return { values, set, unset, stored, reset };\n}\n"]}
1
+ {"version":3,"sources":["../src/hooks.ts","../src/usePrefsStore.ts"],"names":["useCallback","useSyncExternalStore"],"mappings":"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","file":"react.js","sourcesContent":["import { useCallback, useSyncExternalStore } from 'react';\nimport type { PrefPath, PrefValueAt } from './paths';\nimport type { PrefGroup } from './groups';\nimport type { PrefsStore } from './store';\n\n/** One leaf of `store`, as React state. */\nexport function usePref<S extends PrefGroup, P extends PrefPath<S>>(\n store: PrefsStore<S>,\n path: P,\n): [\n PrefValueAt<S, P>,\n (next: PrefValueAt<S, P> | ((prev: PrefValueAt<S, P>) => PrefValueAt<S, P>)) => void,\n] {\n const subscribe = useCallback((onChange: () => void) => store.subscribe(onChange), [store]);\n const getSnapshot = () => store.get(path);\n const value = useSyncExternalStore(subscribe, getSnapshot, getSnapshot);\n const set = useCallback(\n (next: PrefValueAt<S, P> | ((prev: PrefValueAt<S, P>) => PrefValueAt<S, P>)) => {\n store.set(\n path,\n typeof next === 'function'\n ? (next as (prev: PrefValueAt<S, P>) => PrefValueAt<S, P>)(store.get(path))\n : next,\n );\n },\n [store, path],\n );\n return [value, set];\n}\n\n/** The whole of `store` as React state, shaped for `PrefsForm`: `values`,\n * `onChange={set}`, `auto={unset}`, and `reset` for `onAutoChange`. `stored`\n * is `store.stored()`. */\nexport function usePrefsValues<S extends PrefGroup>(store: PrefsStore<S>): {\n values: Record<string, unknown>;\n set: (path: string, value: unknown) => void;\n unset: ReadonlySet<string>;\n stored: Record<string, unknown>;\n reset: (path?: string) => void;\n} {\n const subscribe = useCallback((onChange: () => void) => store.subscribe(onChange), [store]);\n const values = useSyncExternalStore(subscribe, store.values, store.values);\n const unset = useSyncExternalStore(subscribe, store.unset, store.unset);\n const stored = useSyncExternalStore(subscribe, store.stored, store.stored);\n const set = useCallback(\n (path: string, value: unknown) => store.set(path as PrefPath<S>, value as never),\n [store],\n );\n const reset = useCallback((path?: string) => store.reset(path), [store]);\n return { values, set, unset, stored, reset };\n}\n","import type { OwnedRecordCache, StorageAdapter, SyncStorageAdapter } from '@weasel-js/storage';\nimport { useCallback, useSyncExternalStore } from 'react';\nimport type { PrefGroup } from './groups';\nimport { openPrefsCacheSync, type PrefsOptions } from './open';\nimport { createPrefsStore, type PrefsStore } from './store';\n\n/** The store open for one schema, and how many mounted components hold it. */\ninterface Held {\n store: PrefsStore<PrefGroup> | null;\n holders: number;\n}\n\n/** The one cache open on a prefix, and a store over it for each schema in use. */\ninterface Shared {\n open: { cache: OwnedRecordCache; stop: () => void } | null;\n bySchema: Map<PrefGroup, Held>;\n}\n\ntype SyncOptions = PrefsOptions & { storage: SyncStorageAdapter };\n\nconst shared = new WeakMap<StorageAdapter, Map<string, Shared>>();\n\nfunction sharedAt(storage: StorageAdapter, prefix: string): Shared {\n let byPrefix = shared.get(storage);\n if (!byPrefix) shared.set(storage, (byPrefix = new Map()));\n let entry = byPrefix.get(prefix);\n if (!entry) byPrefix.set(prefix, (entry = { open: null, bySchema: new Map() }));\n return entry;\n}\n\nfunction heldAt(at: Shared, schema: PrefGroup): Held {\n let entry = at.bySchema.get(schema);\n if (!entry) at.bySchema.set(schema, (entry = { store: null, holders: 0 }));\n return entry;\n}\n\n/** `cache` for one store among several: closing it sends queued writes and\n * leaves the cache open for the others. */\nconst lent = (cache: OwnedRecordCache): OwnedRecordCache =>\n Object.create(cache, {\n close: {\n value: async (): Promise<void> => {\n await cache.flush();\n },\n },\n }) as OwnedRecordCache;\n\n/** Close a store nothing holds, and the cache once no store is over it, a\n * microtask on: after the remount StrictMode and fast refresh make in the\n * same tick, which holds it again. */\nfunction closeUnheldSoon(storage: StorageAdapter, prefix: string, schema: PrefGroup, entry: Held): void {\n queueMicrotask(() => {\n if (entry.holders > 0) return;\n void entry.store?.close();\n entry.store = null;\n const byPrefix = shared.get(storage);\n const at = byPrefix?.get(prefix);\n if (!at || !byPrefix) return;\n if (at.bySchema.get(schema) === entry) at.bySchema.delete(schema);\n if (at.bySchema.size > 0) return;\n at.open?.stop();\n // `close` sends queued writes before its first await, and a sync adapter's\n // write lands in that call, so a cache opened next reads them.\n void at.open?.cache.close();\n byPrefix.delete(prefix);\n });\n}\n\nfunction acquire<S extends PrefGroup>(schema: S, options: SyncOptions): PrefsStore<S> {\n const { storage, prefix } = options;\n const at = sharedAt(storage, prefix);\n const entry = heldAt(at, schema);\n if (!entry.store) {\n at.open ??= openPrefsCacheSync(options);\n entry.store = createPrefsStore(\n schema,\n lent(at.open.cache),\n options.validators,\n undefined,\n options.migrations?.length ?? 0,\n ) as unknown as PrefsStore<PrefGroup>;\n // Opened by a render that may never mount.\n if (entry.holders === 0) closeUnheldSoon(storage, prefix, schema, entry);\n }\n return entry.store as unknown as PrefsStore<S>;\n}\n\n/**\n * A store for `schema`, open while the component is mounted: opened during\n * the first render, so that render has values, and closed, with its queued\n * writes sent, when the component unmounts.\n *\n * Components naming the same `storage`, `prefix`, and `schema` share one\n * store, which closes when the last of them unmounts. Components naming the\n * same `storage` and `prefix` with different schemas each get a store of\n * their own over the same records, so a write through one is read by the\n * others at once, and each hears it as a `local` change. They must pass the\n * same `migrations`: the records are migrated once, when the first opens.\n *\n * `storage` and `schema` are compared by identity and `prefix` by value, so\n * keep the first two out of the render body. A change to any of the three\n * closes the store and returns the one for the new three. The `options`\n * object itself may be new every render: `migrations` and `validators` are\n * read when a store opens, and a change to either alone opens nothing.\n */\nexport function usePrefsStore<S extends PrefGroup>(schema: S, options: SyncOptions): PrefsStore<S> {\n const { storage, prefix } = options;\n const hold = useCallback(() => {\n const entry = heldAt(sharedAt(storage, prefix), schema);\n entry.holders++;\n return () => {\n if (--entry.holders === 0) closeUnheldSoon(storage, prefix, schema, entry);\n };\n }, [storage, prefix, schema]);\n // Read again once held, so a store closed between render and mount is\n // replaced before anything uses it.\n const current = () => acquire(schema, options);\n return useSyncExternalStore(hold, current, current);\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@weasel-js/prefs",
3
- "version": "1.9.3",
3
+ "version": "1.10.0",
4
4
  "description": "A preferences schema, a store that loads, validates, migrates and persists values against it, and React hooks over the store.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -41,8 +41,9 @@
41
41
  "build": "tsup"
42
42
  },
43
43
  "dependencies": {
44
- "@weasel-js/quantity": "1.9.3",
45
- "@weasel-js/storage": "1.9.3"
44
+ "@weasel-js/quantity": "1.10.0",
45
+ "@weasel-js/registry": "1.10.0",
46
+ "@weasel-js/storage": "1.10.0"
46
47
  },
47
48
  "peerDependencies": {
48
49
  "react": ">=18"
@@ -1,353 +0,0 @@
1
- import { Display, UnitEntry, InfinityText, UnitSystem, Unit } from '@weasel-js/quantity';
2
-
3
- /** The value types a built-in pref leaf can hold. */
4
- type PrefKind = 'number' | 'boolean' | 'string' | 'enum' | 'color' | 'paint' | 'object' | 'field';
5
- /**
6
- * Leaves that share one row in a compact property UI (weasel-ui
7
- * `SelectionPanel`, labkit's `ControlPanel`), declared on the first of them.
8
- * Purely presentational.
9
- */
10
- interface PrefPair {
11
- /** The other leaves on the row, by the full path their values are read and
12
- * written at (`'pose.y'`, `'camera.type'`), in order. Each must follow the
13
- * one before it among its siblings. */
14
- with: string | readonly string[];
15
- /** What the row reads. Default: the declaring leaf's `name`. */
16
- label?: string;
17
- }
18
- /** The fields every leaf pref shares, keyed by its `kind` and typed by its stored value. */
19
- interface PrefBase<K extends string, Value> {
20
- kind: K;
21
- /** Human-readable label. */
22
- name: string;
23
- /** Longer help text — shown in tooltips / a settings pane. */
24
- description: string;
25
- /** Fallback when nothing is persisted. */
26
- default: Value;
27
- /** Hide from a host app's settings UI by default. */
28
- hidden?: boolean;
29
- /** Render full-width with no label row in schema-driven settings UIs
30
- * (weasel-ui `PrefsForm` honors this for leaves whose control brings
31
- * its own chrome). */
32
- block?: boolean;
33
- /** Glyph naming this leaf in a host UI's icon set (weasel-ui resolves it
34
- * against `ICON_PATHS`). A plain string because this package ships no
35
- * icon set and cannot depend on one. Read where a leaf's `name` has nowhere to go — a
36
- * `pair`ed row is labeled by the pair, so its fields have only the glyph
37
- * to tell them apart. */
38
- icon?: string;
39
- /** Puts this leaf and the ones it names on one row. */
40
- pair?: PrefPair;
41
- /** Shorter forms of `name`, longest first. A surface short of room for
42
- * `name` — a strip of controls on one line, a segment in a `pair`ed row —
43
- * shows the longest of them that fits. `name` stays the accessible name, so
44
- * these abbreviate without costing anything. */
45
- short?: readonly string[];
46
- }
47
- /** How a schema-driven UI should present a number pref. */
48
- type PrefNumberControl = 'input' | 'slider';
49
- /** How a schema-driven UI should present a boolean pref. */
50
- type PrefBooleanControl = 'checkbox' | 'switch' | 'toggle';
51
- /** How a schema-driven UI should present a string pref. */
52
- type PrefStringControl = 'input' | 'textarea';
53
- /** How a schema-driven UI should present an enum pref. */
54
- type PrefEnumControl = 'select' | 'radio' | 'toggle';
55
- /** Display-unit conversion for number leaves whose stored value uses a
56
- * canonical unit the user shouldn't see (e.g. radians stored, degrees
57
- * shown). The stored value stays canonical; UIs convert at the edge. */
58
- interface PrefNumberUnit {
59
- toDisplay: (stored: number) => number;
60
- fromDisplay: (display: number) => number;
61
- /** Shown after the input, e.g. `'°'`. */
62
- suffix?: string;
63
- /** Suffixes a person may type, each mapped to the scale that turns a
64
- * number in that unit into a display number: `{ mm: 0.1, cm: 1 }` for a
65
- * field showing centimeters. A unit that disagrees with the display unit
66
- * about zero carries an `offset` as well. */
67
- accepts?: Readonly<Record<string, UnitEntry>>;
68
- /** The stored value as display text, suffix included. */
69
- format?: (stored: number) => string;
70
- }
71
- /**
72
- * A display unit built from a {@link UnitSystem}: values are stored in the
73
- * system's base and shown in `display`, rounded to `precision` places when
74
- * given, and every unit in the system is accepted as typed text. `suffix`
75
- * defaults to the unit's name and is accepted too.
76
- */
77
- declare function prefUnit(system: UnitSystem, display: Unit, opts?: {
78
- precision?: number;
79
- suffix?: string;
80
- }): PrefNumberUnit;
81
- /** A numeric pref, optionally bounded and stepped, and optionally stored in a
82
- * different unit from the one shown. */
83
- interface PrefNumber extends PrefBase<'number', number> {
84
- min?: number;
85
- max?: number;
86
- step?: number;
87
- control?: PrefNumberControl;
88
- /** How the value shows, speaks and reads back when typed — `compact()`
89
- * reads `2.00M`. Presentation only: the stored value stays a number. */
90
- display?: Display;
91
- unit?: PrefNumberUnit;
92
- /** Which end of a slider stands for infinity: the value there is ±Infinity. */
93
- endless?: 'min' | 'max' | 'both';
94
- /** The word infinity shows as — `'never'`, `'uncapped'`. Default `∞`. */
95
- infinity?: InfinityText;
96
- }
97
- /**
98
- * Stored-value bridge for a boolean leaf whose field is not a boolean — the
99
- * flag counterpart of {@link PrefEnumEncoding}. `TextStyle.fontStyle` is
100
- * the case: it stores `'italic'`, and the control is an Italic toggle.
101
- */
102
- interface PrefBooleanEncoding {
103
- /** Whether `stored` reads as on. */
104
- read: (stored: unknown, siblings: Record<string, unknown> | undefined) => boolean;
105
- /** What to store for `on`. `undefined` removes the field. */
106
- write: (on: boolean, siblings: Record<string, unknown> | undefined) => unknown;
107
- }
108
- /** An on/off pref. */
109
- interface PrefBoolean extends PrefBase<'boolean', boolean> {
110
- control?: PrefBooleanControl;
111
- encoding?: PrefBooleanEncoding;
112
- }
113
- /** A free-text pref. */
114
- interface PrefString extends PrefBase<'string', string> {
115
- control?: PrefStringControl;
116
- }
117
- /**
118
- * Stored-value bridge for an enum leaf whose value is not the option string —
119
- * the counterpart of {@link PrefNumberUnit}, which does the same for a
120
- * number stored in a canonical unit.
121
- *
122
- * A dash array is the case that needs it: `Stroke.dash` stores lengths, and
123
- * the thing a person chooses is a style. The presets scale by the stroke's
124
- * width, so both directions are given the object's other fields — a style is
125
- * meaningless without the width it is a multiple of.
126
- */
127
- interface PrefEnumEncoding<T extends string = string> {
128
- /**
129
- * The option `stored` reads as, or `undefined` for none — which a UI shows
130
- * the way it shows a mixed selection, by selecting nothing.
131
- *
132
- * `siblings` is the object the leaf is a field of, or `undefined` when the
133
- * node does not hold that object (and for a top-level leaf, which has none).
134
- */
135
- read: (stored: unknown, siblings: Record<string, unknown> | undefined) => T | undefined;
136
- /** What to store for `option`. `undefined` removes the field. */
137
- write: (option: T, siblings: Record<string, unknown> | undefined) => unknown;
138
- }
139
- /** A pref with a fixed set of labeled choices. `default` is `undefined` only
140
- * for a {@link PrefEnum.clearable} leaf, whose absence is a value. */
141
- interface PrefEnum<T extends string = string> extends PrefBase<'enum', T | undefined> {
142
- /**
143
- * The field may hold none of the options, and that absence is a state of
144
- * its own rather than a missing value — a script that is neither super nor
145
- * sub. A toggle control then lets the chosen segment be clicked off, which
146
- * removes the field.
147
- */
148
- clearable?: boolean;
149
- /** `short` holds shorter forms of `label`, longest first, for a segmented
150
- * control too narrow for the full one — down to a capital or two. `icon`
151
- * names a glyph in the host UI's set (weasel-ui resolves it against
152
- * `ICON_PATHS`), drawn in place of any text form where it resolves. It is
153
- * a plain string because this package ships no icon set and cannot
154
- * depend on one. The full `label` stays the accessible name, so neither the abbreviation nor the glyph becomes the only thing naming
155
- * the option.
156
- *
157
- * `disabled` marks an option a control reports but cannot author — the
158
- * value a stored form reads as when it matches nothing offered. Dropping it
159
- * from the list instead would leave the control selecting nothing and
160
- * claiming the field is unset. */
161
- options: readonly {
162
- value: T;
163
- label: string;
164
- short?: readonly string[];
165
- icon?: string;
166
- disabled?: boolean;
167
- }[];
168
- control?: PrefEnumControl;
169
- encoding?: PrefEnumEncoding<T>;
170
- }
171
- /** A single color, stored as a hex string. For a value that may also be a
172
- * gradient or a pattern, use {@link PrefPaint} instead. */
173
- interface PrefColor extends PrefBase<'color', string> {
174
- /** Value is `#rrggbb`, or `#rrggbbaa` when `alpha` is set (UIs then
175
- * offer an opacity control). */
176
- alpha?: boolean;
177
- }
178
- /**
179
- * Open leaf: any node with a `kind` outside the built-ins. Schema-driven
180
- * UIs (weasel-ui `PrefsForm` / `SelectionPanel`) dispatch it to an
181
- * app-supplied renderer. Deliberately NOT index-signatured so concrete
182
- * app interfaces stay assignable. Mirrors weasel-ui's `PrefCustom`.
183
- */
184
- type PrefCustom = PrefBase<string, unknown>;
185
- /**
186
- * A whole `FillStyle`, not a color inside one. Use it wherever the value is
187
- * the tagged paint union — a solid color, a pattern, a gradient — rather
188
- * than a hex string.
189
- *
190
- * Addressing `…fill.color` instead reads `undefined` off a gradient (so the
191
- * control shows its default and claims the text is black) and writes a
192
- * hybrid `{ fill: 'gradient', stops, color }` that the renderer's structural
193
- * `'color' in paint` checks then paint flat solid. The union has to be
194
- * edited as a union.
195
- */
196
- interface PrefPaint extends PrefBase<'paint', unknown> {
197
- /** Offer an opacity control alongside the color. */
198
- alpha?: boolean;
199
- }
200
- /**
201
- * A leaf whose value is one object, with its own fields hanging off it.
202
- *
203
- * A compound value — a stroke, a shadow, a pattern spec — could be described
204
- * as several sibling leaves addressing into it (`data.stroke.width`,
205
- * `data.stroke.cap`). It shouldn't be: each control would then write one field
206
- * of a value it can only half see, and writing into something that is not an
207
- * object yet corrupts it. Here the fields are `children` of one leaf, and
208
- * every edit commits the parent object whole.
209
- *
210
- * `children` paths are relative to the object. They are ordinary leaves, so a
211
- * field that is itself a union (a stroke's `paint`) declares the kind that
212
- * edits that union. A child may also be a {@link PrefGroup}, which
213
- * organises the fields under a heading without contributing to the path —
214
- * the same rule group keys follow at the top level. A `TextStyle` needs it:
215
- * its character and paragraph fields belong to one value but read as two
216
- * lists.
217
- */
218
- interface PrefObject extends PrefBase<'object', unknown> {
219
- children: Record<string, PrefLeaf | PrefGroup>;
220
- /**
221
- * Lift a non-object value into the object form, for a consumer field that
222
- * may also be held as a scalar. Called before a child edit is applied;
223
- * without it a scalar-valued leaf shows its children empty and refuses the
224
- * edit.
225
- */
226
- fromScalar?: (value: unknown) => Record<string, unknown>;
227
- }
228
- /**
229
- * A reference to another field of the same schema, held as the full path its
230
- * value is read and written at (`'camera.type'`) — the currency fields refer
231
- * to each other in. A surface offers the fields it draws as the choices.
232
- */
233
- interface PrefField extends PrefBase<'field', string> {
234
- /** Only fields of these kinds may be named. Default: any leaf. */
235
- kinds?: readonly string[];
236
- }
237
- /** One built-in pref leaf. `PrefLeaf` widens this to include
238
- * app-defined kinds. */
239
- type BuiltinPref = PrefNumber | PrefBoolean | PrefString | PrefEnum | PrefColor | PrefPaint | PrefObject | PrefField;
240
- /**
241
- * The built-in kinds, as a table. A kind added to {@link PrefKind} is a
242
- * compile error here, and from here it is one in every renderer's `never`
243
- * guard — the only thing standing between a new kind and rendering as
244
- * nothing in four places at once.
245
- */
246
- declare const PREF_KINDS: Record<PrefKind, true>;
247
- /** Which row each pairing among `leaves` puts a path on: a `key` the row's
248
- * members share (the declaring leaf's path) and the `label` it reads. A
249
- * path no `pair` names is absent. `leaves` are given by the path each
250
- * surface reads and writes them at, so references resolve in its terms. */
251
- declare function pairRowsOf(leaves: Iterable<readonly [path: string, leaf: PrefLeaf]>): Map<string, {
252
- key: string;
253
- label: string;
254
- }>;
255
- /**
256
- * Narrows a leaf to the built-in union, so a renderer's switch discriminates
257
- * on {@link PrefKind} instead of the open `string` that
258
- * {@link PrefCustom} widens `kind` to. An app-defined kind answers false
259
- * and belongs to the renderer's custom-renderer path.
260
- */
261
- declare function isBuiltinPref(leaf: PrefLeaf): leaf is BuiltinPref;
262
- /** Built-in or app-defined leaf. */
263
- type PrefLeaf = BuiltinPref | PrefCustom;
264
- /** Nestable group: branch nodes a tool can use to organize its prefs. */
265
- interface PrefGroup {
266
- /** Heading for the group's rows. **Empty means no heading** — for a group
267
- * that exists to organise, not to name: one whose children are themselves
268
- * groups carrying the labels a reader needs. Give it a name whenever the
269
- * name is the referent (a `Border` group over `Top` / `Right` / `Bottom`
270
- * reads as nothing without it). */
271
- name: string;
272
- description?: string;
273
- children: Record<string, PrefLeaf | PrefGroup>;
274
- }
275
-
276
- type Join<A extends string, B extends string> = A extends '' ? B : `${A}.${B}`;
277
- /** Every leaf's dotted path in a schema. A group's key is a segment; an
278
- * `object` leaf is one path, its fields are not. */
279
- type PrefPath<G, Prefix extends string = ''> = G extends {
280
- children: infer C;
281
- } ? string extends keyof C ? Join<Prefix, string> : {
282
- [K in keyof C & string]: C[K] extends {
283
- kind: string;
284
- } ? Join<Prefix, K> : C[K] extends {
285
- children: Record<string, unknown>;
286
- } ? PrefPath<C[K], Join<Prefix, K>> : never;
287
- }[keyof C & string] : never;
288
- /** The leaf at `P`. */
289
- type PrefAtPath<G, P extends string> = G extends {
290
- children: infer C;
291
- } ? P extends `${infer H}.${infer Rest}` ? H extends keyof C ? C[H] extends {
292
- kind: string;
293
- } ? never : PrefAtPath<C[H], Rest> : never : P extends keyof C ? C[P] extends {
294
- kind: string;
295
- } ? C[P] : never : never : never;
296
- /** What a leaf holds: its kind's value type for a built-in kind, the type of
297
- * its `default` for an app-defined one. */
298
- type PrefValueOf<L> = L extends {
299
- kind: 'number';
300
- } ? number : L extends {
301
- kind: 'boolean';
302
- } ? boolean : L extends {
303
- kind: 'string' | 'color' | 'field';
304
- } ? string : L extends {
305
- kind: 'paint';
306
- } ? unknown : L extends {
307
- kind: 'enum';
308
- options: readonly {
309
- value: infer T;
310
- }[];
311
- } ? L extends {
312
- clearable: true;
313
- } ? T | undefined : T : L extends {
314
- default: infer D;
315
- } ? D : unknown;
316
- /** What the leaf at `P` holds. */
317
- type PrefValueAt<G, P extends string> = PrefValueOf<PrefAtPath<G, P>>;
318
-
319
- /** One leaf changing, as a store subscriber hears it: the value readers now
320
- * see, and whether this store or another writer made the change. */
321
- interface PrefChange {
322
- path: string;
323
- value: unknown;
324
- origin: 'local' | 'remote';
325
- }
326
- /** Values for one schema, held in memory and persisted behind. */
327
- interface PrefsStore<S extends PrefGroup> {
328
- readonly schema: S;
329
- /** The leaf's value, repaired against the schema; its default when unset. */
330
- get<P extends PrefPath<S>>(path: P): PrefValueAt<S, P>;
331
- set<P extends PrefPath<S>>(path: P, value: PrefValueAt<S, P>): void;
332
- /** Unset every leaf at or under `path`; with no path, all. */
333
- reset(path?: string): void;
334
- /** Whether the leaf has a value of its own rather than following its default. */
335
- isSet(path: PrefPath<S>): boolean;
336
- /** Every leaf's value as a nested tree. The same object until something changes. */
337
- values(): Record<string, unknown>;
338
- /** The leaves following their default. The same set until something changes. */
339
- unset(): ReadonlySet<string>;
340
- /** Every stored record as a nested tree, unrepaired, including records no
341
- * leaf describes. The same object until something changes. */
342
- stored(): Record<string, unknown>;
343
- /** `changes` is empty when only records no leaf describes changed. */
344
- subscribe(listener: (changes: PrefChange[]) => void): () => void;
345
- /** False when the store cannot persist: storage was unreadable, or written
346
- * by a newer schema. Changes still apply for this session. */
347
- readonly writable: boolean;
348
- /** Resolves true when every queued write reached storage. */
349
- flush(): Promise<boolean>;
350
- close(): Promise<void>;
351
- }
352
-
353
- export { type PrefValueOf as A, type BuiltinPref as B, isBuiltinPref as C, pairRowsOf as D, prefUnit as E, type PrefLeaf as P, type PrefGroup as a, type PrefNumber as b, type PrefsStore as c, PREF_KINDS as d, type PrefAtPath as e, type PrefBase as f, type PrefBoolean as g, type PrefBooleanControl as h, type PrefBooleanEncoding as i, type PrefChange as j, type PrefColor as k, type PrefCustom as l, type PrefEnum as m, type PrefEnumControl as n, type PrefEnumEncoding as o, type PrefField as p, type PrefKind as q, type PrefNumberControl as r, type PrefNumberUnit as s, type PrefObject as t, type PrefPaint as u, type PrefPair as v, type PrefPath as w, type PrefString as x, type PrefStringControl as y, type PrefValueAt as z };