vultus-core 0.4.0 → 0.5.1
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/README.md +313 -6
- package/dist/ArgStoreContext.d.ts +10 -0
- package/dist/ArgStoreContext.d.ts.map +1 -0
- package/dist/ArgStoreContext.js +15 -0
- package/dist/ArgStoreContext.js.map +1 -0
- package/dist/FieldContext.d.ts +38 -0
- package/dist/FieldContext.d.ts.map +1 -0
- package/dist/FieldContext.js +82 -0
- package/dist/FieldContext.js.map +1 -0
- package/dist/argStore.d.ts +26 -0
- package/dist/argStore.d.ts.map +1 -0
- package/dist/argStore.js +27 -0
- package/dist/argStore.js.map +1 -0
- package/dist/choices.d.ts +43 -0
- package/dist/choices.d.ts.map +1 -0
- package/dist/choices.js +20 -0
- package/dist/choices.js.map +1 -0
- package/dist/field.d.ts +42 -15
- package/dist/field.d.ts.map +1 -1
- package/dist/field.js +20 -7
- package/dist/field.js.map +1 -1
- package/dist/index.d.ts +22 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +20 -4
- package/dist/index.js.map +1 -1
- package/dist/kinds.d.ts +55 -0
- package/dist/kinds.d.ts.map +1 -0
- package/dist/kinds.js +158 -0
- package/dist/kinds.js.map +1 -0
- package/dist/localStorageStore.d.ts +16 -0
- package/dist/localStorageStore.d.ts.map +1 -0
- package/dist/localStorageStore.js +72 -0
- package/dist/localStorageStore.js.map +1 -0
- package/dist/normalizeText.d.ts +6 -0
- package/dist/normalizeText.d.ts.map +1 -0
- package/dist/normalizeText.js +8 -0
- package/dist/normalizeText.js.map +1 -0
- package/dist/numeric.d.ts +45 -0
- package/dist/numeric.d.ts.map +1 -0
- package/dist/numeric.js +55 -0
- package/dist/numeric.js.map +1 -0
- package/dist/resolve.d.ts +13 -0
- package/dist/resolve.d.ts.map +1 -1
- package/dist/resolve.js +25 -0
- package/dist/resolve.js.map +1 -1
- package/dist/tree.d.ts +69 -0
- package/dist/tree.d.ts.map +1 -0
- package/dist/tree.js +151 -0
- package/dist/tree.js.map +1 -0
- package/dist/types.d.ts +17 -7
- package/dist/types.d.ts.map +1 -1
- package/dist/useArg.d.ts +45 -0
- package/dist/useArg.d.ts.map +1 -0
- package/dist/useArg.js +83 -0
- package/dist/useArg.js.map +1 -0
- package/dist/useBoolField.d.ts +8 -3
- package/dist/useBoolField.d.ts.map +1 -1
- package/dist/useBoolField.js +9 -1
- package/dist/useBoolField.js.map +1 -1
- package/dist/useManyOfField.d.ts +35 -0
- package/dist/useManyOfField.d.ts.map +1 -0
- package/dist/useManyOfField.js +74 -0
- package/dist/useManyOfField.js.map +1 -0
- package/dist/useMixedBoolField.d.ts +8 -3
- package/dist/useMixedBoolField.d.ts.map +1 -1
- package/dist/useMixedBoolField.js +9 -1
- package/dist/useMixedBoolField.js.map +1 -1
- package/dist/useNumericField.d.ts +25 -0
- package/dist/useNumericField.d.ts.map +1 -0
- package/dist/useNumericField.js +49 -0
- package/dist/useNumericField.js.map +1 -0
- package/dist/useOneOfField.d.ts +18 -34
- package/dist/useOneOfField.d.ts.map +1 -1
- package/dist/useOneOfField.js +18 -18
- package/dist/useOneOfField.js.map +1 -1
- package/dist/useRangeField.d.ts +34 -0
- package/dist/useRangeField.d.ts.map +1 -0
- package/dist/useRangeField.js +60 -0
- package/dist/useRangeField.js.map +1 -0
- package/dist/useStoredField.d.ts +61 -0
- package/dist/useStoredField.d.ts.map +1 -0
- package/dist/useStoredField.js +151 -0
- package/dist/useStoredField.js.map +1 -0
- package/dist/useTextField.d.ts +22 -0
- package/dist/useTextField.d.ts.map +1 -0
- package/dist/useTextField.js +21 -0
- package/dist/useTextField.js.map +1 -0
- package/dist/useTreeField.d.ts +39 -0
- package/dist/useTreeField.d.ts.map +1 -0
- package/dist/useTreeField.js +66 -0
- package/dist/useTreeField.js.map +1 -0
- package/dist/valueTypes.d.ts +28 -0
- package/dist/valueTypes.d.ts.map +1 -0
- package/dist/valueTypes.js +65 -0
- package/dist/valueTypes.js.map +1 -0
- package/package.json +1 -1
- package/src/ArgStoreContext.tsx +16 -0
- package/src/FieldContext.tsx +98 -0
- package/src/argStore.ts +49 -0
- package/src/choices.ts +62 -0
- package/src/field.ts +64 -25
- package/src/index.ts +55 -5
- package/src/kinds.ts +208 -0
- package/src/localStorageStore.ts +68 -0
- package/src/normalizeText.ts +7 -0
- package/src/numeric.ts +80 -0
- package/src/resolve.ts +23 -0
- package/src/tree.ts +175 -0
- package/src/types.ts +28 -7
- package/src/useArg.ts +121 -0
- package/src/useBoolField.ts +19 -6
- package/src/useManyOfField.ts +107 -0
- package/src/useMixedBoolField.ts +19 -6
- package/src/useNumericField.ts +66 -0
- package/src/useOneOfField.ts +38 -58
- package/src/useRangeField.ts +83 -0
- package/src/useStoredField.ts +205 -0
- package/src/useTextField.ts +39 -0
- package/src/useTreeField.ts +108 -0
- package/src/valueTypes.ts +77 -0
package/README.md
CHANGED
|
@@ -20,12 +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
|
|
24
|
-
|
|
25
|
-
`
|
|
26
|
-
`
|
|
27
|
-
`
|
|
28
|
-
`
|
|
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`.
|
|
29
39
|
- `./link` — `InternalLinkContext`, `useInternalLink`, and the
|
|
30
40
|
`InternalLinkComponent` type on their own, for consumers that only need the
|
|
31
41
|
link-injection seam.
|
|
@@ -73,6 +83,303 @@ boolean field (inside: `initialValue`, default the first enabled choice); `confi
|
|
|
73
83
|
depend on the requested choice. vultus-antd's `OneOfSelect` and `OneOfSegmented` take the
|
|
74
84
|
same field.
|
|
75
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
|
+
Caution: the parameter-count rule above only decides what a function gets
|
|
122
|
+
with no provider. With a provider, a function whose declared parameter
|
|
123
|
+
count is zero — a default parameter (`(ctx = {}) => ...`), a rest parameter
|
|
124
|
+
(`(...args) => ...`), or a bound `t` — still receives the guarded context,
|
|
125
|
+
and reading a key it was not given still throws.
|
|
126
|
+
|
|
127
|
+
```tsx
|
|
128
|
+
const { Provider: ModuleContext } = createFieldContext<{ items: Item[] }>();
|
|
129
|
+
|
|
130
|
+
function Filters({ items }: { items: Item[] }) {
|
|
131
|
+
return (
|
|
132
|
+
<ModuleContext value={{ items }}>
|
|
133
|
+
<Categories />
|
|
134
|
+
</ModuleContext>
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function Categories() {
|
|
139
|
+
const field = useManyOfField({ id: 'categories', choices: (ctx) => categoriesOf(ctx.items) });
|
|
140
|
+
// ...
|
|
141
|
+
}
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
## More kinds
|
|
145
|
+
|
|
146
|
+
Next to `useBoolField` and `useOneOfField`, the other field kinds. Each is
|
|
147
|
+
built the same way (`useFieldCore`, the same inside / local / commit
|
|
148
|
+
storage), so everything the Fields section above says about storage,
|
|
149
|
+
visibility, enabling and commits applies to all of them too.
|
|
150
|
+
|
|
151
|
+
### many-of (`useManyOfField`)
|
|
152
|
+
|
|
153
|
+
The value is a list of choices (`OneOfChoice`, as one-of's), kept as a set:
|
|
154
|
+
order and duplicates don't matter to its meaning, and every request (through
|
|
155
|
+
`setValue` or `toggle`) is normalized to choice order without duplicates,
|
|
156
|
+
followed by the values the field already holds that are no longer among the
|
|
157
|
+
choices and that the request keeps, in their current order. `toggle` keeps
|
|
158
|
+
them, so a stored value naming a removed choice isn't silently dropped;
|
|
159
|
+
`setValue([])` clears them along with everything else. Controls add
|
|
160
|
+
`choices` (each with `selected`), `isSelected(value)` and `toggle(value)`. A request naming
|
|
161
|
+
an unknown choice, or flipping a disabled one, is refused with a
|
|
162
|
+
`console.warn` — the hook itself polices this, as `useOneOfField` already
|
|
163
|
+
does for its own `setValue`, not just the widget. Implicit initial value
|
|
164
|
+
(with no `initialValue` and no outside storage): the empty list.
|
|
165
|
+
|
|
166
|
+
```ts
|
|
167
|
+
const field = useManyOfField({
|
|
168
|
+
id: 'tags',
|
|
169
|
+
choices: [{ value: 'urgent' }, { value: 'archived', disabled: true }],
|
|
170
|
+
});
|
|
171
|
+
field.toggle('urgent'); // field.isSelected('urgent') === true
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
### numeric (`useNumericField`)
|
|
175
|
+
|
|
176
|
+
The value is a single number. `min`, `max`, `step`, `integer` (moxb's
|
|
177
|
+
`onlyInteger`) and `unit` may each be a context function; `format(n, ctx)`
|
|
178
|
+
overrides the default display (the number followed by the unit, if any).
|
|
179
|
+
The shown value is always clamped into the bounds, then rounded when
|
|
180
|
+
`integer` to a whole number inside them (a stored `2.5` shows `3`; with
|
|
181
|
+
`max: 9.5`, `9.7` shows `9`), since a value from outside — a
|
|
182
|
+
passed-in `value`, a stored one — may fall outside them or be fractional
|
|
183
|
+
even though every request the hook itself makes fits; a non-finite request
|
|
184
|
+
(a widget that parsed `''`) is refused with a warning. `valueText` carries
|
|
185
|
+
the formatted value. Implicit initial value: `min` (or `0` if there is
|
|
186
|
+
none), clamped into the bounds.
|
|
187
|
+
|
|
188
|
+
```ts
|
|
189
|
+
const field = useNumericField({ id: 'count', min: 0, max: 10, unit: 'pcs' });
|
|
190
|
+
field.valueText; // '0pcs'
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
### range (`useRangeField`)
|
|
194
|
+
|
|
195
|
+
The value is a `NumberRange`, `[from, to]` with `min <= from <= to <= max`;
|
|
196
|
+
it shares numeric's bounds and formatting, but `min` and `max` are required
|
|
197
|
+
here and `step` defaults to `1`. `valueText` joins the formatted ends with
|
|
198
|
+
an en dash ("2010/11–2025/26"), so a layout can put the whole range in the
|
|
199
|
+
field's label instead of next to it. A value from outside is clamped into
|
|
200
|
+
the bounds end by end, rounded when `integer`, then ordered. Implicit
|
|
201
|
+
initial value: `[min, max]`.
|
|
202
|
+
|
|
203
|
+
```ts
|
|
204
|
+
const field = useRangeField({ id: 'years', min: 2010, max: 2026 });
|
|
205
|
+
field.valueText; // '2010–2026'
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
### tree (`useTreeField`)
|
|
209
|
+
|
|
210
|
+
The value is a list of node ids (`nodes`: `TreeNode`, a choice with
|
|
211
|
+
`children`), kept as a set. `selection` says which of the covered nodes the
|
|
212
|
+
value names: `'topmost'` (default; antd's `SHOW_PARENT`: a checked node
|
|
213
|
+
stands for its whole subtree), `'all'`, `'leaves'` (`SHOW_CHILD`), or
|
|
214
|
+
`'independent'` (`checkStrictly`, moxb's `strictSelect`: nodes are chosen on
|
|
215
|
+
their own, with no cascading). The hook normalizes the value under
|
|
216
|
+
`selection` on every render — with `'topmost'`, a node and all its checked
|
|
217
|
+
children collapse to the node alone — and exposes `expandedKeys` (which
|
|
218
|
+
nodes a widget opens at first, controlled by `expand`: `'top'` (default),
|
|
219
|
+
`'none'`, `'selected'`, `'all'`) and `toggle(value)` (refused, with a
|
|
220
|
+
warning, for an unknown or disabled node, as many-of's does).
|
|
221
|
+
`coveredNodes(nodes, value, selection)` expands a value to every node it
|
|
222
|
+
stands for — what a module matches an item's categories against. Search
|
|
223
|
+
over labels is accent- and case-insensitive (`normalizeText`); the matching
|
|
224
|
+
`filterTree(nodes, query)` follows antd TreeSelect's rule: a node whose own
|
|
225
|
+
label matches stays with its whole subtree, any other node stays if one of
|
|
226
|
+
its descendants matches (with only the children that stay), and the rest
|
|
227
|
+
is trimmed, leaving the nodes given unchanged. Implicit initial value: the
|
|
228
|
+
empty list.
|
|
229
|
+
|
|
230
|
+
```ts
|
|
231
|
+
const field = useTreeField({
|
|
232
|
+
id: 'categories',
|
|
233
|
+
nodes: [{ value: 'a', children: [{ value: 'a1' }, { value: 'a2' }] }],
|
|
234
|
+
});
|
|
235
|
+
field.toggle('a'); // field.value === ['a'] — 'topmost' collapses the subtree to the parent
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
### text (`useTextField`)
|
|
239
|
+
|
|
240
|
+
The value is a string. `control: 'input' | 'textarea'` (default `'input'`)
|
|
241
|
+
is a hint for the widget, nothing else. A field inside the field or changed
|
|
242
|
+
locally (`onChange`) requests on every keystroke, as moxb's, sidedao's and
|
|
243
|
+
Taskmaster's text fields do; a field that `commits` holds requests back
|
|
244
|
+
until the widget decides (blur or Enter) — any commit-mode field does this,
|
|
245
|
+
not something specific to text. Highlighting the search words in a result
|
|
246
|
+
list is the app's job (e.g. quaesitor's `WithSearch`), not this hook's.
|
|
247
|
+
Implicit initial value: the empty string.
|
|
248
|
+
|
|
249
|
+
```ts
|
|
250
|
+
const field = useTextField({ id: 'search', control: 'input' });
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
## Definitions as data
|
|
254
|
+
|
|
255
|
+
A `FieldDefinition` is a field's options in their "inside" form (no `value`
|
|
256
|
+
plus `onChange` or `commit`), tagged with a required `kind` — one of
|
|
257
|
+
`'boolean'`, `'mixed-boolean'`, `'oneOf'`, `'manyOf'`, `'numeric'`,
|
|
258
|
+
`'range'`, `'tree'`, `'text'` (the same strings `FieldControls.type` already
|
|
259
|
+
carries). `kind` stays optional on each hook's own options — a hook knows
|
|
260
|
+
its own kind, and throws if a definition names a different one — but is
|
|
261
|
+
required once a field becomes plain data: a module declares its filters as a
|
|
262
|
+
list of `FieldDefinition`s, independent of which hook or widget ends up
|
|
263
|
+
running each one.
|
|
264
|
+
|
|
265
|
+
A definer who doesn't care where the value lives gives only `initialValue`
|
|
266
|
+
(or nothing, for the kind's implicit default); whoever runs the field — a
|
|
267
|
+
hook directly, or `useStoredField` (see Arguments, below) — chooses the
|
|
268
|
+
storage. `initialValueOf(definition, ctx)` is every kind's default in one
|
|
269
|
+
place, and the entry point for it: what a definition with no `initialValue`
|
|
270
|
+
starts at, what a reset restores, and what "is it changed?" compares
|
|
271
|
+
against — `false`; the first enabled choice; the empty list; `min` (or `0`)
|
|
272
|
+
clamped into the bounds; `[min, max]`; the empty string for text; for a
|
|
273
|
+
tree, the value normalized under its `selection`. (The per-kind helpers it
|
|
274
|
+
is built on — `boolInitialValue`, `oneOfInitialValue`, `rangeInitialValue`
|
|
275
|
+
and the like — are exported too.) A hook computes its kind's default once,
|
|
276
|
+
when the field mounts, not on every render.
|
|
277
|
+
|
|
278
|
+
The `widget` hint (`WidgetHint = string | { name: string; [option]: unknown
|
|
279
|
+
}`) names which widget renders the field — `'segmented'`, or `{ name:
|
|
280
|
+
'segmented', iconOnly: true }` with its display options — without
|
|
281
|
+
vultus-core knowing what a "segmented control" is; it's vultus-antd that
|
|
282
|
+
turns the name into a component, applying render-site overrides on top.
|
|
283
|
+
Each kind has a default widget when none is given; an unknown widget name
|
|
284
|
+
throws, in vultus-antd.
|
|
285
|
+
|
|
286
|
+
```ts
|
|
287
|
+
const definitions: FieldDefinition[] = [
|
|
288
|
+
{ kind: 'boolean', id: 'showArchived' },
|
|
289
|
+
{ kind: 'numeric', id: 'count', min: 0, max: 10, widget: 'slider' },
|
|
290
|
+
];
|
|
291
|
+
initialValueOf(definitions[0], {}); // false
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
## Arguments
|
|
295
|
+
|
|
296
|
+
Storage outside a field hook — a URL, localStorage — lives in vultus-core as
|
|
297
|
+
its own layer (ported from moxb's `url-arg`, without mobx), independent of
|
|
298
|
+
the field hooks: an argument is `{ key, valueType, default }`, kept in
|
|
299
|
+
whatever `ArgStore` is nearest.
|
|
300
|
+
|
|
301
|
+
**Value types** (`ValueType<T>`: `parse(raw)`, `format(value)`, `isEqual(a,
|
|
302
|
+
b)`) say how a value is kept as a string and compared; equality is the
|
|
303
|
+
type's own meaning (a list as a set, a range by both ends), used everywhere
|
|
304
|
+
two values of that type are compared — including whether a value is the
|
|
305
|
+
default, so it's left out of the store. `valueTypeOf(kind)` gives each field
|
|
306
|
+
kind's: `booleanType` (`'1'` / `'0'`), `stringType` (one-of, text),
|
|
307
|
+
`stringSetType` (many-of, tree — a sorted comma list; a *present but empty*
|
|
308
|
+
value reads as the empty list, which differs from the key being absent
|
|
309
|
+
altogether), `numberType`, `rangeType` (`from..to`). A mixed-boolean field
|
|
310
|
+
shows an aggregate of other data and has no value type:
|
|
311
|
+
`valueTypeOf('mixed-boolean')` throws, and so does storing one. A choice
|
|
312
|
+
value containing `,`, or an empty one, cannot be written to a list and
|
|
313
|
+
throws — that's the definer's mistake, not outside input. A stored string
|
|
314
|
+
that doesn't parse, or names a choice or node that no longer exists, *is*
|
|
315
|
+
outside input (an edited or stale URL): it never throws, it falls back to
|
|
316
|
+
the default (or is dropped from a list), with a `console.warn`.
|
|
317
|
+
|
|
318
|
+
**Stores.** `ArgStore` is `read(key)`, `write(changes, method)` (`method`:
|
|
319
|
+
`'push'` or `'replace'`; a store without history ignores it),
|
|
320
|
+
`subscribe(listener)`, and optionally `hrefFor(changes)`. vultus-core ships
|
|
321
|
+
`memoryStore()` (tests, demos) and `localStorageStore(prefix)` (keys
|
|
322
|
+
namespaced as `${prefix}.${key}`, guarded for SSR so creating one outside a
|
|
323
|
+
browser is a safe no-op; where the browser blocks storage or refuses a
|
|
324
|
+
write, a failing read reads as absent and a failing write is skipped, with
|
|
325
|
+
one `console.warn` per store); a URL store comes from a router package —
|
|
326
|
+
vultus-react-router for react-router.
|
|
327
|
+
Create a store once — outside render, or in `useMemo` — not on every
|
|
328
|
+
render: each store is a new subscription target, so a fresh one per render
|
|
329
|
+
makes every hook below resubscribe.
|
|
330
|
+
`<ArgStoreProvider store={...}>` gives a subtree its store — nest providers
|
|
331
|
+
to put the URL around a page and localStorage around a dialog inside it. A
|
|
332
|
+
hook with no `ArgStoreProvider` above it throws, rather than silently
|
|
333
|
+
reading nothing.
|
|
334
|
+
|
|
335
|
+
**`useArg(definition)`** reads and writes one argument: `value`,
|
|
336
|
+
`isDefault`, `set(value, method?)`, `reset(method?)` and `hrefFor(value)`
|
|
337
|
+
(where the store supports it). An absent key reads as the default; writing a
|
|
338
|
+
value that equals the default removes the key instead of storing it.
|
|
339
|
+
|
|
340
|
+
**Batches.** `useArgWriter()` returns `apply(changes, method)` where each
|
|
341
|
+
change is `setArg(definition, value)` or `resetArg(definition)` — several
|
|
342
|
+
arguments change in one store write, one history entry (a "clear all"
|
|
343
|
+
button, a filter chip that also closes a detail view).
|
|
344
|
+
|
|
345
|
+
**Fields over arguments.** `argOf(definition, ctx)` is the argument a
|
|
346
|
+
definition's value is stored as — key = the definition's `id`, value type
|
|
347
|
+
from its kind, default = its resolved initial value — for a caller that
|
|
348
|
+
wants `useArg` directly instead of a field. `useStoredField(definition)`
|
|
349
|
+
runs a `FieldDefinition` as a stored field the same way: the argument's key
|
|
350
|
+
is the definition's `id`, its value type comes from the kind, its default
|
|
351
|
+
is `initialValueOf`. It returns `{ options, isDefault, reset }` — `options`
|
|
352
|
+
is the definition in its outside form (`value` plus `onChange`, ready for
|
|
353
|
+
the kind's hook), with the stored raw value made to fit the definition as
|
|
354
|
+
it is now (`sanitizeStored`: unknown choices or nodes dropped, numbers and
|
|
355
|
+
ranges clamped, and rounded when `integer`). A value that needed fixing up
|
|
356
|
+
warns once per field and per raw value — not again on every render, and not
|
|
357
|
+
at all when the key is simply absent (then the default applies, and an
|
|
358
|
+
absent key is not outside input).
|
|
359
|
+
`useStoredFields(definitions)` answers for several definitions over one
|
|
360
|
+
store subscription: `isDefault(id)` (throws for an id not among the
|
|
361
|
+
definitions), `changedCount`, and `resetAll(method)` (one write for every
|
|
362
|
+
field). Being a summary, it never warns — only `useStoredField` on the
|
|
363
|
+
field itself does.
|
|
364
|
+
|
|
365
|
+
```tsx
|
|
366
|
+
const store = memoryStore();
|
|
367
|
+
|
|
368
|
+
function Filters() {
|
|
369
|
+
return (
|
|
370
|
+
<ArgStoreProvider store={store}>
|
|
371
|
+
<Category />
|
|
372
|
+
</ArgStoreProvider>
|
|
373
|
+
);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function Category() {
|
|
377
|
+
const { options, reset } = useStoredField({ kind: 'manyOf', id: 'tags', choices: [{ value: 'urgent' }] });
|
|
378
|
+
const field = useManyOfField(options);
|
|
379
|
+
// ...
|
|
380
|
+
}
|
|
381
|
+
```
|
|
382
|
+
|
|
76
383
|
## Components
|
|
77
384
|
|
|
78
385
|
`VultusComponentsContext` (default `{}`) / `useVultusComponents()` is how an app lends its own
|
|
@@ -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,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"}
|
package/dist/argStore.js
ADDED
|
@@ -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"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import type { Decision } from './types.js';
|
|
3
|
+
/**
|
|
4
|
+
* One choice of a one-of or many-of field, or a tree node (moxb's
|
|
5
|
+
* BindOneOfChoice). Shared by useOneOfField today; useManyOfField and the
|
|
6
|
+
* tree hook (later tasks) build on the same shape.
|
|
7
|
+
*/
|
|
8
|
+
export interface OneOfChoice<T extends string = string> {
|
|
9
|
+
value: T;
|
|
10
|
+
/** Shown for the choice; default: the value. */
|
|
11
|
+
label?: string;
|
|
12
|
+
/** What choosing it does (moxb's `help`), markdown; also the field's current state when it is chosen. */
|
|
13
|
+
description?: string;
|
|
14
|
+
/** Shown before the label (or instead of it, for an icon-only widget), like an action's icon. */
|
|
15
|
+
icon?: ReactNode;
|
|
16
|
+
/** Choosable? Default true, optionally with a reason. Use `enabled` or `disabled`, not contradicting each other. */
|
|
17
|
+
enabled?: Decision;
|
|
18
|
+
disabled?: Decision;
|
|
19
|
+
/** Shown as the main or a dangerous choice, like an action's variant. Default 'default'. */
|
|
20
|
+
variant?: 'default' | 'primary' | 'danger';
|
|
21
|
+
/** A heading the choice is listed under, after the choices without one (OneOfSelect); default none. */
|
|
22
|
+
group?: string;
|
|
23
|
+
/** Shown after the label in secondary colour, e.g. a count ("12", "12 · 8%"). Default none. */
|
|
24
|
+
note?: string;
|
|
25
|
+
/** Shown faded, e.g. a choice with nothing behind it right now (no matching tasks). Default false. */
|
|
26
|
+
dimmed?: boolean;
|
|
27
|
+
}
|
|
28
|
+
/** A choice as the widgets get it. */
|
|
29
|
+
export interface OneOfChoiceControls<T extends string = string> {
|
|
30
|
+
value: T;
|
|
31
|
+
label: string;
|
|
32
|
+
description: string | undefined;
|
|
33
|
+
icon: ReactNode | undefined;
|
|
34
|
+
enabled: boolean;
|
|
35
|
+
whyDisabled: string | undefined;
|
|
36
|
+
variant: 'default' | 'primary' | 'danger';
|
|
37
|
+
group: string | undefined;
|
|
38
|
+
note: string | undefined;
|
|
39
|
+
dimmed: boolean;
|
|
40
|
+
}
|
|
41
|
+
/** Resolves a choice's `enabled` / `disabled` and defaults, as useOneOfField does for each of its choices. */
|
|
42
|
+
export declare function resolveChoice<T extends string>(fieldId: string, choice: OneOfChoice<T>): OneOfChoiceControls<T>;
|
|
43
|
+
//# sourceMappingURL=choices.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"choices.d.ts","sourceRoot":"","sources":["../src/choices.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAGvC,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAE3C;;;;GAIG;AACH,MAAM,WAAW,WAAW,CAAC,CAAC,SAAS,MAAM,GAAG,MAAM;IACpD,KAAK,EAAE,CAAC,CAAC;IACT,gDAAgD;IAChD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,yGAAyG;IACzG,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,iGAAiG;IACjG,IAAI,CAAC,EAAE,SAAS,CAAC;IACjB,oHAAoH;IACpH,OAAO,CAAC,EAAE,QAAQ,CAAC;IACnB,QAAQ,CAAC,EAAE,QAAQ,CAAC;IACpB,4FAA4F;IAC5F,OAAO,CAAC,EAAE,SAAS,GAAG,SAAS,GAAG,QAAQ,CAAC;IAC3C,uGAAuG;IACvG,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,+FAA+F;IAC/F,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,sGAAsG;IACtG,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED,sCAAsC;AACtC,MAAM,WAAW,mBAAmB,CAAC,CAAC,SAAS,MAAM,GAAG,MAAM;IAC5D,KAAK,EAAE,CAAC,CAAC;IACT,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,GAAG,SAAS,CAAC;IAChC,IAAI,EAAE,SAAS,GAAG,SAAS,CAAC;IAC5B,OAAO,EAAE,OAAO,CAAC;IACjB,WAAW,EAAE,MAAM,GAAG,SAAS,CAAC;IAChC,OAAO,EAAE,SAAS,GAAG,SAAS,GAAG,QAAQ,CAAC;IAC1C,KAAK,EAAE,MAAM,GAAG,SAAS,CAAC;IAC1B,IAAI,EAAE,MAAM,GAAG,SAAS,CAAC;IACzB,MAAM,EAAE,OAAO,CAAC;CACjB;AAED,8GAA8G;AAC9G,wBAAgB,aAAa,CAAC,CAAC,SAAS,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,CAAC,CAAC,CAAC,GAAG,mBAAmB,CAAC,CAAC,CAAC,CAe/G"}
|
package/dist/choices.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { calculateEnabled } from './field.js';
|
|
2
|
+
import { getReason, getVerdict } from './decision.js';
|
|
3
|
+
/** Resolves a choice's `enabled` / `disabled` and defaults, as useOneOfField does for each of its choices. */
|
|
4
|
+
export function resolveChoice(fieldId, choice) {
|
|
5
|
+
const decision = calculateEnabled(`${fieldId}.${choice.value}`, choice.enabled, choice.disabled);
|
|
6
|
+
const enabled = getVerdict(decision, true);
|
|
7
|
+
return {
|
|
8
|
+
value: choice.value,
|
|
9
|
+
label: choice.label ?? choice.value,
|
|
10
|
+
description: choice.description,
|
|
11
|
+
icon: choice.icon,
|
|
12
|
+
enabled,
|
|
13
|
+
whyDisabled: enabled ? undefined : getReason(decision),
|
|
14
|
+
variant: choice.variant ?? 'default',
|
|
15
|
+
group: choice.group,
|
|
16
|
+
note: choice.note,
|
|
17
|
+
dimmed: choice.dimmed ?? false,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
//# sourceMappingURL=choices.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"choices.js","sourceRoot":"","sources":["../src/choices.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAC9C,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AA2CtD,8GAA8G;AAC9G,MAAM,UAAU,aAAa,CAAmB,OAAe,EAAE,MAAsB;IACrF,MAAM,QAAQ,GAAG,gBAAgB,CAAC,GAAG,OAAO,IAAI,MAAM,CAAC,KAAK,EAAE,EAAE,MAAM,CAAC,OAAO,EAAE,MAAM,CAAC,QAAQ,CAAC,CAAC;IACjG,MAAM,OAAO,GAAG,UAAU,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC;IAC3C,OAAO;QACL,KAAK,EAAE,MAAM,CAAC,KAAK;QACnB,KAAK,EAAE,MAAM,CAAC,KAAK,IAAI,MAAM,CAAC,KAAK;QACnC,WAAW,EAAE,MAAM,CAAC,WAAW;QAC/B,IAAI,EAAE,MAAM,CAAC,IAAI;QACjB,OAAO;QACP,WAAW,EAAE,OAAO,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,QAAQ,CAAC;QACtD,OAAO,EAAE,MAAM,CAAC,OAAO,IAAI,SAAS;QACpC,KAAK,EAAE,MAAM,CAAC,KAAK;QACnB,IAAI,EAAE,MAAM,CAAC,IAAI;QACjB,MAAM,EAAE,MAAM,CAAC,MAAM,IAAI,KAAK;KAC/B,CAAC;AACJ,CAAC"}
|