@weasel-js/prefs 0.0.0-stage → 1.9.3
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/LICENSE +21 -0
- package/README.md +19 -2
- package/dist/index.d.ts +81 -0
- package/dist/index.js +378 -0
- package/dist/index.js.map +1 -0
- package/dist/react.d.ts +20 -0
- package/dist/react.js +34 -0
- package/dist/react.js.map +1 -0
- package/dist/store-BU2tRX5H.d.ts +353 -0
- package/package.json +57 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 orochi235
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,20 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @weasel-js/prefs
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A preferences schema — groups of typed leaves with names, defaults and
|
|
4
|
+
rendering hints — and a store that keeps values for it.
|
|
5
|
+
|
|
6
|
+
```ts
|
|
7
|
+
const store = await openPrefs(SCHEMA, { storage: indexedDbAdapter, prefix: 'myapp.prefs.' });
|
|
8
|
+
store.get('view.gridVisible');
|
|
9
|
+
store.set('view.gridVisible', false);
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
One record per leaf. A leaf with no record follows its schema default. Reads
|
|
13
|
+
repair stored values that no longer fit the schema without writing them back.
|
|
14
|
+
`migrations[i]` takes stored records from version `i` to `i + 1`.
|
|
15
|
+
`openPrefsSync` opens from an adapter that reads synchronously, such as
|
|
16
|
+
`localStorageAdapter`, with no `await`.
|
|
17
|
+
|
|
18
|
+
`usePref(store, path)` and `usePrefsValues(store)` bind it to React. They
|
|
19
|
+
live in `@weasel-js/prefs/react`, so the main entry never loads React.
|
|
20
|
+
`usePrefsValues` returns the pieces `@weasel-js/ui`'s `PrefsForm` takes.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { P as PrefLeaf, a as PrefGroup, b as PrefNumber, c as PrefsStore } from './store-BU2tRX5H.js';
|
|
2
|
+
export { B as BuiltinPref, d as PREF_KINDS, e as PrefAtPath, f as PrefBase, g as PrefBoolean, h as PrefBooleanControl, i as PrefBooleanEncoding, j as PrefChange, k as PrefColor, l as PrefCustom, m as PrefEnum, n as PrefEnumControl, o as PrefEnumEncoding, p as PrefField, q as PrefKind, r as PrefNumberControl, s as PrefNumberUnit, t as PrefObject, u as PrefPaint, v as PrefPair, w as PrefPath, x as PrefString, y as PrefStringControl, z as PrefValueAt, A as PrefValueOf, C as isBuiltinPref, D as pairRowsOf, E as prefUnit } from './store-BU2tRX5H.js';
|
|
3
|
+
import { StorageAdapter, SyncStorageAdapter } from '@weasel-js/storage';
|
|
4
|
+
import '@weasel-js/quantity';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* A number leaf's bounds in the unit it is displayed in.
|
|
8
|
+
*
|
|
9
|
+
* Only what the leaf declares converts: an omitted bound has no stored
|
|
10
|
+
* counterpart to put through the conversion, and its fallback — 0..100 for a
|
|
11
|
+
* slider's track, a step of 1 — is a display-space number already. `min` and
|
|
12
|
+
* `max` are points, so they convert the way the value does; `step` is a
|
|
13
|
+
* distance, and a unit with an offset maps zero somewhere else, so converting
|
|
14
|
+
* it as a point would scale it wrong. A decreasing conversion swaps which end
|
|
15
|
+
* is the lower one.
|
|
16
|
+
*/
|
|
17
|
+
declare function prefDisplayBounds(p: PrefNumber): {
|
|
18
|
+
min?: number;
|
|
19
|
+
max?: number;
|
|
20
|
+
step: number;
|
|
21
|
+
};
|
|
22
|
+
/** Distinguishes a leaf from a group while walking a schema tree. */
|
|
23
|
+
declare function isPrefLeaf(node: PrefLeaf | PrefGroup): node is PrefLeaf;
|
|
24
|
+
/** Get the value at a dotted path inside a nested value tree. Returns
|
|
25
|
+
* `undefined` when a segment is missing or hits a non-object. */
|
|
26
|
+
declare function prefValueAtPath(values: unknown, path: string): unknown;
|
|
27
|
+
/**
|
|
28
|
+
* Recursively drop `hidden` leaves (unless `showHidden`), pruning groups
|
|
29
|
+
* that end up empty. Returns null when the entire subtree is hidden.
|
|
30
|
+
*/
|
|
31
|
+
declare function visiblePrefSubtree<T extends PrefLeaf | PrefGroup>(node: T, showHidden: boolean): T | null;
|
|
32
|
+
/** The record holding the schema version the stored values were written at.
|
|
33
|
+
* Reserved: no top-level schema key may take its name. */
|
|
34
|
+
declare const VERSION_RECORD = "$version";
|
|
35
|
+
/** Every leaf under `schema`, by dotted path, in schema order. */
|
|
36
|
+
declare function prefLeaves(schema: PrefGroup): Map<string, PrefLeaf>;
|
|
37
|
+
/** Each of `schema`'s leaves that `tree` holds a value for, as a path and
|
|
38
|
+
* that value — the inverse of building a tree from per-leaf records. */
|
|
39
|
+
declare function flattenPrefValues(schema: PrefGroup, tree: unknown): [string, unknown][];
|
|
40
|
+
/**
|
|
41
|
+
* Drop every leaf that does not answer to `query`, pruning groups left empty.
|
|
42
|
+
* A group whose own name matches keeps all of its leaves — a reader who typed
|
|
43
|
+
* the group's name is asking for the group, not for leaves repeating it.
|
|
44
|
+
*
|
|
45
|
+
* An empty or whitespace query matches everything, so a cleared field restores
|
|
46
|
+
* the tree rather than emptying it.
|
|
47
|
+
*/
|
|
48
|
+
declare function filterPrefSubtree<T extends PrefLeaf | PrefGroup>(node: T, query: string, path?: string): T | null;
|
|
49
|
+
|
|
50
|
+
/** Takes stored records from one version to the next: rename, transform or
|
|
51
|
+
* delete entries in `records`, keyed by leaf path. */
|
|
52
|
+
type PrefsMigration = (records: Map<string, unknown>) => void;
|
|
53
|
+
|
|
54
|
+
/** Decides what a stored value reads as for one kind of leaf: the value to
|
|
55
|
+
* read, or `undefined` for "invalid, read the default". */
|
|
56
|
+
type PrefValidator = (stored: unknown, leaf: PrefLeaf) => unknown;
|
|
57
|
+
/** What `stored` reads as for `leaf`. Never writes anything back: a schema
|
|
58
|
+
* rolled back finds the original still in storage. */
|
|
59
|
+
declare function repairPrefValue(leaf: PrefLeaf, stored: unknown, validators?: Readonly<Record<string, PrefValidator>>): unknown;
|
|
60
|
+
|
|
61
|
+
/** Options for `openPrefs` and `openPrefsSync`. */
|
|
62
|
+
interface PrefsOptions {
|
|
63
|
+
storage: StorageAdapter;
|
|
64
|
+
/** Prepended to every record's key: `'myapp.prefs.'`. */
|
|
65
|
+
prefix: string;
|
|
66
|
+
/** `migrations[i]` takes stored records from version `i` to `i + 1`. The
|
|
67
|
+
* current version is `migrations.length`. */
|
|
68
|
+
migrations?: readonly PrefsMigration[];
|
|
69
|
+
/** Validators for app-defined kinds, keyed by `kind`. One given for a
|
|
70
|
+
* built-in kind replaces that kind's own rule. */
|
|
71
|
+
validators?: Readonly<Record<string, PrefValidator>>;
|
|
72
|
+
}
|
|
73
|
+
/** Load every stored value for `schema`, migrate it, and return the store. */
|
|
74
|
+
declare function openPrefs<S extends PrefGroup>(schema: S, options: PrefsOptions): Promise<PrefsStore<S>>;
|
|
75
|
+
/** `openPrefs` for an adapter that reads synchronously: the store is ready
|
|
76
|
+
* on return, so values can be read before anything renders. */
|
|
77
|
+
declare function openPrefsSync<S extends PrefGroup>(schema: S, options: PrefsOptions & {
|
|
78
|
+
storage: SyncStorageAdapter;
|
|
79
|
+
}): PrefsStore<S>;
|
|
80
|
+
|
|
81
|
+
export { PrefGroup, PrefLeaf, PrefNumber, type PrefValidator, type PrefsMigration, type PrefsOptions, PrefsStore, VERSION_RECORD, filterPrefSubtree, flattenPrefValues, isPrefLeaf, openPrefs, openPrefsSync, prefDisplayBounds, prefLeaves, prefValueAtPath, repairPrefValue, visiblePrefSubtree };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
import { openRecords, openRecordsSync } from '@weasel-js/storage';
|
|
2
|
+
import { unitScale, formatUnit } from '@weasel-js/quantity';
|
|
3
|
+
|
|
4
|
+
// src/helpers.ts
|
|
5
|
+
function prefDisplayBounds(p) {
|
|
6
|
+
if (!p.unit) return { min: p.min, max: p.max, step: p.step ?? 1 };
|
|
7
|
+
const { toDisplay } = p.unit;
|
|
8
|
+
const lo = p.min === void 0 ? void 0 : toDisplay(p.min);
|
|
9
|
+
const hi = p.max === void 0 ? void 0 : toDisplay(p.max);
|
|
10
|
+
const flipped = lo !== void 0 && hi !== void 0 && lo > hi;
|
|
11
|
+
const step = p.step === void 0 ? 1 : Math.abs(toDisplay(p.step) - toDisplay(0)) || p.step;
|
|
12
|
+
return { min: flipped ? hi : lo, max: flipped ? lo : hi, step };
|
|
13
|
+
}
|
|
14
|
+
function isPrefLeaf(node) {
|
|
15
|
+
return "kind" in node;
|
|
16
|
+
}
|
|
17
|
+
function prefValueAtPath(values, path) {
|
|
18
|
+
let cur = values;
|
|
19
|
+
for (const seg of path.split(".")) {
|
|
20
|
+
if (cur == null || typeof cur !== "object") return void 0;
|
|
21
|
+
cur = cur[seg];
|
|
22
|
+
}
|
|
23
|
+
return cur;
|
|
24
|
+
}
|
|
25
|
+
function visiblePrefSubtree(node, showHidden) {
|
|
26
|
+
if (isPrefLeaf(node)) return node.hidden && !showHidden ? null : node;
|
|
27
|
+
const group = node;
|
|
28
|
+
const children = {};
|
|
29
|
+
for (const [key, child] of Object.entries(group.children)) {
|
|
30
|
+
const kept = visiblePrefSubtree(child, showHidden);
|
|
31
|
+
if (kept) children[key] = kept;
|
|
32
|
+
}
|
|
33
|
+
if (Object.keys(children).length === 0) return null;
|
|
34
|
+
return { ...node, children };
|
|
35
|
+
}
|
|
36
|
+
var VERSION_RECORD = "$version";
|
|
37
|
+
function prefLeaves(schema) {
|
|
38
|
+
const out = /* @__PURE__ */ new Map();
|
|
39
|
+
const walk = (group, prefix) => {
|
|
40
|
+
for (const [key, child] of Object.entries(group.children)) {
|
|
41
|
+
if (key.includes(".")) {
|
|
42
|
+
throw new Error(`[prefs] schema key "${key}" contains "."; group keys are path segments`);
|
|
43
|
+
}
|
|
44
|
+
if (prefix === "" && key === VERSION_RECORD) {
|
|
45
|
+
throw new Error(`[prefs] schema key "${VERSION_RECORD}" is reserved`);
|
|
46
|
+
}
|
|
47
|
+
const path = prefix === "" ? key : `${prefix}.${key}`;
|
|
48
|
+
if (isPrefLeaf(child)) out.set(path, child);
|
|
49
|
+
else walk(child, path);
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
walk(schema, "");
|
|
53
|
+
return out;
|
|
54
|
+
}
|
|
55
|
+
function assignPrefValueAtPath(root, path, value) {
|
|
56
|
+
const parts = path.split(".");
|
|
57
|
+
let cursor = root;
|
|
58
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
59
|
+
cursor = cursor[parts[i]] ??= {};
|
|
60
|
+
}
|
|
61
|
+
cursor[parts[parts.length - 1]] = value;
|
|
62
|
+
}
|
|
63
|
+
function flattenPrefValues(schema, tree) {
|
|
64
|
+
const out = [];
|
|
65
|
+
for (const path of prefLeaves(schema).keys()) {
|
|
66
|
+
const value = prefValueAtPath(tree, path);
|
|
67
|
+
if (value !== void 0) out.push([path, value]);
|
|
68
|
+
}
|
|
69
|
+
return out;
|
|
70
|
+
}
|
|
71
|
+
function prefLeafMatches(pref, path, query) {
|
|
72
|
+
if (pref.name.toLowerCase().includes(query)) return true;
|
|
73
|
+
if (pref.description?.toLowerCase().includes(query)) return true;
|
|
74
|
+
return path.toLowerCase().includes(query);
|
|
75
|
+
}
|
|
76
|
+
function filterPrefSubtree(node, query, path = "") {
|
|
77
|
+
const q = query.trim().toLowerCase();
|
|
78
|
+
if (q === "") return node;
|
|
79
|
+
if (isPrefLeaf(node)) return prefLeafMatches(node, path, q) ? node : null;
|
|
80
|
+
const group = node;
|
|
81
|
+
if (group.name.toLowerCase().includes(q)) return node;
|
|
82
|
+
const children = {};
|
|
83
|
+
for (const [key, child] of Object.entries(group.children)) {
|
|
84
|
+
const kept = filterPrefSubtree(child, q, path === "" ? key : `${path}.${key}`);
|
|
85
|
+
if (kept) children[key] = kept;
|
|
86
|
+
}
|
|
87
|
+
if (Object.keys(children).length === 0) return null;
|
|
88
|
+
return { ...node, children };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// src/migrate.ts
|
|
92
|
+
var isVersion = (v) => typeof v === "number" && Number.isInteger(v) && v >= 0;
|
|
93
|
+
function runPrefsMigrations(cache, migrations) {
|
|
94
|
+
const target = migrations.length;
|
|
95
|
+
const raw = cache.get(VERSION_RECORD);
|
|
96
|
+
if (cache.has(VERSION_RECORD) && !isVersion(raw)) {
|
|
97
|
+
console.warn(
|
|
98
|
+
`[prefs] "${cache.prefix}" has an unreadable ${VERSION_RECORD} (${JSON.stringify(raw)}); opening read-only`
|
|
99
|
+
);
|
|
100
|
+
cache.stopWriting();
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
const stored = cache.has(VERSION_RECORD) ? raw : 0;
|
|
104
|
+
if (stored > target) {
|
|
105
|
+
console.warn(
|
|
106
|
+
`[prefs] "${cache.prefix}" was written at version ${stored}, newer than this build's ${target}; opening read-only`
|
|
107
|
+
);
|
|
108
|
+
cache.stopWriting();
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
if (stored === target || !cache.writable) return false;
|
|
112
|
+
const before = cache.entries().filter(([name]) => name !== VERSION_RECORD);
|
|
113
|
+
const records = new Map(before.map(([name, value]) => [name, structuredClone(value)]));
|
|
114
|
+
for (let v = stored; v < target; v++) {
|
|
115
|
+
try {
|
|
116
|
+
migrations[v](records);
|
|
117
|
+
} catch (error) {
|
|
118
|
+
console.warn(
|
|
119
|
+
`[prefs] migration ${v} \u2192 ${v + 1} for "${cache.prefix}" threw; opening read-only on the unmigrated records`,
|
|
120
|
+
error
|
|
121
|
+
);
|
|
122
|
+
cache.stopWriting();
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
for (const [name] of before) if (!records.has(name)) cache.delete(name);
|
|
127
|
+
for (const [name, value] of records) {
|
|
128
|
+
if (value === void 0) cache.delete(name);
|
|
129
|
+
else cache.set(name, value);
|
|
130
|
+
}
|
|
131
|
+
cache.set(VERSION_RECORD, target);
|
|
132
|
+
return true;
|
|
133
|
+
}
|
|
134
|
+
function watchPrefsVersion(cache, target) {
|
|
135
|
+
return cache.subscribe((changes) => {
|
|
136
|
+
for (const change of changes) {
|
|
137
|
+
if (change.origin !== "remote" || change.name !== VERSION_RECORD || change.value === void 0) continue;
|
|
138
|
+
if (!isVersion(change.value)) {
|
|
139
|
+
console.warn(
|
|
140
|
+
`[prefs] "${cache.prefix}" has an unreadable ${VERSION_RECORD} (${JSON.stringify(change.value)}) from another writer; no longer persisting`
|
|
141
|
+
);
|
|
142
|
+
cache.stopWriting();
|
|
143
|
+
} else if (change.value > target) {
|
|
144
|
+
console.warn(
|
|
145
|
+
`[prefs] "${cache.prefix}" was migrated to version ${change.value} by another writer; this build knows ${target}; no longer persisting`
|
|
146
|
+
);
|
|
147
|
+
cache.stopWriting();
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// src/repair.ts
|
|
154
|
+
function repairPrefValue(leaf, stored, validators) {
|
|
155
|
+
if (leaf.kind === "number") {
|
|
156
|
+
if (stored === "Infinity") stored = Infinity;
|
|
157
|
+
else if (stored === "-Infinity") stored = -Infinity;
|
|
158
|
+
}
|
|
159
|
+
const validate = validators?.[leaf.kind];
|
|
160
|
+
if (validate) {
|
|
161
|
+
try {
|
|
162
|
+
const out = validate(stored, leaf);
|
|
163
|
+
return out === void 0 ? leaf.default : out;
|
|
164
|
+
} catch {
|
|
165
|
+
return leaf.default;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
switch (leaf.kind) {
|
|
169
|
+
case "number":
|
|
170
|
+
return repairNumber(leaf, stored);
|
|
171
|
+
case "boolean":
|
|
172
|
+
return leaf.encoding || typeof stored === "boolean" ? stored : leaf.default;
|
|
173
|
+
case "string":
|
|
174
|
+
case "color":
|
|
175
|
+
case "field":
|
|
176
|
+
return typeof stored === "string" ? stored : leaf.default;
|
|
177
|
+
case "enum": {
|
|
178
|
+
const e = leaf;
|
|
179
|
+
if (e.encoding) return stored;
|
|
180
|
+
return e.options.some((o) => o.value === stored) ? stored : leaf.default;
|
|
181
|
+
}
|
|
182
|
+
case "object":
|
|
183
|
+
if (leaf.fromScalar || isPlainObject(stored)) return stored;
|
|
184
|
+
return leaf.default;
|
|
185
|
+
default:
|
|
186
|
+
return stored;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
function isPlainObject(v) {
|
|
190
|
+
return v !== null && typeof v === "object" && !Array.isArray(v);
|
|
191
|
+
}
|
|
192
|
+
function repairNumber(leaf, stored) {
|
|
193
|
+
if (typeof stored !== "number" || Number.isNaN(stored)) return leaf.default;
|
|
194
|
+
const { endless } = leaf;
|
|
195
|
+
if (stored === Infinity && (endless === "max" || endless === "both")) return stored;
|
|
196
|
+
if (stored === -Infinity && (endless === "min" || endless === "both")) return stored;
|
|
197
|
+
let v = stored;
|
|
198
|
+
if (leaf.min !== void 0 && v < leaf.min) v = leaf.min;
|
|
199
|
+
if (leaf.max !== void 0 && v > leaf.max) v = leaf.max;
|
|
200
|
+
return Number.isFinite(v) ? v : leaf.default;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// src/store.ts
|
|
204
|
+
var encodeInfinity = (leaf, value) => leaf.kind === "number" && (value === Infinity || value === -Infinity) ? String(value) : value;
|
|
205
|
+
function createPrefsStore(schema, cache, validators, onClose) {
|
|
206
|
+
const leaves = prefLeaves(schema);
|
|
207
|
+
let snapshot = null;
|
|
208
|
+
let previous = null;
|
|
209
|
+
const build = () => {
|
|
210
|
+
const byPath = /* @__PURE__ */ new Map();
|
|
211
|
+
const raw = /* @__PURE__ */ new Map();
|
|
212
|
+
const unset = /* @__PURE__ */ new Set();
|
|
213
|
+
const tree = {};
|
|
214
|
+
for (const [path, leaf] of leaves) {
|
|
215
|
+
const has = cache.has(path);
|
|
216
|
+
let value;
|
|
217
|
+
if (!has) {
|
|
218
|
+
value = leaf.default;
|
|
219
|
+
unset.add(path);
|
|
220
|
+
} else {
|
|
221
|
+
const stored = cache.get(path);
|
|
222
|
+
raw.set(path, stored);
|
|
223
|
+
value = previous?.raw.has(path) && previous.raw.get(path) === stored ? previous.byPath.get(path) : repairPrefValue(leaf, stored, validators);
|
|
224
|
+
}
|
|
225
|
+
byPath.set(path, value);
|
|
226
|
+
assignPrefValueAtPath(tree, path, value);
|
|
227
|
+
}
|
|
228
|
+
previous = { raw, byPath };
|
|
229
|
+
return { byPath, tree, unset };
|
|
230
|
+
};
|
|
231
|
+
const current = () => snapshot ??= build();
|
|
232
|
+
let storedTree = null;
|
|
233
|
+
const buildStored = () => {
|
|
234
|
+
const tree = {};
|
|
235
|
+
const branches = new WeakSet([tree]);
|
|
236
|
+
for (const [name, value] of cache.entries()) {
|
|
237
|
+
if (name === VERSION_RECORD) continue;
|
|
238
|
+
const parts = name.split(".");
|
|
239
|
+
let cursor = tree;
|
|
240
|
+
for (const part of parts.slice(0, -1)) {
|
|
241
|
+
const next = cursor[part];
|
|
242
|
+
if (typeof next === "object" && next !== null && branches.has(next)) {
|
|
243
|
+
cursor = next;
|
|
244
|
+
} else {
|
|
245
|
+
const branch = {};
|
|
246
|
+
branches.add(branch);
|
|
247
|
+
cursor[part] = branch;
|
|
248
|
+
cursor = branch;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
const last = parts[parts.length - 1];
|
|
252
|
+
if (!branches.has(cursor[last])) cursor[last] = value;
|
|
253
|
+
}
|
|
254
|
+
return tree;
|
|
255
|
+
};
|
|
256
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
257
|
+
const stopCache = cache.subscribe((changes) => {
|
|
258
|
+
const records = changes.filter((c) => c.name !== VERSION_RECORD);
|
|
259
|
+
if (records.length === 0) return;
|
|
260
|
+
storedTree = null;
|
|
261
|
+
const mine = records.filter((c) => leaves.has(c.name));
|
|
262
|
+
if (mine.length > 0) snapshot = null;
|
|
263
|
+
const now = current();
|
|
264
|
+
const out = mine.map((c) => ({ path: c.name, value: now.byPath.get(c.name), origin: c.origin }));
|
|
265
|
+
for (const listener of [...listeners]) listener(out);
|
|
266
|
+
});
|
|
267
|
+
return {
|
|
268
|
+
schema,
|
|
269
|
+
get: (path) => current().byPath.get(path),
|
|
270
|
+
set: (path, value) => {
|
|
271
|
+
const leaf = leaves.get(path);
|
|
272
|
+
if (!leaf) {
|
|
273
|
+
console.warn(`[prefs] no leaf at "${path}"; ignoring the write`);
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
cache.set(path, encodeInfinity(leaf, value));
|
|
277
|
+
},
|
|
278
|
+
reset: (path) => {
|
|
279
|
+
for (const p of leaves.keys()) {
|
|
280
|
+
if (path === void 0 || p === path || p.startsWith(`${path}.`)) cache.delete(p);
|
|
281
|
+
}
|
|
282
|
+
},
|
|
283
|
+
isSet: (path) => cache.has(path),
|
|
284
|
+
values: () => current().tree,
|
|
285
|
+
unset: () => current().unset,
|
|
286
|
+
stored: () => storedTree ??= buildStored(),
|
|
287
|
+
subscribe: (listener) => {
|
|
288
|
+
listeners.add(listener);
|
|
289
|
+
return () => {
|
|
290
|
+
listeners.delete(listener);
|
|
291
|
+
};
|
|
292
|
+
},
|
|
293
|
+
get writable() {
|
|
294
|
+
return cache.writable;
|
|
295
|
+
},
|
|
296
|
+
flush: () => cache.flush(),
|
|
297
|
+
close: async () => {
|
|
298
|
+
onClose?.();
|
|
299
|
+
stopCache();
|
|
300
|
+
listeners.clear();
|
|
301
|
+
await cache.close();
|
|
302
|
+
}
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// src/open.ts
|
|
307
|
+
async function openPrefs(schema, options) {
|
|
308
|
+
const cache = await openRecords({ storage: options.storage, prefix: options.prefix });
|
|
309
|
+
const stop = watch(cache, options);
|
|
310
|
+
if (runPrefsMigrations(cache, options.migrations ?? [])) await cache.flush();
|
|
311
|
+
return createPrefsStore(schema, cache, options.validators, stop);
|
|
312
|
+
}
|
|
313
|
+
function openPrefsSync(schema, options) {
|
|
314
|
+
const cache = openRecordsSync({ storage: options.storage, prefix: options.prefix });
|
|
315
|
+
const stop = watch(cache, options);
|
|
316
|
+
if (runPrefsMigrations(cache, options.migrations ?? [])) void cache.flush();
|
|
317
|
+
return createPrefsStore(schema, cache, options.validators, stop);
|
|
318
|
+
}
|
|
319
|
+
var watch = (cache, options) => cache.writable ? watchPrefsVersion(cache, (options.migrations ?? []).length) : void 0;
|
|
320
|
+
function prefUnit(system, display, opts) {
|
|
321
|
+
let self;
|
|
322
|
+
try {
|
|
323
|
+
self = unitScale(system, display);
|
|
324
|
+
} catch (e) {
|
|
325
|
+
throw new Error(`prefUnit: ${e instanceof Error ? e.message : String(e)}`);
|
|
326
|
+
}
|
|
327
|
+
const scale = opts?.precision === void 0 ? void 0 : 10 ** opts.precision;
|
|
328
|
+
const suffix = opts?.suffix ?? display;
|
|
329
|
+
const accepts = { [suffix]: 1 };
|
|
330
|
+
for (const name of Object.keys(system.units)) {
|
|
331
|
+
const other = unitScale(system, name);
|
|
332
|
+
const factor = other.factor / self.factor;
|
|
333
|
+
const offset = (other.offset - self.offset) / self.factor;
|
|
334
|
+
accepts[name] = offset === 0 ? factor : { factor, offset };
|
|
335
|
+
}
|
|
336
|
+
const toDisplay = (stored) => {
|
|
337
|
+
const shown = (stored - self.offset) / self.factor;
|
|
338
|
+
return scale === void 0 ? shown : Math.round(shown * scale) / scale;
|
|
339
|
+
};
|
|
340
|
+
return {
|
|
341
|
+
toDisplay,
|
|
342
|
+
fromDisplay: (shown) => shown * self.factor + self.offset,
|
|
343
|
+
suffix,
|
|
344
|
+
accepts,
|
|
345
|
+
format: (stored) => `${formatUnit(stored, display, system, {
|
|
346
|
+
precision: opts?.precision ?? 2,
|
|
347
|
+
suffix: false
|
|
348
|
+
})}${suffix}`
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
var PREF_KINDS = {
|
|
352
|
+
number: true,
|
|
353
|
+
boolean: true,
|
|
354
|
+
string: true,
|
|
355
|
+
enum: true,
|
|
356
|
+
color: true,
|
|
357
|
+
paint: true,
|
|
358
|
+
object: true,
|
|
359
|
+
field: true
|
|
360
|
+
};
|
|
361
|
+
function pairRowsOf(leaves) {
|
|
362
|
+
const rows = /* @__PURE__ */ new Map();
|
|
363
|
+
for (const [path, leaf] of leaves) {
|
|
364
|
+
if (!leaf.pair || rows.has(path)) continue;
|
|
365
|
+
const row = { key: path, label: leaf.pair.label ?? leaf.name };
|
|
366
|
+
rows.set(path, row);
|
|
367
|
+
const partners = typeof leaf.pair.with === "string" ? [leaf.pair.with] : leaf.pair.with;
|
|
368
|
+
for (const partner of partners) if (!rows.has(partner)) rows.set(partner, row);
|
|
369
|
+
}
|
|
370
|
+
return rows;
|
|
371
|
+
}
|
|
372
|
+
function isBuiltinPref(leaf) {
|
|
373
|
+
return Object.hasOwn(PREF_KINDS, leaf.kind);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
export { PREF_KINDS, VERSION_RECORD, filterPrefSubtree, flattenPrefValues, isBuiltinPref, isPrefLeaf, openPrefs, openPrefsSync, pairRowsOf, prefDisplayBounds, prefLeaves, prefUnit, prefValueAtPath, repairPrefValue, visiblePrefSubtree };
|
|
377
|
+
//# sourceMappingURL=index.js.map
|
|
378
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/helpers.ts","../src/migrate.ts","../src/repair.ts","../src/store.ts","../src/open.ts","../src/schema.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["import type { PrefGroup, PrefLeaf, PrefNumber } from './schema';\n\n/**\n * A number leaf's bounds in the unit it is displayed in.\n *\n * Only what the leaf declares converts: an omitted bound has no stored\n * counterpart to put through the conversion, and its fallback — 0..100 for a\n * slider's track, a step of 1 — is a display-space number already. `min` and\n * `max` are points, so they convert the way the value does; `step` is a\n * distance, and a unit with an offset maps zero somewhere else, so converting\n * it as a point would scale it wrong. A decreasing conversion swaps which end\n * is the lower one.\n */\nexport function prefDisplayBounds(\n p: PrefNumber,\n): { min?: number; max?: number; step: number } {\n if (!p.unit) return { min: p.min, max: p.max, step: p.step ?? 1 };\n const { toDisplay } = p.unit;\n const lo = p.min === undefined ? undefined : toDisplay(p.min);\n const hi = p.max === undefined ? undefined : toDisplay(p.max);\n const flipped = lo !== undefined && hi !== undefined && lo > hi;\n const step = p.step === undefined\n ? 1\n : Math.abs(toDisplay(p.step) - toDisplay(0)) || p.step;\n return { min: flipped ? hi : lo, max: flipped ? lo : hi, step };\n}\n\n/** Distinguishes a leaf from a group while walking a schema tree. */\nexport function isPrefLeaf(node: PrefLeaf | PrefGroup): node is PrefLeaf {\n return 'kind' in node;\n}\n\n/** Get the value at a dotted path inside a nested value tree. Returns\n * `undefined` when a segment is missing or hits a non-object. */\nexport function prefValueAtPath(values: unknown, path: string): unknown {\n let cur: unknown = values;\n for (const seg of path.split('.')) {\n if (cur == null || typeof cur !== 'object') return undefined;\n cur = (cur as Record<string, unknown>)[seg];\n }\n return cur;\n}\n\n/**\n * Recursively drop `hidden` leaves (unless `showHidden`), pruning groups\n * that end up empty. Returns null when the entire subtree is hidden.\n */\nexport function visiblePrefSubtree<T extends PrefLeaf | PrefGroup>(\n node: T,\n showHidden: boolean,\n): T | null {\n if (isPrefLeaf(node)) return node.hidden && !showHidden ? null : node;\n const group = node as PrefGroup;\n const children: Record<string, PrefLeaf | PrefGroup> = {};\n for (const [key, child] of Object.entries(group.children)) {\n const kept = visiblePrefSubtree(child, showHidden);\n if (kept) children[key] = kept;\n }\n if (Object.keys(children).length === 0) return null;\n return { ...node, children };\n}\n\n/** The record holding the schema version the stored values were written at.\n * Reserved: no top-level schema key may take its name. */\nexport const VERSION_RECORD = '$version';\n\n/** Every leaf under `schema`, by dotted path, in schema order. */\nexport function prefLeaves(schema: PrefGroup): Map<string, PrefLeaf> {\n const out = new Map<string, PrefLeaf>();\n const walk = (group: PrefGroup, prefix: string): void => {\n for (const [key, child] of Object.entries(group.children)) {\n if (key.includes('.')) {\n throw new Error(`[prefs] schema key \"${key}\" contains \".\"; group keys are path segments`);\n }\n if (prefix === '' && key === VERSION_RECORD) {\n throw new Error(`[prefs] schema key \"${VERSION_RECORD}\" is reserved`);\n }\n const path = prefix === '' ? key : `${prefix}.${key}`;\n if (isPrefLeaf(child)) out.set(path, child);\n else walk(child, path);\n }\n };\n walk(schema, '');\n return out;\n}\n\n/** Set `value` at the dotted `path` in `root` itself, creating missing branches. */\nexport function assignPrefValueAtPath(root: Record<string, unknown>, path: string, value: unknown): void {\n const parts = path.split('.');\n let cursor = root;\n for (let i = 0; i < parts.length - 1; i++) {\n cursor = (cursor[parts[i]!] ??= {}) as Record<string, unknown>;\n }\n cursor[parts[parts.length - 1]!] = value;\n}\n\n/** Each of `schema`'s leaves that `tree` holds a value for, as a path and\n * that value — the inverse of building a tree from per-leaf records. */\nexport function flattenPrefValues(schema: PrefGroup, tree: unknown): [string, unknown][] {\n const out: [string, unknown][] = [];\n for (const path of prefLeaves(schema).keys()) {\n const value = prefValueAtPath(tree, path);\n if (value !== undefined) out.push([path, value]);\n }\n return out;\n}\n\n/** Whether one leaf answers to a filter query, by name, description or path. */\nfunction prefLeafMatches(pref: PrefLeaf, path: string, query: string): boolean {\n if (pref.name.toLowerCase().includes(query)) return true;\n if (pref.description?.toLowerCase().includes(query)) return true;\n return path.toLowerCase().includes(query);\n}\n\n/**\n * Drop every leaf that does not answer to `query`, pruning groups left empty.\n * A group whose own name matches keeps all of its leaves — a reader who typed\n * the group's name is asking for the group, not for leaves repeating it.\n *\n * An empty or whitespace query matches everything, so a cleared field restores\n * the tree rather than emptying it.\n */\nexport function filterPrefSubtree<T extends PrefLeaf | PrefGroup>(\n node: T,\n query: string,\n path = '',\n): T | null {\n const q = query.trim().toLowerCase();\n if (q === '') return node;\n if (isPrefLeaf(node)) return prefLeafMatches(node, path, q) ? node : null;\n const group = node as PrefGroup;\n if (group.name.toLowerCase().includes(q)) return node;\n const children: Record<string, PrefLeaf | PrefGroup> = {};\n for (const [key, child] of Object.entries(group.children)) {\n const kept = filterPrefSubtree(child, q, path === '' ? key : `${path}.${key}`);\n if (kept) children[key] = kept;\n }\n if (Object.keys(children).length === 0) return null;\n return { ...node, children };\n}\n","import type { OwnedRecordCache } from '@weasel-js/storage';\nimport { VERSION_RECORD } from './helpers';\n\nconst isVersion = (v: unknown): v is number => typeof v === 'number' && Number.isInteger(v) && v >= 0;\n\n/** Takes stored records from one version to the next: rename, transform or\n * delete entries in `records`, keyed by leaf path. */\nexport type PrefsMigration = (records: Map<string, unknown>) => void;\n\n/**\n * Bring `cache` up to `migrations.length`, the current version. Every record\n * a migration leaves is written back, so a migration may replace values or\n * mutate them in place. Stops writing, and writes nothing, when the stored\n * version is newer than this build knows or a migration throws. Returns\n * whether it wrote.\n */\nexport function runPrefsMigrations(\n cache: OwnedRecordCache,\n migrations: readonly PrefsMigration[],\n): boolean {\n const target = migrations.length;\n const raw = cache.get(VERSION_RECORD);\n if (cache.has(VERSION_RECORD) && !isVersion(raw)) {\n console.warn(\n `[prefs] \"${cache.prefix}\" has an unreadable ${VERSION_RECORD} (${JSON.stringify(raw)}); opening read-only`,\n );\n cache.stopWriting();\n return false;\n }\n const stored = cache.has(VERSION_RECORD) ? (raw as number) : 0;\n if (stored > target) {\n console.warn(\n `[prefs] \"${cache.prefix}\" was written at version ${stored}, newer than this build's ${target}; opening read-only`,\n );\n cache.stopWriting();\n return false;\n }\n if (stored === target || !cache.writable) return false;\n\n const before = cache.entries().filter(([name]) => name !== VERSION_RECORD);\n const records = new Map(before.map(([name, value]): [string, unknown] => [name, structuredClone(value)]));\n for (let v = stored; v < target; v++) {\n try {\n migrations[v]!(records);\n } catch (error) {\n console.warn(\n `[prefs] migration ${v} → ${v + 1} for \"${cache.prefix}\" threw; opening read-only on the unmigrated records`,\n error,\n );\n cache.stopWriting();\n return false;\n }\n }\n for (const [name] of before) if (!records.has(name)) cache.delete(name);\n for (const [name, value] of records) {\n if (value === undefined) cache.delete(name);\n else cache.set(name, value);\n }\n cache.set(VERSION_RECORD, target);\n return true;\n}\n\n/**\n * Stop persisting when another writer, a newer build in another tab, records a\n * version past `target`: this build would otherwise keep writing the old shape.\n * Returns the unsubscribe.\n */\nexport function watchPrefsVersion(cache: OwnedRecordCache, target: number): () => void {\n return cache.subscribe((changes) => {\n for (const change of changes) {\n if (change.origin !== 'remote' || change.name !== VERSION_RECORD || change.value === undefined) continue;\n if (!isVersion(change.value)) {\n console.warn(\n `[prefs] \"${cache.prefix}\" has an unreadable ${VERSION_RECORD} (${JSON.stringify(change.value)}) from another writer; no longer persisting`,\n );\n cache.stopWriting();\n } else if (change.value > target) {\n console.warn(\n `[prefs] \"${cache.prefix}\" was migrated to version ${change.value} by another writer; this build knows ${target}; no longer persisting`,\n );\n cache.stopWriting();\n }\n }\n });\n}\n","import type { PrefBoolean, PrefEnum, PrefLeaf, PrefNumber, PrefObject } from './schema';\n\n/** Decides what a stored value reads as for one kind of leaf: the value to\n * read, or `undefined` for \"invalid, read the default\". */\nexport type PrefValidator = (stored: unknown, leaf: PrefLeaf) => unknown;\n\n/** What `stored` reads as for `leaf`. Never writes anything back: a schema\n * rolled back finds the original still in storage. */\nexport function repairPrefValue(\n leaf: PrefLeaf,\n stored: unknown,\n validators?: Readonly<Record<string, PrefValidator>>,\n): unknown {\n if (leaf.kind === 'number') {\n if (stored === 'Infinity') stored = Infinity;\n else if (stored === '-Infinity') stored = -Infinity;\n }\n const validate = validators?.[leaf.kind];\n if (validate) {\n try {\n const out = validate(stored, leaf);\n return out === undefined ? leaf.default : out;\n } catch {\n return leaf.default;\n }\n }\n switch (leaf.kind) {\n case 'number':\n return repairNumber(leaf as PrefNumber, stored);\n case 'boolean':\n return (leaf as PrefBoolean).encoding || typeof stored === 'boolean' ? stored : leaf.default;\n case 'string':\n case 'color':\n case 'field':\n return typeof stored === 'string' ? stored : leaf.default;\n case 'enum': {\n const e = leaf as PrefEnum;\n if (e.encoding) return stored;\n return e.options.some((o) => o.value === stored) ? stored : leaf.default;\n }\n case 'object':\n if ((leaf as PrefObject).fromScalar || isPlainObject(stored)) return stored;\n return leaf.default;\n default:\n return stored;\n }\n}\n\nfunction isPlainObject(v: unknown): boolean {\n return v !== null && typeof v === 'object' && !Array.isArray(v);\n}\n\nfunction repairNumber(leaf: PrefNumber, stored: unknown): unknown {\n if (typeof stored !== 'number' || Number.isNaN(stored)) return leaf.default;\n const { endless } = leaf;\n if (stored === Infinity && (endless === 'max' || endless === 'both')) return stored;\n if (stored === -Infinity && (endless === 'min' || endless === 'both')) return stored;\n let v = stored;\n if (leaf.min !== undefined && v < leaf.min) v = leaf.min;\n if (leaf.max !== undefined && v > leaf.max) v = leaf.max;\n return Number.isFinite(v) ? v : leaf.default;\n}\n","import type { OwnedRecordCache } from '@weasel-js/storage';\nimport { assignPrefValueAtPath, prefLeaves, VERSION_RECORD } from './helpers';\nimport type { PrefPath, PrefValueAt } from './paths';\nimport { type PrefValidator, repairPrefValue } from './repair';\nimport type { PrefGroup, PrefLeaf } from './schema';\n\n/** One leaf changing, as a store subscriber hears it: the value readers now\n * see, and whether this store or another writer made the change. */\nexport interface PrefChange {\n path: string;\n value: unknown;\n origin: 'local' | 'remote';\n}\n\n/** Values for one schema, held in memory and persisted behind. */\nexport interface PrefsStore<S extends PrefGroup> {\n readonly schema: S;\n /** The leaf's value, repaired against the schema; its default when unset. */\n get<P extends PrefPath<S>>(path: P): PrefValueAt<S, P>;\n set<P extends PrefPath<S>>(path: P, value: PrefValueAt<S, P>): void;\n /** Unset every leaf at or under `path`; with no path, all. */\n reset(path?: string): void;\n /** Whether the leaf has a value of its own rather than following its default. */\n isSet(path: PrefPath<S>): boolean;\n /** Every leaf's value as a nested tree. The same object until something changes. */\n values(): Record<string, unknown>;\n /** The leaves following their default. The same set until something changes. */\n unset(): ReadonlySet<string>;\n /** Every stored record as a nested tree, unrepaired, including records no\n * leaf describes. The same object until something changes. */\n stored(): Record<string, unknown>;\n /** `changes` is empty when only records no leaf describes changed. */\n subscribe(listener: (changes: PrefChange[]) => void): () => void;\n /** False when the store cannot persist: storage was unreadable, or written\n * by a newer schema. Changes still apply for this session. */\n readonly writable: boolean;\n /** Resolves true when every queued write reached storage. */\n flush(): Promise<boolean>;\n close(): Promise<void>;\n}\n\ninterface Snapshot {\n byPath: Map<string, unknown>;\n tree: Record<string, unknown>;\n unset: ReadonlySet<string>;\n}\n\n/** JSON storage cannot keep an infinity; `repairPrefValue` reads these back. */\nconst encodeInfinity = (leaf: PrefLeaf, value: unknown): unknown =>\n leaf.kind === 'number' && (value === Infinity || value === -Infinity) ? String(value) : value;\n\n/** A store over an open cache. `openPrefs` / `openPrefsSync` are the usual\n * way in; they run migrations first. */\nexport function createPrefsStore<S extends PrefGroup>(\n schema: S,\n cache: OwnedRecordCache,\n validators?: Readonly<Record<string, PrefValidator>>,\n onClose?: () => void,\n): PrefsStore<S> {\n const leaves = prefLeaves(schema);\n let snapshot: Snapshot | null = null;\n let previous: { raw: Map<string, unknown>; byPath: Map<string, unknown> } | null = null;\n\n const build = (): Snapshot => {\n const byPath = new Map<string, unknown>();\n const raw = new Map<string, unknown>();\n const unset = new Set<string>();\n const tree: Record<string, unknown> = {};\n for (const [path, leaf] of leaves) {\n const has = cache.has(path);\n let value: unknown;\n if (!has) {\n value = leaf.default;\n unset.add(path);\n } else {\n const stored = cache.get(path);\n raw.set(path, stored);\n value =\n previous?.raw.has(path) && previous.raw.get(path) === stored\n ? previous.byPath.get(path)\n : repairPrefValue(leaf, stored, validators);\n }\n byPath.set(path, value);\n assignPrefValueAtPath(tree, path, value);\n }\n previous = { raw, byPath };\n return { byPath, tree, unset };\n };\n const current = (): Snapshot => (snapshot ??= build());\n\n let storedTree: Record<string, unknown> | null = null;\n const buildStored = (): Record<string, unknown> => {\n const tree: Record<string, unknown> = {};\n const branches = new WeakSet<object>([tree]);\n for (const [name, value] of cache.entries()) {\n if (name === VERSION_RECORD) continue;\n const parts = name.split('.');\n let cursor = tree;\n for (const part of parts.slice(0, -1)) {\n const next = cursor[part];\n // A record's own value is never descended into: one named `a` and\n // another `a.b` cannot both show, and the branch wins.\n if (typeof next === 'object' && next !== null && branches.has(next)) {\n cursor = next as Record<string, unknown>;\n } else {\n const branch: Record<string, unknown> = {};\n branches.add(branch);\n cursor[part] = branch;\n cursor = branch;\n }\n }\n const last = parts[parts.length - 1]!;\n if (!branches.has(cursor[last] as object)) cursor[last] = value;\n }\n return tree;\n };\n\n const listeners = new Set<(changes: PrefChange[]) => void>();\n const stopCache = cache.subscribe((changes) => {\n const records = changes.filter((c) => c.name !== VERSION_RECORD);\n if (records.length === 0) return;\n storedTree = null;\n const mine = records.filter((c) => leaves.has(c.name));\n if (mine.length > 0) snapshot = null;\n const now = current();\n const out = mine.map((c): PrefChange => ({ path: c.name, value: now.byPath.get(c.name), origin: c.origin }));\n for (const listener of [...listeners]) listener(out);\n });\n\n return {\n schema,\n get: (path) => current().byPath.get(path) as never,\n set: (path, value) => {\n const leaf = leaves.get(path);\n if (!leaf) {\n console.warn(`[prefs] no leaf at \"${path}\"; ignoring the write`);\n return;\n }\n cache.set(path, encodeInfinity(leaf, value));\n },\n reset: (path) => {\n for (const p of leaves.keys()) {\n if (path === undefined || p === path || p.startsWith(`${path}.`)) cache.delete(p);\n }\n },\n isSet: (path) => cache.has(path),\n values: () => current().tree,\n unset: () => current().unset,\n stored: () => (storedTree ??= buildStored()),\n subscribe: (listener) => {\n listeners.add(listener);\n return () => {\n listeners.delete(listener);\n };\n },\n get writable() {\n return cache.writable;\n },\n flush: () => cache.flush(),\n close: async () => {\n onClose?.();\n stopCache();\n listeners.clear();\n await cache.close();\n },\n };\n}\n","import {\n openRecords,\n openRecordsSync,\n type OwnedRecordCache,\n type StorageAdapter,\n type SyncStorageAdapter,\n} from '@weasel-js/storage';\nimport { type PrefsMigration, runPrefsMigrations, watchPrefsVersion } from './migrate';\nimport type { PrefValidator } from './repair';\nimport type { PrefGroup } from './schema';\nimport { createPrefsStore, type PrefsStore } from './store';\n\n/** Options for `openPrefs` and `openPrefsSync`. */\nexport interface PrefsOptions {\n storage: StorageAdapter;\n /** Prepended to every record's key: `'myapp.prefs.'`. */\n prefix: string;\n /** `migrations[i]` takes stored records from version `i` to `i + 1`. The\n * current version is `migrations.length`. */\n migrations?: readonly PrefsMigration[];\n /** Validators for app-defined kinds, keyed by `kind`. One given for a\n * built-in kind replaces that kind's own rule. */\n validators?: Readonly<Record<string, PrefValidator>>;\n}\n\n/** Load every stored value for `schema`, migrate it, and return the store. */\nexport async function openPrefs<S extends PrefGroup>(\n schema: S,\n options: PrefsOptions,\n): Promise<PrefsStore<S>> {\n const cache = await openRecords({ storage: options.storage, prefix: options.prefix });\n const stop = watch(cache, options);\n if (runPrefsMigrations(cache, options.migrations ?? [])) await cache.flush();\n return createPrefsStore(schema, cache, options.validators, stop);\n}\n\n/** `openPrefs` for an adapter that reads synchronously: the store is ready\n * on return, so values can be read before anything renders. */\nexport function openPrefsSync<S extends PrefGroup>(\n schema: S,\n options: PrefsOptions & { storage: SyncStorageAdapter },\n): PrefsStore<S> {\n const cache = openRecordsSync({ storage: options.storage, prefix: options.prefix });\n const stop = watch(cache, options);\n if (runPrefsMigrations(cache, options.migrations ?? [])) void cache.flush();\n return createPrefsStore(schema, cache, options.validators, stop);\n}\n\n/** Attached before migrations run, so a peer's version bump during the open's\n * own flush is heard. */\nconst watch = (cache: OwnedRecordCache, options: PrefsOptions): (() => void) | undefined =>\n cache.writable ? watchPrefsVersion(cache, (options.migrations ?? []).length) : undefined;\n","// The preferences schema: groups of typed leaves. Tools declare theirs as a\n// `PrefGroup`; apps compose them into one registry, which `PrefsForm` renders\n// and `openPrefs` stores.\n\nimport { formatUnit, unitScale } from '@weasel-js/quantity';\nimport type { Display, InfinityText, Unit, UnitEntry, UnitScale, UnitSystem } from '@weasel-js/quantity';\n\n/** The value types a built-in pref leaf can hold. */\nexport type PrefKind =\n | 'number' | 'boolean' | 'string' | 'enum' | 'color' | 'paint' | 'object' | 'field';\n\n/**\n * Leaves that share one row in a compact property UI (weasel-ui\n * `SelectionPanel`, labkit's `ControlPanel`), declared on the first of them.\n * Purely presentational.\n */\nexport interface PrefPair {\n /** The other leaves on the row, by the full path their values are read and\n * written at (`'pose.y'`, `'camera.type'`), in order. Each must follow the\n * one before it among its siblings. */\n with: string | readonly string[];\n /** What the row reads. Default: the declaring leaf's `name`. */\n label?: string;\n}\n\n/** The fields every leaf pref shares, keyed by its `kind` and typed by its stored value. */\nexport interface PrefBase<K extends string, Value> {\n kind: K;\n /** Human-readable label. */\n name: string;\n /** Longer help text — shown in tooltips / a settings pane. */\n description: string;\n /** Fallback when nothing is persisted. */\n default: Value;\n /** Hide from a host app's settings UI by default. */\n hidden?: boolean;\n /** Render full-width with no label row in schema-driven settings UIs\n * (weasel-ui `PrefsForm` honors this for leaves whose control brings\n * its own chrome). */\n block?: boolean;\n /** Glyph naming this leaf in a host UI's icon set (weasel-ui resolves it\n * against `ICON_PATHS`). A plain string because this package ships no\n * icon set and cannot depend on one. Read where a leaf's `name` has nowhere to go — a\n * `pair`ed row is labeled by the pair, so its fields have only the glyph\n * to tell them apart. */\n icon?: string;\n /** Puts this leaf and the ones it names on one row. */\n pair?: PrefPair;\n /** Shorter forms of `name`, longest first. A surface short of room for\n * `name` — a strip of controls on one line, a segment in a `pair`ed row —\n * shows the longest of them that fits. `name` stays the accessible name, so\n * these abbreviate without costing anything. */\n short?: readonly string[];\n}\n\n/** How a schema-driven UI should present a number pref. */\nexport type PrefNumberControl = 'input' | 'slider';\n/** How a schema-driven UI should present a boolean pref. */\nexport type PrefBooleanControl = 'checkbox' | 'switch' | 'toggle';\n/** How a schema-driven UI should present a string pref. */\nexport type PrefStringControl = 'input' | 'textarea';\n/** How a schema-driven UI should present an enum pref. */\nexport type PrefEnumControl = 'select' | 'radio' | 'toggle';\n\n/** Display-unit conversion for number leaves whose stored value uses a\n * canonical unit the user shouldn't see (e.g. radians stored, degrees\n * shown). The stored value stays canonical; UIs convert at the edge. */\nexport interface PrefNumberUnit {\n toDisplay: (stored: number) => number;\n fromDisplay: (display: number) => number;\n /** Shown after the input, e.g. `'°'`. */\n suffix?: string;\n /** Suffixes a person may type, each mapped to the scale that turns a\n * number in that unit into a display number: `{ mm: 0.1, cm: 1 }` for a\n * field showing centimeters. A unit that disagrees with the display unit\n * about zero carries an `offset` as well. */\n accepts?: Readonly<Record<string, UnitEntry>>;\n /** The stored value as display text, suffix included. */\n format?: (stored: number) => string;\n}\n\n/**\n * A display unit built from a {@link UnitSystem}: values are stored in the\n * system's base and shown in `display`, rounded to `precision` places when\n * given, and every unit in the system is accepted as typed text. `suffix`\n * defaults to the unit's name and is accepted too.\n */\nexport function prefUnit(\n system: UnitSystem,\n display: Unit,\n opts?: { precision?: number; suffix?: string },\n): PrefNumberUnit {\n let self: Required<UnitScale>;\n try {\n self = unitScale(system, display);\n } catch (e) {\n throw new Error(`prefUnit: ${e instanceof Error ? e.message : String(e)}`);\n }\n const scale = opts?.precision === undefined ? undefined : 10 ** opts.precision;\n const suffix = opts?.suffix ?? display;\n const accepts: Record<string, UnitEntry> = { [suffix]: 1 };\n for (const name of Object.keys(system.units)) {\n const other = unitScale(system, name);\n // A number typed in `name` reaches display units through base:\n // `(n * factor + offset - self.offset) / self.factor`.\n const factor = other.factor / self.factor;\n const offset = (other.offset - self.offset) / self.factor;\n accepts[name] = offset === 0 ? factor : { factor, offset };\n }\n const toDisplay = (stored: number) => {\n const shown = (stored - self.offset) / self.factor;\n return scale === undefined ? shown : Math.round(shown * scale) / scale;\n };\n return {\n toDisplay,\n fromDisplay: (shown) => shown * self.factor + self.offset,\n suffix,\n accepts,\n format: (stored) =>\n `${formatUnit(stored, display, system, {\n precision: opts?.precision ?? 2,\n suffix: false,\n })}${suffix}`,\n };\n}\n\n/** A numeric pref, optionally bounded and stepped, and optionally stored in a\n * different unit from the one shown. */\nexport interface PrefNumber extends PrefBase<'number', number> {\n min?: number;\n max?: number;\n step?: number;\n control?: PrefNumberControl;\n /** How the value shows, speaks and reads back when typed — `compact()`\n * reads `2.00M`. Presentation only: the stored value stays a number. */\n display?: Display;\n unit?: PrefNumberUnit;\n /** Which end of a slider stands for infinity: the value there is ±Infinity. */\n endless?: 'min' | 'max' | 'both';\n /** The word infinity shows as — `'never'`, `'uncapped'`. Default `∞`. */\n infinity?: InfinityText;\n}\n/**\n * Stored-value bridge for a boolean leaf whose field is not a boolean — the\n * flag counterpart of {@link PrefEnumEncoding}. `TextStyle.fontStyle` is\n * the case: it stores `'italic'`, and the control is an Italic toggle.\n */\nexport interface PrefBooleanEncoding {\n /** Whether `stored` reads as on. */\n read: (stored: unknown, siblings: Record<string, unknown> | undefined) => boolean;\n /** What to store for `on`. `undefined` removes the field. */\n write: (on: boolean, siblings: Record<string, unknown> | undefined) => unknown;\n}\n\n/** An on/off pref. */\nexport interface PrefBoolean extends PrefBase<'boolean', boolean> {\n control?: PrefBooleanControl;\n encoding?: PrefBooleanEncoding;\n}\n/** A free-text pref. */\nexport interface PrefString extends PrefBase<'string', string> {\n control?: PrefStringControl;\n}\n/**\n * Stored-value bridge for an enum leaf whose value is not the option string —\n * the counterpart of {@link PrefNumberUnit}, which does the same for a\n * number stored in a canonical unit.\n *\n * A dash array is the case that needs it: `Stroke.dash` stores lengths, and\n * the thing a person chooses is a style. The presets scale by the stroke's\n * width, so both directions are given the object's other fields — a style is\n * meaningless without the width it is a multiple of.\n */\nexport interface PrefEnumEncoding<T extends string = string> {\n /**\n * The option `stored` reads as, or `undefined` for none — which a UI shows\n * the way it shows a mixed selection, by selecting nothing.\n *\n * `siblings` is the object the leaf is a field of, or `undefined` when the\n * node does not hold that object (and for a top-level leaf, which has none).\n */\n read: (stored: unknown, siblings: Record<string, unknown> | undefined) => T | undefined;\n /** What to store for `option`. `undefined` removes the field. */\n write: (option: T, siblings: Record<string, unknown> | undefined) => unknown;\n}\n\n/** A pref with a fixed set of labeled choices. `default` is `undefined` only\n * for a {@link PrefEnum.clearable} leaf, whose absence is a value. */\nexport interface PrefEnum<T extends string = string>\n extends PrefBase<'enum', T | undefined> {\n /**\n * The field may hold none of the options, and that absence is a state of\n * its own rather than a missing value — a script that is neither super nor\n * sub. A toggle control then lets the chosen segment be clicked off, which\n * removes the field.\n */\n clearable?: boolean;\n /** `short` holds shorter forms of `label`, longest first, for a segmented\n * control too narrow for the full one — down to a capital or two. `icon`\n * names a glyph in the host UI's set (weasel-ui resolves it against\n * `ICON_PATHS`), drawn in place of any text form where it resolves. It is\n * a plain string because this package ships no icon set and cannot\n * depend on one. The full `label` stays the accessible name, so neither the abbreviation nor the glyph becomes the only thing naming\n * the option.\n *\n * `disabled` marks an option a control reports but cannot author — the\n * value a stored form reads as when it matches nothing offered. Dropping it\n * from the list instead would leave the control selecting nothing and\n * claiming the field is unset. */\n options: readonly {\n value: T;\n label: string;\n short?: readonly string[];\n icon?: string;\n disabled?: boolean;\n }[];\n control?: PrefEnumControl;\n encoding?: PrefEnumEncoding<T>;\n}\n\n/** A single color, stored as a hex string. For a value that may also be a\n * gradient or a pattern, use {@link PrefPaint} instead. */\nexport interface PrefColor extends PrefBase<'color', string> {\n /** Value is `#rrggbb`, or `#rrggbbaa` when `alpha` is set (UIs then\n * offer an opacity control). */\n alpha?: boolean;\n}\n\n/**\n * Open leaf: any node with a `kind` outside the built-ins. Schema-driven\n * UIs (weasel-ui `PrefsForm` / `SelectionPanel`) dispatch it to an\n * app-supplied renderer. Deliberately NOT index-signatured so concrete\n * app interfaces stay assignable. Mirrors weasel-ui's `PrefCustom`.\n */\nexport type PrefCustom = PrefBase<string, unknown>;\n\n/**\n * A whole `FillStyle`, not a color inside one. Use it wherever the value is\n * the tagged paint union — a solid color, a pattern, a gradient — rather\n * than a hex string.\n *\n * Addressing `…fill.color` instead reads `undefined` off a gradient (so the\n * control shows its default and claims the text is black) and writes a\n * hybrid `{ fill: 'gradient', stops, color }` that the renderer's structural\n * `'color' in paint` checks then paint flat solid. The union has to be\n * edited as a union.\n */\nexport interface PrefPaint extends PrefBase<'paint', unknown> {\n /** Offer an opacity control alongside the color. */\n alpha?: boolean;\n}\n\n/**\n * A leaf whose value is one object, with its own fields hanging off it.\n *\n * A compound value — a stroke, a shadow, a pattern spec — could be described\n * as several sibling leaves addressing into it (`data.stroke.width`,\n * `data.stroke.cap`). It shouldn't be: each control would then write one field\n * of a value it can only half see, and writing into something that is not an\n * object yet corrupts it. Here the fields are `children` of one leaf, and\n * every edit commits the parent object whole.\n *\n * `children` paths are relative to the object. They are ordinary leaves, so a\n * field that is itself a union (a stroke's `paint`) declares the kind that\n * edits that union. A child may also be a {@link PrefGroup}, which\n * organises the fields under a heading without contributing to the path —\n * the same rule group keys follow at the top level. A `TextStyle` needs it:\n * its character and paragraph fields belong to one value but read as two\n * lists.\n */\nexport interface PrefObject extends PrefBase<'object', unknown> {\n children: Record<string, PrefLeaf | PrefGroup>;\n /**\n * Lift a non-object value into the object form, for a consumer field that\n * may also be held as a scalar. Called before a child edit is applied;\n * without it a scalar-valued leaf shows its children empty and refuses the\n * edit.\n */\n fromScalar?: (value: unknown) => Record<string, unknown>;\n}\n\n/**\n * A reference to another field of the same schema, held as the full path its\n * value is read and written at (`'camera.type'`) — the currency fields refer\n * to each other in. A surface offers the fields it draws as the choices.\n */\nexport interface PrefField extends PrefBase<'field', string> {\n /** Only fields of these kinds may be named. Default: any leaf. */\n kinds?: readonly string[];\n}\n\n/** One built-in pref leaf. `PrefLeaf` widens this to include\n * app-defined kinds. */\nexport type BuiltinPref =\n | PrefNumber\n | PrefBoolean\n | PrefString\n | PrefEnum\n | PrefColor\n | PrefPaint\n | PrefObject\n | PrefField;\n\n// Compile-time tie: every built-in leaf kind must appear in PrefKind\n// and vice versa (PrefBase's K is open for PrefCustom's sake, so\n// the union no longer enforces it).\ntype _BuiltinKindsExact = [BuiltinPref['kind']] extends [PrefKind]\n ? [PrefKind] extends [BuiltinPref['kind']] ? true : never\n : never;\nconst _builtinKindsExact: _BuiltinKindsExact = true;\nvoid _builtinKindsExact;\n\n/**\n * The built-in kinds, as a table. A kind added to {@link PrefKind} is a\n * compile error here, and from here it is one in every renderer's `never`\n * guard — the only thing standing between a new kind and rendering as\n * nothing in four places at once.\n */\nexport const PREF_KINDS: Record<PrefKind, true> = {\n number: true,\n boolean: true,\n string: true,\n enum: true,\n color: true,\n paint: true,\n object: true,\n field: true,\n};\n\n/** Which row each pairing among `leaves` puts a path on: a `key` the row's\n * members share (the declaring leaf's path) and the `label` it reads. A\n * path no `pair` names is absent. `leaves` are given by the path each\n * surface reads and writes them at, so references resolve in its terms. */\nexport function pairRowsOf(\n leaves: Iterable<readonly [path: string, leaf: PrefLeaf]>,\n): Map<string, { key: string; label: string }> {\n const rows = new Map<string, { key: string; label: string }>();\n for (const [path, leaf] of leaves) {\n if (!leaf.pair || rows.has(path)) continue;\n const row = { key: path, label: leaf.pair.label ?? leaf.name };\n rows.set(path, row);\n const partners = typeof leaf.pair.with === 'string' ? [leaf.pair.with] : leaf.pair.with;\n for (const partner of partners) if (!rows.has(partner)) rows.set(partner, row);\n }\n return rows;\n}\n\n/**\n * Narrows a leaf to the built-in union, so a renderer's switch discriminates\n * on {@link PrefKind} instead of the open `string` that\n * {@link PrefCustom} widens `kind` to. An app-defined kind answers false\n * and belongs to the renderer's custom-renderer path.\n */\nexport function isBuiltinPref(leaf: PrefLeaf): leaf is BuiltinPref {\n return Object.hasOwn(PREF_KINDS, leaf.kind);\n}\n\n/** Built-in or app-defined leaf. */\nexport type PrefLeaf = BuiltinPref | PrefCustom;\n\n/** Nestable group: branch nodes a tool can use to organize its prefs. */\nexport interface PrefGroup {\n /** Heading for the group's rows. **Empty means no heading** — for a group\n * that exists to organise, not to name: one whose children are themselves\n * groups carrying the labels a reader needs. Give it a name whenever the\n * name is the referent (a `Border` group over `Top` / `Right` / `Bottom`\n * reads as nothing without it). */\n name: string;\n description?: string;\n children: Record<string, PrefLeaf | PrefGroup>;\n}\n"]}
|
package/dist/react.d.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { a as PrefGroup, w as PrefPath, c as PrefsStore, z as PrefValueAt } from './store-BU2tRX5H.js';
|
|
2
|
+
import '@weasel-js/quantity';
|
|
3
|
+
|
|
4
|
+
/** One leaf of `store`, as React state. */
|
|
5
|
+
declare function usePref<S extends PrefGroup, P extends PrefPath<S>>(store: PrefsStore<S>, path: P): [
|
|
6
|
+
PrefValueAt<S, P>,
|
|
7
|
+
(next: PrefValueAt<S, P> | ((prev: PrefValueAt<S, P>) => PrefValueAt<S, P>)) => void
|
|
8
|
+
];
|
|
9
|
+
/** The whole of `store` as React state, shaped for `PrefsForm`: `values`,
|
|
10
|
+
* `onChange={set}`, `auto={unset}`, and `reset` for `onAutoChange`. `stored`
|
|
11
|
+
* is `store.stored()`. */
|
|
12
|
+
declare function usePrefsValues<S extends PrefGroup>(store: PrefsStore<S>): {
|
|
13
|
+
values: Record<string, unknown>;
|
|
14
|
+
set: (path: string, value: unknown) => void;
|
|
15
|
+
unset: ReadonlySet<string>;
|
|
16
|
+
stored: Record<string, unknown>;
|
|
17
|
+
reset: (path?: string) => void;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export { usePref, usePrefsValues };
|
package/dist/react.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { useCallback, useSyncExternalStore } from 'react';
|
|
2
|
+
|
|
3
|
+
// src/hooks.ts
|
|
4
|
+
function usePref(store, path) {
|
|
5
|
+
const subscribe = useCallback((onChange) => store.subscribe(onChange), [store]);
|
|
6
|
+
const getSnapshot = () => store.get(path);
|
|
7
|
+
const value = useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
|
|
8
|
+
const set = useCallback(
|
|
9
|
+
(next) => {
|
|
10
|
+
store.set(
|
|
11
|
+
path,
|
|
12
|
+
typeof next === "function" ? next(store.get(path)) : next
|
|
13
|
+
);
|
|
14
|
+
},
|
|
15
|
+
[store, path]
|
|
16
|
+
);
|
|
17
|
+
return [value, set];
|
|
18
|
+
}
|
|
19
|
+
function usePrefsValues(store) {
|
|
20
|
+
const subscribe = useCallback((onChange) => store.subscribe(onChange), [store]);
|
|
21
|
+
const values = useSyncExternalStore(subscribe, store.values, store.values);
|
|
22
|
+
const unset = useSyncExternalStore(subscribe, store.unset, store.unset);
|
|
23
|
+
const stored = useSyncExternalStore(subscribe, store.stored, store.stored);
|
|
24
|
+
const set = useCallback(
|
|
25
|
+
(path, value) => store.set(path, value),
|
|
26
|
+
[store]
|
|
27
|
+
);
|
|
28
|
+
const reset = useCallback((path) => store.reset(path), [store]);
|
|
29
|
+
return { values, set, unset, stored, reset };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export { usePref, usePrefsValues };
|
|
33
|
+
//# sourceMappingURL=react.js.map
|
|
34
|
+
//# sourceMappingURL=react.js.map
|
|
@@ -0,0 +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"]}
|
|
@@ -0,0 +1,353 @@
|
|
|
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 };
|
package/package.json
CHANGED
|
@@ -1,6 +1,59 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@weasel-js/prefs",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "1.9.3",
|
|
4
|
+
"description": "A preferences schema, a store that loads, validates, migrates and persists values against it, and React hooks over the store.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"sideEffects": false,
|
|
8
|
+
"main": "./dist/index.js",
|
|
9
|
+
"module": "./dist/index.js",
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"import": "./dist/index.js",
|
|
14
|
+
"types": "./dist/index.d.ts"
|
|
15
|
+
},
|
|
16
|
+
"./react": {
|
|
17
|
+
"import": "./dist/react.js",
|
|
18
|
+
"types": "./dist/react.d.ts"
|
|
19
|
+
},
|
|
20
|
+
"./package.json": "./package.json"
|
|
21
|
+
},
|
|
22
|
+
"author": "orochi235",
|
|
23
|
+
"homepage": "https://orochi235.github.io/weasel/",
|
|
24
|
+
"repository": {
|
|
25
|
+
"type": "git",
|
|
26
|
+
"url": "git+https://github.com/orochi235/weasel.git",
|
|
27
|
+
"directory": "packages/prefs"
|
|
28
|
+
},
|
|
29
|
+
"bugs": {
|
|
30
|
+
"url": "https://github.com/orochi235/weasel/issues"
|
|
31
|
+
},
|
|
32
|
+
"engines": {
|
|
33
|
+
"node": ">=22"
|
|
34
|
+
},
|
|
35
|
+
"files": [
|
|
36
|
+
"dist",
|
|
37
|
+
"README.md",
|
|
38
|
+
"LICENSE"
|
|
39
|
+
],
|
|
40
|
+
"scripts": {
|
|
41
|
+
"build": "tsup"
|
|
42
|
+
},
|
|
43
|
+
"dependencies": {
|
|
44
|
+
"@weasel-js/quantity": "1.9.3",
|
|
45
|
+
"@weasel-js/storage": "1.9.3"
|
|
46
|
+
},
|
|
47
|
+
"peerDependencies": {
|
|
48
|
+
"react": ">=18"
|
|
49
|
+
},
|
|
50
|
+
"peerDependenciesMeta": {
|
|
51
|
+
"react": {
|
|
52
|
+
"optional": true
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
"publishConfig": {
|
|
56
|
+
"access": "public",
|
|
57
|
+
"provenance": true
|
|
58
|
+
}
|
|
59
|
+
}
|