vultus-core 0.3.0 → 0.5.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.
Files changed (125) hide show
  1. package/README.md +319 -4
  2. package/dist/ArgStoreContext.d.ts +10 -0
  3. package/dist/ArgStoreContext.d.ts.map +1 -0
  4. package/dist/ArgStoreContext.js +15 -0
  5. package/dist/ArgStoreContext.js.map +1 -0
  6. package/dist/Components.d.ts +30 -0
  7. package/dist/Components.d.ts.map +1 -0
  8. package/dist/Components.js +28 -0
  9. package/dist/Components.js.map +1 -0
  10. package/dist/FieldContext.d.ts +38 -0
  11. package/dist/FieldContext.d.ts.map +1 -0
  12. package/dist/FieldContext.js +82 -0
  13. package/dist/FieldContext.js.map +1 -0
  14. package/dist/argStore.d.ts +26 -0
  15. package/dist/argStore.d.ts.map +1 -0
  16. package/dist/argStore.js +27 -0
  17. package/dist/argStore.js.map +1 -0
  18. package/dist/choices.d.ts +43 -0
  19. package/dist/choices.d.ts.map +1 -0
  20. package/dist/choices.js +20 -0
  21. package/dist/choices.js.map +1 -0
  22. package/dist/field.d.ts +42 -15
  23. package/dist/field.d.ts.map +1 -1
  24. package/dist/field.js +20 -7
  25. package/dist/field.js.map +1 -1
  26. package/dist/index.d.ts +22 -4
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/index.js +21 -3
  29. package/dist/index.js.map +1 -1
  30. package/dist/kinds.d.ts +51 -0
  31. package/dist/kinds.d.ts.map +1 -0
  32. package/dist/kinds.js +143 -0
  33. package/dist/kinds.js.map +1 -0
  34. package/dist/localStorageStore.d.ts +16 -0
  35. package/dist/localStorageStore.d.ts.map +1 -0
  36. package/dist/localStorageStore.js +72 -0
  37. package/dist/localStorageStore.js.map +1 -0
  38. package/dist/normalizeText.d.ts +6 -0
  39. package/dist/normalizeText.d.ts.map +1 -0
  40. package/dist/normalizeText.js +8 -0
  41. package/dist/normalizeText.js.map +1 -0
  42. package/dist/numeric.d.ts +38 -0
  43. package/dist/numeric.d.ts.map +1 -0
  44. package/dist/numeric.js +43 -0
  45. package/dist/numeric.js.map +1 -0
  46. package/dist/resolve.d.ts +13 -0
  47. package/dist/resolve.d.ts.map +1 -1
  48. package/dist/resolve.js +25 -0
  49. package/dist/resolve.js.map +1 -1
  50. package/dist/tree.d.ts +69 -0
  51. package/dist/tree.d.ts.map +1 -0
  52. package/dist/tree.js +151 -0
  53. package/dist/tree.js.map +1 -0
  54. package/dist/types.d.ts +15 -7
  55. package/dist/types.d.ts.map +1 -1
  56. package/dist/useArg.d.ts +45 -0
  57. package/dist/useArg.d.ts.map +1 -0
  58. package/dist/useArg.js +83 -0
  59. package/dist/useArg.js.map +1 -0
  60. package/dist/useBoolField.d.ts +8 -3
  61. package/dist/useBoolField.d.ts.map +1 -1
  62. package/dist/useBoolField.js +9 -1
  63. package/dist/useBoolField.js.map +1 -1
  64. package/dist/useManyOfField.d.ts +35 -0
  65. package/dist/useManyOfField.d.ts.map +1 -0
  66. package/dist/useManyOfField.js +74 -0
  67. package/dist/useManyOfField.js.map +1 -0
  68. package/dist/useMixedBoolField.d.ts +8 -3
  69. package/dist/useMixedBoolField.d.ts.map +1 -1
  70. package/dist/useMixedBoolField.js +9 -1
  71. package/dist/useMixedBoolField.js.map +1 -1
  72. package/dist/useNumericField.d.ts +25 -0
  73. package/dist/useNumericField.d.ts.map +1 -0
  74. package/dist/useNumericField.js +52 -0
  75. package/dist/useNumericField.js.map +1 -0
  76. package/dist/useOneOfField.d.ts +18 -34
  77. package/dist/useOneOfField.d.ts.map +1 -1
  78. package/dist/useOneOfField.js +18 -18
  79. package/dist/useOneOfField.js.map +1 -1
  80. package/dist/useRangeField.d.ts +34 -0
  81. package/dist/useRangeField.d.ts.map +1 -0
  82. package/dist/useRangeField.js +63 -0
  83. package/dist/useRangeField.js.map +1 -0
  84. package/dist/useStoredField.d.ts +61 -0
  85. package/dist/useStoredField.d.ts.map +1 -0
  86. package/dist/useStoredField.js +151 -0
  87. package/dist/useStoredField.js.map +1 -0
  88. package/dist/useTextField.d.ts +22 -0
  89. package/dist/useTextField.d.ts.map +1 -0
  90. package/dist/useTextField.js +21 -0
  91. package/dist/useTextField.js.map +1 -0
  92. package/dist/useTreeField.d.ts +39 -0
  93. package/dist/useTreeField.d.ts.map +1 -0
  94. package/dist/useTreeField.js +66 -0
  95. package/dist/useTreeField.js.map +1 -0
  96. package/dist/valueTypes.d.ts +28 -0
  97. package/dist/valueTypes.d.ts.map +1 -0
  98. package/dist/valueTypes.js +65 -0
  99. package/dist/valueTypes.js.map +1 -0
  100. package/package.json +1 -1
  101. package/src/ArgStoreContext.tsx +16 -0
  102. package/src/Components.tsx +47 -0
  103. package/src/FieldContext.tsx +98 -0
  104. package/src/argStore.ts +49 -0
  105. package/src/choices.ts +62 -0
  106. package/src/field.ts +64 -25
  107. package/src/index.ts +54 -4
  108. package/src/kinds.ts +197 -0
  109. package/src/localStorageStore.ts +68 -0
  110. package/src/normalizeText.ts +7 -0
  111. package/src/numeric.ts +68 -0
  112. package/src/resolve.ts +23 -0
  113. package/src/tree.ts +175 -0
  114. package/src/types.ts +28 -7
  115. package/src/useArg.ts +121 -0
  116. package/src/useBoolField.ts +19 -6
  117. package/src/useManyOfField.ts +107 -0
  118. package/src/useMixedBoolField.ts +19 -6
  119. package/src/useNumericField.ts +69 -0
  120. package/src/useOneOfField.ts +38 -58
  121. package/src/useRangeField.ts +86 -0
  122. package/src/useStoredField.ts +205 -0
  123. package/src/useTextField.ts +39 -0
  124. package/src/useTreeField.ts +108 -0
  125. package/src/valueTypes.ts +77 -0
package/README.md CHANGED
@@ -20,10 +20,22 @@ npm install vultus-core
20
20
 
21
21
  - `.` — the action model, exporting: the core `types`, `decision`, `resolve`,
22
22
  `parseApiError`, `routeApiError`, `ActionErrorContext` / `useActionErrorCtx`,
23
- `useAction`, `useActionList`, the fields (`useBoolField`, `useFieldCore` and
24
- their types), `InternalLinkContext` / `useInternalLink` /
25
- `InternalLinkComponent`, and `MessageSinkContext` / `useMessageSink` /
26
- `MessageSink`.
23
+ `useAction`, `useActionList`; the field context (`FieldContextProvider`,
24
+ `createFieldContext`, `useFieldContext`, `NO_FIELD_CONTEXT`); every field
25
+ kind's hook (`useBoolField`, `useMixedBoolField`, `useOneOfField`,
26
+ `useManyOfField`, `useNumericField`, `useRangeField`, `useTreeField`,
27
+ `useTextField`, `useFieldCore` and their types) and their shared helpers
28
+ (`resolveChoice`, `normalizeText`, `clamp`, `resolveNumeric`, the tree
29
+ helpers `normalizeTreeValue` / `coveredNodes` / `toggleTreeValue` /
30
+ `expandedTreeKeys` / `filterTree`); definitions as data (`FieldDefinition`,
31
+ `initialValueOf`, `valueTypeOf`, `sanitizeStored`); the argument layer
32
+ (the value types, `memoryStore`, `localStorageStore`, `ArgStoreProvider` /
33
+ `useArgStore`, `useArg`, `useArgWriter`, `setArg` / `resetArg`, `argOf`,
34
+ `useStoredField` / `useStoredFields`); `InternalLinkContext` /
35
+ `useInternalLink` / `InternalLinkComponent`, `MessageSinkContext` /
36
+ `useMessageSink` / `MessageSink`, and `VultusComponentsContext` /
37
+ `useVultusComponents` / `SpinnerScope` / `useSpinnerKey` /
38
+ `VultusComponents` / `SpinnerProps` / `SpinnerComponent`.
27
39
  - `./link` — `InternalLinkContext`, `useInternalLink`, and the
28
40
  `InternalLinkComponent` type on their own, for consumers that only need the
29
41
  link-injection seam.
@@ -71,6 +83,309 @@ boolean field (inside: `initialValue`, default the first enabled choice); `confi
71
83
  depend on the requested choice. vultus-antd's `OneOfSelect` and `OneOfSegmented` take the
72
84
  same field.
73
85
 
86
+ ## Field context
87
+
88
+ A field's options may depend on something only the app knows — a translation
89
+ function, the data a page is showing. `createFieldContext<Ctx>()` returns a
90
+ typed `Provider` (it's `FieldContextProvider` itself, just typed); the hooks
91
+ read the nearest one. Nested providers merge, the inner value winning, so an
92
+ app can provide `t` near the root and `items` further down the tree.
93
+
94
+ Every option that takes a `ValueOrFn<T, Ctx>` (`T | ((ctx: Ctx) => T)`) may be
95
+ a plain value or a function of the context. With a provider above, every
96
+ function receives the nearest provider's value, whatever parameters it
97
+ declares — `(ctx = {}) => ...` gets it too. With none, a function with no
98
+ declared parameter (`() => ...`) is called with nothing, so it runs without
99
+ needing a provider at all, while one that declares a parameter (`(ctx) =>
100
+ ...`) is a loud failure: an error naming the field, the option, and that no
101
+ `FieldContextProvider` is above it. This goes by the function's declared
102
+ parameter count, so a default parameter (`(ctx = {}) => ...`) escapes that
103
+ check and, with no provider, runs with its default. A function that returns
104
+ `undefined` counts as an absent option too, and falls back to the option's
105
+ own default exactly as leaving it out would (useful for a lookup that
106
+ misses, e.g. `(ctx) => ctx.items[id]`). Numeric's `format(n, ctx)` follows
107
+ the same rule for its second parameter.
108
+
109
+ The provided value is wrapped so that reading a key no provider gave throws,
110
+ naming the key and the keys that are present, and, when an option's
111
+ function reads it, the field and the option in front (`Field 'sec': option
112
+ 'label': Field context: 'items' was never provided (provided: t, lang)`) — a
113
+ forgotten value fails loudly instead of silently becoming `undefined`. A key
114
+ explicitly provided as `undefined` is fine. The wrapper lets test tooling and
115
+ printers probe the object without tripping the guard (`then`, `toJSON`,
116
+ `asymmetricMatch`, `$$typeof`, `nodeType`, `tagName`, `hasAttribute`,
117
+ `_isMockFunction`, any key starting with `@@`, and all symbol keys), so a
118
+ context value can be logged, pattern-matched or passed around during tests
119
+ without the guard misfiring.
120
+
121
+ ```tsx
122
+ const { Provider: ModuleContext } = createFieldContext<{ items: Item[] }>();
123
+
124
+ function Filters({ items }: { items: Item[] }) {
125
+ return (
126
+ <ModuleContext value={{ items }}>
127
+ <Categories />
128
+ </ModuleContext>
129
+ );
130
+ }
131
+
132
+ function Categories() {
133
+ const field = useManyOfField({ id: 'categories', choices: (ctx) => categoriesOf(ctx.items) });
134
+ // ...
135
+ }
136
+ ```
137
+
138
+ ## More kinds
139
+
140
+ Next to `useBoolField` and `useOneOfField`, the other field kinds. Each is
141
+ built the same way (`useFieldCore`, the same inside / local / commit
142
+ storage), so everything the Fields section above says about storage,
143
+ visibility, enabling and commits applies to all of them too.
144
+
145
+ ### many-of (`useManyOfField`)
146
+
147
+ The value is a list of choices (`OneOfChoice`, as one-of's), kept as a set:
148
+ order and duplicates don't matter to its meaning, and every request (through
149
+ `setValue` or `toggle`) is normalized to choice order without duplicates,
150
+ followed by the values the field already holds that are no longer among the
151
+ choices and that the request keeps, in their current order. `toggle` keeps
152
+ them, so a stored value naming a removed choice isn't silently dropped;
153
+ `setValue([])` clears them along with everything else. Controls add
154
+ `choices` (each with `selected`), `isSelected(value)` and `toggle(value)`. A request naming
155
+ an unknown choice, or flipping a disabled one, is refused with a
156
+ `console.warn` — the hook itself polices this, as `useOneOfField` already
157
+ does for its own `setValue`, not just the widget. Implicit initial value
158
+ (with no `initialValue` and no outside storage): the empty list.
159
+
160
+ ```ts
161
+ const field = useManyOfField({
162
+ id: 'tags',
163
+ choices: [{ value: 'urgent' }, { value: 'archived', disabled: true }],
164
+ });
165
+ field.toggle('urgent'); // field.isSelected('urgent') === true
166
+ ```
167
+
168
+ ### numeric (`useNumericField`)
169
+
170
+ The value is a single number. `min`, `max`, `step`, `integer` (moxb's
171
+ `onlyInteger`) and `unit` may each be a context function; `format(n, ctx)`
172
+ overrides the default display (the number followed by the unit, if any).
173
+ The shown value is always clamped into the bounds, then rounded when
174
+ `integer` (a stored `2.5` shows `3`), since a value from outside — a
175
+ passed-in `value`, a stored one — may fall outside them or be fractional
176
+ even though every request the hook itself makes fits; a non-finite request
177
+ (a widget that parsed `''`) is refused with a warning. `valueText` carries
178
+ the formatted value. Implicit initial value: `min` (or `0` if there is
179
+ none), clamped into the bounds.
180
+
181
+ ```ts
182
+ const field = useNumericField({ id: 'count', min: 0, max: 10, unit: 'pcs' });
183
+ field.valueText; // '0pcs'
184
+ ```
185
+
186
+ ### range (`useRangeField`)
187
+
188
+ The value is a `NumberRange`, `[from, to]` with `min <= from <= to <= max`;
189
+ it shares numeric's bounds and formatting, but `min` and `max` are required
190
+ here and `step` defaults to `1`. `valueText` joins the formatted ends with
191
+ an en dash ("2010/11–2025/26"), so a layout can put the whole range in the
192
+ field's label instead of next to it. A value from outside is clamped into
193
+ the bounds end by end, rounded when `integer`, then ordered. Implicit
194
+ initial value: `[min, max]`.
195
+
196
+ ```ts
197
+ const field = useRangeField({ id: 'years', min: 2010, max: 2026 });
198
+ field.valueText; // '2010–2026'
199
+ ```
200
+
201
+ ### tree (`useTreeField`)
202
+
203
+ The value is a list of node ids (`nodes`: `TreeNode`, a choice with
204
+ `children`), kept as a set. `selection` says which of the covered nodes the
205
+ value names: `'topmost'` (default; antd's `SHOW_PARENT`: a checked node
206
+ stands for its whole subtree), `'all'`, `'leaves'` (`SHOW_CHILD`), or
207
+ `'independent'` (`checkStrictly`, moxb's `strictSelect`: nodes are chosen on
208
+ their own, with no cascading). The hook normalizes the value under
209
+ `selection` on every render — with `'topmost'`, a node and all its checked
210
+ children collapse to the node alone — and exposes `expandedKeys` (which
211
+ nodes a widget opens at first, controlled by `expand`: `'top'` (default),
212
+ `'none'`, `'selected'`, `'all'`) and `toggle(value)` (refused, with a
213
+ warning, for an unknown or disabled node, as many-of's does).
214
+ `coveredNodes(nodes, value, selection)` expands a value to every node it
215
+ stands for — what a module matches an item's categories against. Search
216
+ over labels is accent- and case-insensitive (`normalizeText`); the matching
217
+ `filterTree(nodes, query)` follows antd TreeSelect's rule: a node whose own
218
+ label matches stays with its whole subtree, any other node stays if one of
219
+ its descendants matches (with only the children that stay), and the rest
220
+ is trimmed, leaving the nodes given unchanged. Implicit initial value: the
221
+ empty list.
222
+
223
+ ```ts
224
+ const field = useTreeField({
225
+ id: 'categories',
226
+ nodes: [{ value: 'a', children: [{ value: 'a1' }, { value: 'a2' }] }],
227
+ });
228
+ field.toggle('a'); // field.value === ['a'] — 'topmost' collapses the subtree to the parent
229
+ ```
230
+
231
+ ### text (`useTextField`)
232
+
233
+ The value is a string. `control: 'input' | 'textarea'` (default `'input'`)
234
+ is a hint for the widget, nothing else. A field inside the field or changed
235
+ locally (`onChange`) requests on every keystroke, as moxb's, sidedao's and
236
+ Taskmaster's text fields do; a field that `commits` holds requests back
237
+ until the widget decides (blur or Enter) — any commit-mode field does this,
238
+ not something specific to text. Highlighting the search words in a result
239
+ list is the app's job (e.g. quaesitor's `WithSearch`), not this hook's.
240
+ Implicit initial value: the empty string.
241
+
242
+ ```ts
243
+ const field = useTextField({ id: 'search', control: 'input' });
244
+ ```
245
+
246
+ ## Definitions as data
247
+
248
+ A `FieldDefinition` is a field's options in their "inside" form (no `value`
249
+ plus `onChange` or `commit`), tagged with a required `kind` — one of
250
+ `'boolean'`, `'mixed-boolean'`, `'oneOf'`, `'manyOf'`, `'numeric'`,
251
+ `'range'`, `'tree'`, `'text'` (the same strings `FieldControls.type` already
252
+ carries). `kind` stays optional on each hook's own options — a hook knows
253
+ its own kind, and throws if a definition names a different one — but is
254
+ required once a field becomes plain data: a module declares its filters as a
255
+ list of `FieldDefinition`s, independent of which hook or widget ends up
256
+ running each one.
257
+
258
+ A definer who doesn't care where the value lives gives only `initialValue`
259
+ (or nothing, for the kind's implicit default); whoever runs the field — a
260
+ hook directly, or `useStoredField` (see Arguments, below) — chooses the
261
+ storage. `initialValueOf(definition, ctx)` is every kind's default in one
262
+ place, and the entry point for it: what a definition with no `initialValue`
263
+ starts at, what a reset restores, and what "is it changed?" compares
264
+ against — `false`; the first enabled choice; the empty list; `min` (or `0`)
265
+ clamped into the bounds; `[min, max]`; the empty string for text; for a
266
+ tree, the value normalized under its `selection`. (The per-kind helpers it
267
+ is built on — `boolInitialValue`, `oneOfInitialValue`, `rangeInitialValue`
268
+ and the like — are exported too.) A hook computes its kind's default once,
269
+ when the field mounts, not on every render.
270
+
271
+ The `widget` hint (`WidgetHint = string | { name: string; [option]: unknown
272
+ }`) names which widget renders the field — `'segmented'`, or `{ name:
273
+ 'segmented', iconOnly: true }` with its display options — without
274
+ vultus-core knowing what a "segmented control" is; it's vultus-antd that
275
+ turns the name into a component, applying render-site overrides on top.
276
+ Each kind has a default widget when none is given; an unknown widget name
277
+ throws, in vultus-antd.
278
+
279
+ ```ts
280
+ const definitions: FieldDefinition[] = [
281
+ { kind: 'boolean', id: 'showArchived' },
282
+ { kind: 'numeric', id: 'count', min: 0, max: 10, widget: 'slider' },
283
+ ];
284
+ initialValueOf(definitions[0], {}); // false
285
+ ```
286
+
287
+ ## Arguments
288
+
289
+ Storage outside a field hook — a URL, localStorage — lives in vultus-core as
290
+ its own layer (ported from moxb's `url-arg`, without mobx), independent of
291
+ the field hooks: an argument is `{ key, valueType, default }`, kept in
292
+ whatever `ArgStore` is nearest.
293
+
294
+ **Value types** (`ValueType<T>`: `parse(raw)`, `format(value)`, `isEqual(a,
295
+ b)`) say how a value is kept as a string and compared; equality is the
296
+ type's own meaning (a list as a set, a range by both ends), used everywhere
297
+ two values of that type are compared — including whether a value is the
298
+ default, so it's left out of the store. `valueTypeOf(kind)` gives each field
299
+ kind's: `booleanType` (`'1'` / `'0'`), `stringType` (one-of, text),
300
+ `stringSetType` (many-of, tree — a sorted comma list; a *present but empty*
301
+ value reads as the empty list, which differs from the key being absent
302
+ altogether), `numberType`, `rangeType` (`from..to`). A mixed-boolean field
303
+ shows an aggregate of other data and has no value type:
304
+ `valueTypeOf('mixed-boolean')` throws, and so does storing one. A choice
305
+ value containing `,`, or an empty one, cannot be written to a list and
306
+ throws — that's the definer's mistake, not outside input. A stored string
307
+ that doesn't parse, or names a choice or node that no longer exists, *is*
308
+ outside input (an edited or stale URL): it never throws, it falls back to
309
+ the default (or is dropped from a list), with a `console.warn`.
310
+
311
+ **Stores.** `ArgStore` is `read(key)`, `write(changes, method)` (`method`:
312
+ `'push'` or `'replace'`; a store without history ignores it),
313
+ `subscribe(listener)`, and optionally `hrefFor(changes)`. vultus-core ships
314
+ `memoryStore()` (tests, demos) and `localStorageStore(prefix)` (keys
315
+ namespaced as `${prefix}.${key}`, guarded for SSR so creating one outside a
316
+ browser is a safe no-op; where the browser blocks storage or refuses a
317
+ write, a failing read reads as absent and a failing write is skipped, with
318
+ one `console.warn` per store); a URL store comes from a router package.
319
+ Create a store once — outside render, or in `useMemo` — not on every
320
+ render: each store is a new subscription target, so a fresh one per render
321
+ makes every hook below resubscribe.
322
+ `<ArgStoreProvider store={...}>` gives a subtree its store — nest providers
323
+ to put the URL around a page and localStorage around a dialog inside it. A
324
+ hook with no `ArgStoreProvider` above it throws, rather than silently
325
+ reading nothing.
326
+
327
+ **`useArg(definition)`** reads and writes one argument: `value`,
328
+ `isDefault`, `set(value, method?)`, `reset(method?)` and `hrefFor(value)`
329
+ (where the store supports it). An absent key reads as the default; writing a
330
+ value that equals the default removes the key instead of storing it.
331
+
332
+ **Batches.** `useArgWriter()` returns `apply(changes, method)` where each
333
+ change is `setArg(definition, value)` or `resetArg(definition)` — several
334
+ arguments change in one store write, one history entry (a "clear all"
335
+ button, a filter chip that also closes a detail view).
336
+
337
+ **Fields over arguments.** `argOf(definition, ctx)` is the argument a
338
+ definition's value is stored as — key = the definition's `id`, value type
339
+ from its kind, default = its resolved initial value — for a caller that
340
+ wants `useArg` directly instead of a field. `useStoredField(definition)`
341
+ runs a `FieldDefinition` as a stored field the same way: the argument's key
342
+ is the definition's `id`, its value type comes from the kind, its default
343
+ is `initialValueOf`. It returns `{ options, isDefault, reset }` — `options`
344
+ is the definition in its outside form (`value` plus `onChange`, ready for
345
+ the kind's hook), with the stored raw value made to fit the definition as
346
+ it is now (`sanitizeStored`: unknown choices or nodes dropped, numbers and
347
+ ranges clamped). A value that needed fixing up warns once per field and per raw
348
+ value — not again on every render, and not at all when the key is simply
349
+ absent (then the default applies, and an absent key is not outside input).
350
+ `useStoredFields(definitions)` answers for several definitions over one
351
+ store subscription: `isDefault(id)` (throws for an id not among the
352
+ definitions), `changedCount`, and `resetAll(method)` (one write for every
353
+ field). Being a summary, it never warns — only `useStoredField` on the
354
+ field itself does.
355
+
356
+ ```tsx
357
+ const store = memoryStore();
358
+
359
+ function Filters() {
360
+ return (
361
+ <ArgStoreProvider store={store}>
362
+ <Category />
363
+ </ArgStoreProvider>
364
+ );
365
+ }
366
+
367
+ function Category() {
368
+ const { options, reset } = useStoredField({ kind: 'manyOf', id: 'tags', choices: [{ value: 'urgent' }] });
369
+ const field = useManyOfField(options);
370
+ // ...
371
+ }
372
+ ```
373
+
374
+ ## Components
375
+
376
+ `VultusComponentsContext` (default `{}`) / `useVultusComponents()` is how an app lends its own
377
+ UI to vultus/optio in place of their native indicators — today just `Spinner` (`SpinnerProps`:
378
+ `spinnerKey`, `size?`, `label?`; `VultusComponents.Spinner?: SpinnerComponent`). vultus-antd's
379
+ `VultusProvider` merges a `components` prop into the outer context value; without one, the
380
+ outer value (or this default) applies, as `linkComponent` does today.
381
+
382
+ Every call site that may render a lent `Spinner` asks for its key through `useSpinnerKey(localKey)`
383
+ rather than hardcoding it, so an app can scope a whole subtree's keys to one instance:
384
+ `<SpinnerScope name="optio:process:p1">` makes every `useSpinnerKey` call inside it return
385
+ `'optio:process:p1/' + localKey`; scopes nest by joining names with `/`, outermost first.
386
+ Library-local keys are namespaced (`vultus:...`, `optio:...`) so they never collide with an
387
+ app's own keys.
388
+
74
389
  ## Distribution
75
390
 
76
391
  This package ships compiled ESM (`dist/`) with TypeScript declarations to
@@ -0,0 +1,10 @@
1
+ import { type ReactNode } from 'react';
2
+ import type { ArgStore } from './argStore.js';
3
+ /** Gives the subtree below an ArgStore (Praeceptor: the URL store around the module page, localStorage around the print dialog). */
4
+ export declare function ArgStoreProvider({ store, children }: {
5
+ store: ArgStore;
6
+ children?: ReactNode;
7
+ }): import("react").JSX.Element;
8
+ /** The nearest ArgStoreProvider's store; a hook with none above it fails loudly rather than reading nothing. */
9
+ export declare function useArgStore(): ArgStore;
10
+ //# sourceMappingURL=ArgStoreContext.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ArgStoreContext.d.ts","sourceRoot":"","sources":["../src/ArgStoreContext.tsx"],"names":[],"mappings":"AAAA,OAAO,EAA6B,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAClE,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAI9C,oIAAoI;AACpI,wBAAgB,gBAAgB,CAAC,EAAE,KAAK,EAAE,QAAQ,EAAE,EAAE;IAAE,KAAK,EAAE,QAAQ,CAAC;IAAC,QAAQ,CAAC,EAAE,SAAS,CAAA;CAAE,+BAE9F;AAED,gHAAgH;AAChH,wBAAgB,WAAW,IAAI,QAAQ,CAItC"}
@@ -0,0 +1,15 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { createContext, useContext } from 'react';
3
+ const ArgStoreContext = createContext(undefined);
4
+ /** Gives the subtree below an ArgStore (Praeceptor: the URL store around the module page, localStorage around the print dialog). */
5
+ export function ArgStoreProvider({ store, children }) {
6
+ return _jsx(ArgStoreContext.Provider, { value: store, children: children });
7
+ }
8
+ /** The nearest ArgStoreProvider's store; a hook with none above it fails loudly rather than reading nothing. */
9
+ export function useArgStore() {
10
+ const store = useContext(ArgStoreContext);
11
+ if (store === undefined)
12
+ throw new Error('No ArgStoreProvider is above this component');
13
+ return store;
14
+ }
15
+ //# sourceMappingURL=ArgStoreContext.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ArgStoreContext.js","sourceRoot":"","sources":["../src/ArgStoreContext.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,aAAa,EAAE,UAAU,EAAkB,MAAM,OAAO,CAAC;AAGlE,MAAM,eAAe,GAAG,aAAa,CAAuB,SAAS,CAAC,CAAC;AAEvE,oIAAoI;AACpI,MAAM,UAAU,gBAAgB,CAAC,EAAE,KAAK,EAAE,QAAQ,EAA6C;IAC7F,OAAO,KAAC,eAAe,CAAC,QAAQ,IAAC,KAAK,EAAE,KAAK,YAAG,QAAQ,GAA4B,CAAC;AACvF,CAAC;AAED,gHAAgH;AAChH,MAAM,UAAU,WAAW;IACzB,MAAM,KAAK,GAAG,UAAU,CAAC,eAAe,CAAC,CAAC;IAC1C,IAAI,KAAK,KAAK,SAAS;QAAE,MAAM,IAAI,KAAK,CAAC,6CAA6C,CAAC,CAAC;IACxF,OAAO,KAAK,CAAC;AACf,CAAC"}
@@ -0,0 +1,30 @@
1
+ import { type ComponentType, type ReactNode } from 'react';
2
+ /** A spinner an app lends to vultus/optio, rendered in place of their native loading indicators. */
3
+ export interface SpinnerProps {
4
+ /** The spinner's identity: stable across re-renders and remounts, unique on the page (after scoping). */
5
+ spinnerKey: string;
6
+ /** antd's names (the height of antd's Spin indicator) or a height in px. */
7
+ size?: 'small' | 'default' | 'large' | number;
8
+ /** Accessible label; the spinner's own default when omitted. */
9
+ label?: string;
10
+ }
11
+ export type SpinnerComponent = ComponentType<SpinnerProps>;
12
+ /** Components an app lends to the libraries. */
13
+ export interface VultusComponents {
14
+ Spinner?: SpinnerComponent;
15
+ }
16
+ /**
17
+ * The components an app lends to vultus/optio in place of their own UI. Default is `{}`
18
+ * (none lent) so vultus-core stays framework-neutral; vultus-antd's `VultusProvider` merges
19
+ * in whatever an app passes as `components`.
20
+ */
21
+ export declare const VultusComponentsContext: import("react").Context<VultusComponents>;
22
+ export declare function useVultusComponents(): VultusComponents;
23
+ /** Prefixes every spinner key below with `name` (scopes nest: outer/inner). */
24
+ export declare function SpinnerScope({ name, children }: {
25
+ name: string;
26
+ children?: ReactNode;
27
+ }): import("react").JSX.Element;
28
+ /** The scoped key for a library-local key: 'outer/inner/local', or 'local' outside any scope. */
29
+ export declare function useSpinnerKey(localKey: string): string;
30
+ //# sourceMappingURL=Components.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Components.d.ts","sourceRoot":"","sources":["../src/Components.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAsC,KAAK,aAAa,EAAE,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAE/F,oGAAoG;AACpG,MAAM,WAAW,YAAY;IAC3B,yGAAyG;IACzG,UAAU,EAAE,MAAM,CAAC;IACnB,4EAA4E;IAC5E,IAAI,CAAC,EAAE,OAAO,GAAG,SAAS,GAAG,OAAO,GAAG,MAAM,CAAC;IAC9C,gEAAgE;IAChE,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AACD,MAAM,MAAM,gBAAgB,GAAG,aAAa,CAAC,YAAY,CAAC,CAAC;AAE3D,gDAAgD;AAChD,MAAM,WAAW,gBAAgB;IAC/B,OAAO,CAAC,EAAE,gBAAgB,CAAC;CAC5B;AAED;;;;GAIG;AACH,eAAO,MAAM,uBAAuB,2CAAsC,CAAC;AAE3E,wBAAgB,mBAAmB,IAAI,gBAAgB,CAEtD;AAKD,+EAA+E;AAC/E,wBAAgB,YAAY,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,EAAE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,SAAS,CAAA;CAAE,+BAOtF;AAED,iGAAiG;AACjG,wBAAgB,aAAa,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAGtD"}
@@ -0,0 +1,28 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { createContext, useContext, useMemo } from 'react';
3
+ /**
4
+ * The components an app lends to vultus/optio in place of their own UI. Default is `{}`
5
+ * (none lent) so vultus-core stays framework-neutral; vultus-antd's `VultusProvider` merges
6
+ * in whatever an app passes as `components`.
7
+ */
8
+ export const VultusComponentsContext = createContext({});
9
+ export function useVultusComponents() {
10
+ return useContext(VultusComponentsContext);
11
+ }
12
+ /** The scope path (segment names, outermost first) that `useSpinnerKey` prefixes onto local keys. */
13
+ const SpinnerScopeContext = createContext([]);
14
+ /** Prefixes every spinner key below with `name` (scopes nest: outer/inner). */
15
+ export function SpinnerScope({ name, children }) {
16
+ const outer = useContext(SpinnerScopeContext);
17
+ // Memoized so consumers below (useSpinnerKey, via SpinnerScopeContext) don't re-render
18
+ // on every SpinnerScope re-render — only when the scope path actually changes. Matters
19
+ // once whole conversation views sit inside a SpinnerScope.
20
+ const scope = useMemo(() => [...outer, name], [outer, name]);
21
+ return _jsx(SpinnerScopeContext.Provider, { value: scope, children: children });
22
+ }
23
+ /** The scoped key for a library-local key: 'outer/inner/local', or 'local' outside any scope. */
24
+ export function useSpinnerKey(localKey) {
25
+ const scope = useContext(SpinnerScopeContext);
26
+ return [...scope, localKey].join('/');
27
+ }
28
+ //# sourceMappingURL=Components.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Components.js","sourceRoot":"","sources":["../src/Components.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,aAAa,EAAE,UAAU,EAAE,OAAO,EAAsC,MAAM,OAAO,CAAC;AAkB/F;;;;GAIG;AACH,MAAM,CAAC,MAAM,uBAAuB,GAAG,aAAa,CAAmB,EAAE,CAAC,CAAC;AAE3E,MAAM,UAAU,mBAAmB;IACjC,OAAO,UAAU,CAAC,uBAAuB,CAAC,CAAC;AAC7C,CAAC;AAED,qGAAqG;AACrG,MAAM,mBAAmB,GAAG,aAAa,CAAW,EAAE,CAAC,CAAC;AAExD,+EAA+E;AAC/E,MAAM,UAAU,YAAY,CAAC,EAAE,IAAI,EAAE,QAAQ,EAA0C;IACrF,MAAM,KAAK,GAAG,UAAU,CAAC,mBAAmB,CAAC,CAAC;IAC9C,uFAAuF;IACvF,uFAAuF;IACvF,2DAA2D;IAC3D,MAAM,KAAK,GAAG,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC,GAAG,KAAK,EAAE,IAAI,CAAC,EAAE,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC,CAAC;IAC7D,OAAO,KAAC,mBAAmB,CAAC,QAAQ,IAAC,KAAK,EAAE,KAAK,YAAG,QAAQ,GAAgC,CAAC;AAC/F,CAAC;AAED,iGAAiG;AACjG,MAAM,UAAU,aAAa,CAAC,QAAgB;IAC5C,MAAM,KAAK,GAAG,UAAU,CAAC,mBAAmB,CAAC,CAAC;IAC9C,OAAO,CAAC,GAAG,KAAK,EAAE,QAAQ,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AACxC,CAAC"}
@@ -0,0 +1,38 @@
1
+ import { type JSX, type ReactNode } from 'react';
2
+ /**
3
+ * What useFieldContext gives when no FieldContextProvider is above: a field's function that
4
+ * declares a parameter then has nothing to receive, and resolving it throws.
5
+ */
6
+ export declare const NO_FIELD_CONTEXT: unique symbol;
7
+ /** The field context as the hooks hold it: the guarded value, or the mark that there is none. */
8
+ export type FieldCtx = object | typeof NO_FIELD_CONTEXT;
9
+ /**
10
+ * Runs `read`, a call of a definition's function with the field context. A key it reads that
11
+ * no provider gave throws with `where` (`Field '<id>': option '<name>'`) in front of the
12
+ * guard's message, so the error names the field and the option as well as the key; any other
13
+ * error passes through unchanged.
14
+ */
15
+ export declare function readingContext<T>(where: string, read: () => T): T;
16
+ /**
17
+ * Gives the fields below a context, the argument their definitions' functions receive. Nested
18
+ * providers merge, the inner value winning: an app can provide `t` near the root and the data
19
+ * a page shows further down.
20
+ */
21
+ export declare function FieldContextProvider({ value, children }: {
22
+ value: object;
23
+ children?: ReactNode;
24
+ }): JSX.Element;
25
+ /**
26
+ * A FieldContextProvider typed for the context `Ctx` an app's definitions are written against.
27
+ * It is the same provider (one React context), so typed providers nest and merge with each
28
+ * other and with the untyped one; each gives part of the context.
29
+ */
30
+ export declare function createFieldContext<Ctx extends object>(): {
31
+ Provider: (props: {
32
+ value: Partial<Ctx>;
33
+ children?: ReactNode;
34
+ }) => JSX.Element;
35
+ };
36
+ /** The field context the hooks resolve functions with: the guarded value, or NO_FIELD_CONTEXT. */
37
+ export declare function useFieldContext(): FieldCtx;
38
+ //# sourceMappingURL=FieldContext.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"FieldContext.d.ts","sourceRoot":"","sources":["../src/FieldContext.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAsC,KAAK,GAAG,EAAE,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAErF;;;GAGG;AACH,eAAO,MAAM,gBAAgB,EAAE,OAAO,MAAwC,CAAC;AAE/E,iGAAiG;AACjG,MAAM,MAAM,QAAQ,GAAG,MAAM,GAAG,OAAO,gBAAgB,CAAC;AA2CxD;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC,GAAG,CAAC,CAOjE;AAED;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,EAAE,KAAK,EAAE,QAAQ,EAAE,EAAE;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,SAAS,CAAA;CAAE,eAShG;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,SAAS,MAAM,KAAK;IACxD,QAAQ,EAAE,CAAC,KAAK,EAAE;QAAE,KAAK,EAAE,OAAO,CAAC,GAAG,CAAC,CAAC;QAAC,QAAQ,CAAC,EAAE,SAAS,CAAA;KAAE,KAAK,GAAG,CAAC,OAAO,CAAC;CACjF,CAEA;AAED,kGAAkG;AAClG,wBAAgB,eAAe,IAAI,QAAQ,CAE1C"}
@@ -0,0 +1,82 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { createContext, useContext, useMemo } from 'react';
3
+ /**
4
+ * What useFieldContext gives when no FieldContextProvider is above: a field's function that
5
+ * declares a parameter then has nothing to receive, and resolving it throws.
6
+ */
7
+ export const NO_FIELD_CONTEXT = Symbol('vultus.noFieldContext');
8
+ const FieldContext = createContext(undefined);
9
+ /**
10
+ * Keys that libraries read to find out what an object is (a thenable, JSON, a test matcher, a
11
+ * React element, a DOM node or element, a mock function), and the `@@`-prefixed sentinels
12
+ * (Immutable's) that printers such as pretty-format test for. Reading them on the context is a
13
+ * probe, not a mistake.
14
+ */
15
+ const PROBED_KEYS = new Set([
16
+ 'then', 'toJSON', 'asymmetricMatch', '$$typeof', 'nodeType', 'tagName', 'hasAttribute', '_isMockFunction',
17
+ ]);
18
+ function isProbe(key) {
19
+ return PROBED_KEYS.has(key) || key.startsWith('@@');
20
+ }
21
+ /** What the guard throws for a key no provider gave, so readingContext can tell it from the function's own errors. */
22
+ class MissingKeyError extends Error {
23
+ }
24
+ /**
25
+ * The context value as the definitions' functions receive it: reading a key that no provider
26
+ * gave throws, naming it and the keys present, so a forgotten value fails loudly instead of
27
+ * passing `undefined` along. A key provided with the value `undefined` is fine.
28
+ */
29
+ function guard(raw) {
30
+ return new Proxy(raw, {
31
+ get(target, key, receiver) {
32
+ if (typeof key === 'string' && !isProbe(key) && !(key in target)) {
33
+ throw new MissingKeyError(`Field context: '${key}' was never provided (provided: ${Object.keys(target).join(', ')})`);
34
+ }
35
+ return Reflect.get(target, key, receiver);
36
+ },
37
+ });
38
+ }
39
+ /**
40
+ * Runs `read`, a call of a definition's function with the field context. A key it reads that
41
+ * no provider gave throws with `where` (`Field '<id>': option '<name>'`) in front of the
42
+ * guard's message, so the error names the field and the option as well as the key; any other
43
+ * error passes through unchanged.
44
+ */
45
+ export function readingContext(where, read) {
46
+ try {
47
+ return read();
48
+ }
49
+ catch (error) {
50
+ if (error instanceof MissingKeyError)
51
+ throw new Error(`${where}: ${error.message}`, { cause: error });
52
+ throw error;
53
+ }
54
+ }
55
+ /**
56
+ * Gives the fields below a context, the argument their definitions' functions receive. Nested
57
+ * providers merge, the inner value winning: an app can provide `t` near the root and the data
58
+ * a page shows further down.
59
+ */
60
+ export function FieldContextProvider({ value, children }) {
61
+ const outer = useContext(FieldContext);
62
+ // Memoized on the outer context and `value`: a caller passing a fresh object literal gets a
63
+ // new context every render anyway; a stable `value` keeps the fields below from re-rendering.
64
+ const provided = useMemo(() => {
65
+ const raw = { ...outer?.raw, ...value };
66
+ return { raw, guarded: guard(raw) };
67
+ }, [outer, value]);
68
+ return _jsx(FieldContext.Provider, { value: provided, children: children });
69
+ }
70
+ /**
71
+ * A FieldContextProvider typed for the context `Ctx` an app's definitions are written against.
72
+ * It is the same provider (one React context), so typed providers nest and merge with each
73
+ * other and with the untyped one; each gives part of the context.
74
+ */
75
+ export function createFieldContext() {
76
+ return { Provider: FieldContextProvider };
77
+ }
78
+ /** The field context the hooks resolve functions with: the guarded value, or NO_FIELD_CONTEXT. */
79
+ export function useFieldContext() {
80
+ return useContext(FieldContext)?.guarded ?? NO_FIELD_CONTEXT;
81
+ }
82
+ //# sourceMappingURL=FieldContext.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"FieldContext.js","sourceRoot":"","sources":["../src/FieldContext.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,aAAa,EAAE,UAAU,EAAE,OAAO,EAA4B,MAAM,OAAO,CAAC;AAErF;;;GAGG;AACH,MAAM,CAAC,MAAM,gBAAgB,GAAkB,MAAM,CAAC,uBAAuB,CAAC,CAAC;AAW/E,MAAM,YAAY,GAAG,aAAa,CAAuB,SAAS,CAAC,CAAC;AAEpE;;;;;GAKG;AACH,MAAM,WAAW,GAAG,IAAI,GAAG,CAAC;IAC1B,MAAM,EAAE,QAAQ,EAAE,iBAAiB,EAAE,UAAU,EAAE,UAAU,EAAE,SAAS,EAAE,cAAc,EAAE,iBAAiB;CAC1G,CAAC,CAAC;AAEH,SAAS,OAAO,CAAC,GAAW;IAC1B,OAAO,WAAW,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,GAAG,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC;AACtD,CAAC;AAED,sHAAsH;AACtH,MAAM,eAAgB,SAAQ,KAAK;CAAG;AAEtC;;;;GAIG;AACH,SAAS,KAAK,CAAC,GAAW;IACxB,OAAO,IAAI,KAAK,CAAC,GAAG,EAAE;QACpB,GAAG,CAAC,MAAM,EAAE,GAAG,EAAE,QAAQ;YACvB,IAAI,OAAO,GAAG,KAAK,QAAQ,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,GAAG,IAAI,MAAM,CAAC,EAAE,CAAC;gBACjE,MAAM,IAAI,eAAe,CAAC,mBAAmB,GAAG,mCAAmC,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;YACxH,CAAC;YACD,OAAO,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,GAAG,EAAE,QAAQ,CAAC,CAAC;QAC5C,CAAC;KACF,CAAC,CAAC;AACL,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,cAAc,CAAI,KAAa,EAAE,IAAa;IAC5D,IAAI,CAAC;QACH,OAAO,IAAI,EAAE,CAAC;IAChB,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,IAAI,KAAK,YAAY,eAAe;YAAE,MAAM,IAAI,KAAK,CAAC,GAAG,KAAK,KAAK,KAAK,CAAC,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,CAAC;QACtG,MAAM,KAAK,CAAC;IACd,CAAC;AACH,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,oBAAoB,CAAC,EAAE,KAAK,EAAE,QAAQ,EAA2C;IAC/F,MAAM,KAAK,GAAG,UAAU,CAAC,YAAY,CAAC,CAAC;IACvC,4FAA4F;IAC5F,8FAA8F;IAC9F,MAAM,QAAQ,GAAG,OAAO,CAAC,GAAG,EAAE;QAC5B,MAAM,GAAG,GAAG,EAAE,GAAG,KAAK,EAAE,GAAG,EAAE,GAAG,KAAK,EAAE,CAAC;QACxC,OAAO,EAAE,GAAG,EAAE,OAAO,EAAE,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC;IACtC,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC,CAAC;IACnB,OAAO,KAAC,YAAY,CAAC,QAAQ,IAAC,KAAK,EAAE,QAAQ,YAAG,QAAQ,GAAyB,CAAC;AACpF,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,kBAAkB;IAGhC,OAAO,EAAE,QAAQ,EAAE,oBAAoB,EAAE,CAAC;AAC5C,CAAC;AAED,kGAAkG;AAClG,MAAM,UAAU,eAAe;IAC7B,OAAO,UAAU,CAAC,YAAY,CAAC,EAAE,OAAO,IAAI,gBAAgB,CAAC;AAC/D,CAAC"}
@@ -0,0 +1,26 @@
1
+ /** Whether a write pushes a new history entry or replaces the current one; a store without history ignores it. */
2
+ export type UpdateMethod = 'push' | 'replace';
3
+ /** One key's new raw value, or `undefined` to remove it (the key reads back as its default). */
4
+ export interface RawChange {
5
+ key: string;
6
+ raw: string | undefined;
7
+ }
8
+ /**
9
+ * A store for argument values kept as strings (moxb's url-arg backends, batched): a URL query,
10
+ * localStorage, or (here) memory. `read` and `write` move raw strings only — parsing, defaults
11
+ * and equality live in the value type and in useArg, not here. The hooks call its methods on
12
+ * the store, never detached, so a store may be a class whose methods use `this`.
13
+ */
14
+ export interface ArgStore {
15
+ read(key: string): string | undefined;
16
+ /** Applies every change, then notifies subscribers once — several keys changed together are one history entry. */
17
+ write(changes: RawChange[], method: UpdateMethod): void;
18
+ subscribe(listener: () => void): () => void;
19
+ /** The href a write of `changes` would produce, where the store has one (a URL store); omitted otherwise. */
20
+ hrefFor?(changes: RawChange[]): string;
21
+ }
22
+ /** An ArgStore over a plain object, for tests and demos. `snapshot()` is a copy, for assertions. */
23
+ export declare function memoryStore(initial?: Record<string, string>): ArgStore & {
24
+ snapshot(): Record<string, string>;
25
+ };
26
+ //# sourceMappingURL=argStore.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"argStore.d.ts","sourceRoot":"","sources":["../src/argStore.ts"],"names":[],"mappings":"AAAA,kHAAkH;AAClH,MAAM,MAAM,YAAY,GAAG,MAAM,GAAG,SAAS,CAAC;AAE9C,gGAAgG;AAChG,MAAM,WAAW,SAAS;IACxB,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,GAAG,SAAS,CAAC;CACzB;AAED;;;;;GAKG;AACH,MAAM,WAAW,QAAQ;IACvB,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAAC;IACtC,kHAAkH;IAClH,KAAK,CAAC,OAAO,EAAE,SAAS,EAAE,EAAE,MAAM,EAAE,YAAY,GAAG,IAAI,CAAC;IACxD,SAAS,CAAC,QAAQ,EAAE,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC;IAC5C,6GAA6G;IAC7G,OAAO,CAAC,CAAC,OAAO,EAAE,SAAS,EAAE,GAAG,MAAM,CAAC;CACxC;AAED,oGAAoG;AACpG,wBAAgB,WAAW,CAAC,OAAO,GAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAM,GAAG,QAAQ,GAAG;IAAE,QAAQ,IAAI,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAAE,CAuBnH"}
@@ -0,0 +1,27 @@
1
+ /** An ArgStore over a plain object, for tests and demos. `snapshot()` is a copy, for assertions. */
2
+ export function memoryStore(initial = {}) {
3
+ const data = { ...initial };
4
+ const listeners = new Set();
5
+ return {
6
+ read(key) {
7
+ return data[key];
8
+ },
9
+ write(changes, _method) {
10
+ for (const { key, raw } of changes) {
11
+ if (raw === undefined)
12
+ delete data[key];
13
+ else
14
+ data[key] = raw;
15
+ }
16
+ listeners.forEach((listener) => listener());
17
+ },
18
+ subscribe(listener) {
19
+ listeners.add(listener);
20
+ return () => listeners.delete(listener);
21
+ },
22
+ snapshot() {
23
+ return { ...data };
24
+ },
25
+ };
26
+ }
27
+ //# sourceMappingURL=argStore.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"argStore.js","sourceRoot":"","sources":["../src/argStore.ts"],"names":[],"mappings":"AAwBA,oGAAoG;AACpG,MAAM,UAAU,WAAW,CAAC,UAAkC,EAAE;IAC9D,MAAM,IAAI,GAA2B,EAAE,GAAG,OAAO,EAAE,CAAC;IACpD,MAAM,SAAS,GAAG,IAAI,GAAG,EAAc,CAAC;IAExC,OAAO;QACL,IAAI,CAAC,GAAG;YACN,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC;QACnB,CAAC;QACD,KAAK,CAAC,OAAO,EAAE,OAAO;YACpB,KAAK,MAAM,EAAE,GAAG,EAAE,GAAG,EAAE,IAAI,OAAO,EAAE,CAAC;gBACnC,IAAI,GAAG,KAAK,SAAS;oBAAE,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC;;oBACnC,IAAI,CAAC,GAAG,CAAC,GAAG,GAAG,CAAC;YACvB,CAAC;YACD,SAAS,CAAC,OAAO,CAAC,CAAC,QAAQ,EAAE,EAAE,CAAC,QAAQ,EAAE,CAAC,CAAC;QAC9C,CAAC;QACD,SAAS,CAAC,QAAQ;YAChB,SAAS,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;YACxB,OAAO,GAAG,EAAE,CAAC,SAAS,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;QAC1C,CAAC;QACD,QAAQ;YACN,OAAO,EAAE,GAAG,IAAI,EAAE,CAAC;QACrB,CAAC;KACF,CAAC;AACJ,CAAC"}