vultus-core 0.2.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -61,6 +61,16 @@ and can be asked for `true` or `false` only (the field core separates the shown
61
61
  requestable one); a toggle from mixed requests `true`. `aggregateBool(values)` derives the
62
62
  value; with no items at all, disable the field with a reason instead.
63
63
 
64
+ `useOneOfField` is a field whose value is one of a list of `choices` (static or derived):
65
+ each `{ value, label?, description?, icon?, enabled?/disabled?, variant? }`. A choice may
66
+ be disabled with a reason (a `Decision`, as a field's), and be `primary` or `danger` like an
67
+ action's variant; `description` (markdown) says what choosing it does and is also the field's
68
+ current state while it is chosen, unless `valueDescriptions` words that state differently.
69
+ Requesting a disabled or unknown choice is ignored with a warning. Storage is as for the
70
+ boolean field (inside: `initialValue`, default the first enabled choice); `confirmation` may
71
+ depend on the requested choice. vultus-antd's `OneOfSelect` and `OneOfSegmented` take the
72
+ same field.
73
+
64
74
  ## Distribution
65
75
 
66
76
  This package ships compiled ESM (`dist/`) with TypeScript declarations to
package/dist/index.d.ts CHANGED
@@ -9,6 +9,7 @@ export { useActionList } from './useActionList.js';
9
9
  export { useFieldCore, fieldStorageMode, calculateVisible, calculateEnabled, type FieldCommit, type FieldControls, type FieldOptions, type FieldStorage, type FieldStorageMode, type ValueKey, } from './field.js';
10
10
  export { useBoolField, type BoolFieldControls, type BoolFieldOptions } from './useBoolField.js';
11
11
  export { useMixedBoolField, aggregateBool, type MixedBool, type MixedBoolFieldControls, type MixedBoolFieldOptions, } from './useMixedBoolField.js';
12
+ export { useOneOfField, type OneOfChoice, type OneOfChoiceControls, type OneOfFieldControls, type OneOfFieldOptions, } from './useOneOfField.js';
12
13
  export { InternalLinkContext, useInternalLink, type InternalLinkComponent } from './LinkContext.js';
13
14
  export { MessageSinkContext, useMessageSink, type MessageSink } from './MessageSink.js';
14
15
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAC;AAC3B,cAAc,eAAe,CAAC;AAC9B,cAAc,cAAc,CAAC;AAC7B,cAAc,oBAAoB,CAAC;AACnC,cAAc,oBAAoB,CAAC;AACnC,OAAO,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAC/E,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAC3C,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACnD,OAAO,EACL,YAAY,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,gBAAgB,EAClE,KAAK,WAAW,EAAE,KAAK,aAAa,EAAE,KAAK,YAAY,EAAE,KAAK,YAAY,EAAE,KAAK,gBAAgB,EAAE,KAAK,QAAQ,GACjH,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,YAAY,EAAE,KAAK,iBAAiB,EAAE,KAAK,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAChG,OAAO,EACL,iBAAiB,EAAE,aAAa,EAAE,KAAK,SAAS,EAAE,KAAK,sBAAsB,EAAE,KAAK,qBAAqB,GAC1G,MAAM,wBAAwB,CAAC;AAChC,OAAO,EAAE,mBAAmB,EAAE,eAAe,EAAE,KAAK,qBAAqB,EAAE,MAAM,kBAAkB,CAAC;AACpG,OAAO,EAAE,kBAAkB,EAAE,cAAc,EAAE,KAAK,WAAW,EAAE,MAAM,kBAAkB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAC;AAC3B,cAAc,eAAe,CAAC;AAC9B,cAAc,cAAc,CAAC;AAC7B,cAAc,oBAAoB,CAAC;AACnC,cAAc,oBAAoB,CAAC;AACnC,OAAO,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAC/E,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAC3C,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACnD,OAAO,EACL,YAAY,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,gBAAgB,EAClE,KAAK,WAAW,EAAE,KAAK,aAAa,EAAE,KAAK,YAAY,EAAE,KAAK,YAAY,EAAE,KAAK,gBAAgB,EAAE,KAAK,QAAQ,GACjH,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,YAAY,EAAE,KAAK,iBAAiB,EAAE,KAAK,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAChG,OAAO,EACL,iBAAiB,EAAE,aAAa,EAAE,KAAK,SAAS,EAAE,KAAK,sBAAsB,EAAE,KAAK,qBAAqB,GAC1G,MAAM,wBAAwB,CAAC;AAChC,OAAO,EACL,aAAa,EAAE,KAAK,WAAW,EAAE,KAAK,mBAAmB,EAAE,KAAK,kBAAkB,EAAE,KAAK,iBAAiB,GAC3G,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAE,mBAAmB,EAAE,eAAe,EAAE,KAAK,qBAAqB,EAAE,MAAM,kBAAkB,CAAC;AACpG,OAAO,EAAE,kBAAkB,EAAE,cAAc,EAAE,KAAK,WAAW,EAAE,MAAM,kBAAkB,CAAC"}
package/dist/index.js CHANGED
@@ -9,6 +9,7 @@ export { useActionList } from './useActionList.js';
9
9
  export { useFieldCore, fieldStorageMode, calculateVisible, calculateEnabled, } from './field.js';
10
10
  export { useBoolField } from './useBoolField.js';
11
11
  export { useMixedBoolField, aggregateBool, } from './useMixedBoolField.js';
12
+ export { useOneOfField, } from './useOneOfField.js';
12
13
  export { InternalLinkContext, useInternalLink } from './LinkContext.js';
13
14
  export { MessageSinkContext, useMessageSink } from './MessageSink.js';
14
15
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAC;AAC3B,cAAc,eAAe,CAAC;AAC9B,cAAc,cAAc,CAAC;AAC7B,cAAc,oBAAoB,CAAC;AACnC,cAAc,oBAAoB,CAAC;AACnC,OAAO,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAC/E,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAC3C,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACnD,OAAO,EACL,YAAY,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,gBAAgB,GAEnE,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,YAAY,EAAiD,MAAM,mBAAmB,CAAC;AAChG,OAAO,EACL,iBAAiB,EAAE,aAAa,GACjC,MAAM,wBAAwB,CAAC;AAChC,OAAO,EAAE,mBAAmB,EAAE,eAAe,EAA8B,MAAM,kBAAkB,CAAC;AACpG,OAAO,EAAE,kBAAkB,EAAE,cAAc,EAAoB,MAAM,kBAAkB,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAC;AAC3B,cAAc,eAAe,CAAC;AAC9B,cAAc,cAAc,CAAC;AAC7B,cAAc,oBAAoB,CAAC;AACnC,cAAc,oBAAoB,CAAC;AACnC,OAAO,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAC/E,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAC3C,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACnD,OAAO,EACL,YAAY,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,gBAAgB,GAEnE,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,YAAY,EAAiD,MAAM,mBAAmB,CAAC;AAChG,OAAO,EACL,iBAAiB,EAAE,aAAa,GACjC,MAAM,wBAAwB,CAAC;AAChC,OAAO,EACL,aAAa,GACd,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAE,mBAAmB,EAAE,eAAe,EAA8B,MAAM,kBAAkB,CAAC;AACpG,OAAO,EAAE,kBAAkB,EAAE,cAAc,EAAoB,MAAM,kBAAkB,CAAC"}
@@ -0,0 +1,51 @@
1
+ import type { ReactNode } from 'react';
2
+ import { type FieldControls, type FieldOptions } from './field.js';
3
+ import type { Decision, ErrorRoutesRegistry, ValueOrFn } from './types.js';
4
+ /** One choice of a one-of field (moxb's BindOneOfChoice). */
5
+ export interface OneOfChoice<T extends string = string> {
6
+ value: T;
7
+ /** Shown for the choice; default: the value. */
8
+ label?: string;
9
+ /** What choosing it does (moxb's `help`), markdown; also the field's current state when it is chosen. */
10
+ description?: string;
11
+ /** Shown before the label (or instead of it, for an icon-only widget), like an action's icon. */
12
+ icon?: ReactNode;
13
+ /** Choosable? Default true, optionally with a reason. Use `enabled` or `disabled`, not contradicting each other. */
14
+ enabled?: Decision;
15
+ disabled?: Decision;
16
+ /** Shown as the main or a dangerous choice, like an action's variant. Default 'default'. */
17
+ variant?: 'default' | 'primary' | 'danger';
18
+ /** A heading the choice is listed under, after the choices without one (OneOfSelect); default none. */
19
+ group?: string;
20
+ }
21
+ /** A choice as the widgets get it. */
22
+ export interface OneOfChoiceControls<T extends string = string> {
23
+ value: T;
24
+ label: string;
25
+ description: string | undefined;
26
+ icon: ReactNode | undefined;
27
+ enabled: boolean;
28
+ whyDisabled: string | undefined;
29
+ variant: 'default' | 'primary' | 'danger';
30
+ group: string | undefined;
31
+ }
32
+ export type OneOfFieldOptions<T extends string = string, RouteId extends string = string> = FieldOptions<T, T, RouteId> & {
33
+ /** The choices, in order; static or derived. */
34
+ choices: ValueOrFn<OneOfChoice<T>[]>;
35
+ };
36
+ export interface OneOfFieldControls<T extends string = string> extends FieldControls<T, T> {
37
+ choices: OneOfChoiceControls<T>[];
38
+ /** The choice the value is, if it is one of them. */
39
+ current: OneOfChoiceControls<T> | undefined;
40
+ }
41
+ /**
42
+ * A field whose value is one of a list of choices (moxb's OneOf). Rendered by
43
+ * vultus-antd's OneOfSelect and OneOfSegmented, which take the same controls.
44
+ * Each choice may be disabled with a reason; the field's current state is the
45
+ * current choice's description unless `valueDescriptions` words it
46
+ * differently. Requesting a disabled or unknown choice is ignored with a
47
+ * warning, as a disabled field ignores any request. Inside the field the value
48
+ * starts at `initialValue`, else at the first enabled choice.
49
+ */
50
+ export declare function useOneOfField<T extends string = string, RouteId extends string = string>(opts: OneOfFieldOptions<T, RouteId>, registry?: ErrorRoutesRegistry<RouteId>): OneOfFieldControls<T>;
51
+ //# sourceMappingURL=useOneOfField.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useOneOfField.d.ts","sourceRoot":"","sources":["../src/useOneOfField.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AACvC,OAAO,EAAkC,KAAK,aAAa,EAAE,KAAK,YAAY,EAAE,MAAM,YAAY,CAAC;AAGnG,OAAO,KAAK,EAAE,QAAQ,EAAE,mBAAmB,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAE3E,6DAA6D;AAC7D,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;CAChB;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;CAC3B;AAED,MAAM,MAAM,iBAAiB,CAAC,CAAC,SAAS,MAAM,GAAG,MAAM,EAAE,OAAO,SAAS,MAAM,GAAG,MAAM,IACtF,YAAY,CAAC,CAAC,EAAE,CAAC,EAAE,OAAO,CAAC,GAAG;IAC5B,gDAAgD;IAChD,OAAO,EAAE,SAAS,CAAC,WAAW,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;CACtC,CAAC;AAEJ,MAAM,WAAW,kBAAkB,CAAC,CAAC,SAAS,MAAM,GAAG,MAAM,CAAE,SAAQ,aAAa,CAAC,CAAC,EAAE,CAAC,CAAC;IACxF,OAAO,EAAE,mBAAmB,CAAC,CAAC,CAAC,EAAE,CAAC;IAClC,qDAAqD;IACrD,OAAO,EAAE,mBAAmB,CAAC,CAAC,CAAC,GAAG,SAAS,CAAC;CAC7C;AAiBD;;;;;;;;GAQG;AACH,wBAAgB,aAAa,CAAC,CAAC,SAAS,MAAM,GAAG,MAAM,EAAE,OAAO,SAAS,MAAM,GAAG,MAAM,EACtF,IAAI,EAAE,iBAAiB,CAAC,CAAC,EAAE,OAAO,CAAC,EACnC,QAAQ,CAAC,EAAE,mBAAmB,CAAC,OAAO,CAAC,GACtC,kBAAkB,CAAC,CAAC,CAAC,CA2BvB"}
@@ -0,0 +1,53 @@
1
+ import { calculateEnabled, useFieldCore } from './field.js';
2
+ import { getReason, getVerdict } from './decision.js';
3
+ import { resolve } from './resolve.js';
4
+ 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
+ };
17
+ }
18
+ /**
19
+ * A field whose value is one of a list of choices (moxb's OneOf). Rendered by
20
+ * vultus-antd's OneOfSelect and OneOfSegmented, which take the same controls.
21
+ * Each choice may be disabled with a reason; the field's current state is the
22
+ * current choice's description unless `valueDescriptions` words it
23
+ * differently. Requesting a disabled or unknown choice is ignored with a
24
+ * warning, as a disabled field ignores any request. Inside the field the value
25
+ * starts at `initialValue`, else at the first enabled choice.
26
+ */
27
+ export function useOneOfField(opts, registry) {
28
+ const choices = resolve(opts.choices, []).map((c) => resolveChoice(opts.id, c));
29
+ const inside = !opts.onChange && !opts.commit;
30
+ const initialValue = opts.initialValue ?? choices.find((c) => c.enabled)?.value;
31
+ const field = useFieldCore('oneOf', inside ? { ...opts, initialValue } : opts, registry);
32
+ const current = choices.find((c) => c.value === field.value);
33
+ const setValue = (next) => {
34
+ const choice = choices.find((c) => c.value === next);
35
+ if (!choice) {
36
+ console.warn(`Field '${opts.id}': '${next}' is not one of its choices`);
37
+ return;
38
+ }
39
+ if (!choice.enabled) {
40
+ console.warn(`Field '${opts.id}': choice '${next}' is disabled — ${choice.whyDisabled ?? '(no reason)'}`);
41
+ return;
42
+ }
43
+ field.setValue(next);
44
+ };
45
+ return {
46
+ ...field,
47
+ valueDescription: field.valueDescription ?? current?.description,
48
+ setValue,
49
+ choices,
50
+ current,
51
+ };
52
+ }
53
+ //# sourceMappingURL=useOneOfField.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useOneOfField.js","sourceRoot":"","sources":["../src/useOneOfField.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,gBAAgB,EAAE,YAAY,EAAyC,MAAM,YAAY,CAAC;AACnG,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AACtD,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AA6CvC,SAAS,aAAa,CAAmB,OAAe,EAAE,MAAsB;IAC9E,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;KACpB,CAAC;AACJ,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,aAAa,CAC3B,IAAmC,EACnC,QAAuC;IAEvC,MAAM,OAAO,GAAG,OAAO,CAAmB,IAAI,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,aAAa,CAAC,IAAI,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC;IAClG,MAAM,MAAM,GAAG,CAAC,IAAI,CAAC,QAAQ,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC;IAC9C,MAAM,YAAY,GAAG,IAAI,CAAC,YAAY,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,EAAE,KAAK,CAAC;IAChF,MAAM,KAAK,GAAG,YAAY,CAAgB,OAAO,EAAE,MAAM,CAAC,CAAC,CAAC,EAAE,GAAG,IAAI,EAAE,YAAY,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC;IACxG,MAAM,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,KAAK,KAAK,CAAC,KAAK,CAAC,CAAC;IAE7D,MAAM,QAAQ,GAAG,CAAC,IAAO,EAAE,EAAE;QAC3B,MAAM,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,KAAK,IAAI,CAAC,CAAC;QACrD,IAAI,CAAC,MAAM,EAAE,CAAC;YACZ,OAAO,CAAC,IAAI,CAAC,UAAU,IAAI,CAAC,EAAE,OAAO,IAAI,6BAA6B,CAAC,CAAC;YACxE,OAAO;QACT,CAAC;QACD,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;YACpB,OAAO,CAAC,IAAI,CAAC,UAAU,IAAI,CAAC,EAAE,cAAc,IAAI,mBAAmB,MAAM,CAAC,WAAW,IAAI,aAAa,EAAE,CAAC,CAAC;YAC1G,OAAO;QACT,CAAC;QACD,KAAK,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;IACvB,CAAC,CAAC;IAEF,OAAO;QACL,GAAG,KAAK;QACR,gBAAgB,EAAE,KAAK,CAAC,gBAAgB,IAAI,OAAO,EAAE,WAAW;QAChE,QAAQ;QACR,OAAO;QACP,OAAO;KACR,CAAC;AACJ,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vultus-core",
3
- "version": "0.2.1",
3
+ "version": "0.3.0",
4
4
  "license": "Apache-2.0",
5
5
  "description": "Framework-neutral action model + hooks for vultus (antd-free by design)",
6
6
  "repository": {
package/src/index.ts CHANGED
@@ -14,5 +14,8 @@ export { useBoolField, type BoolFieldControls, type BoolFieldOptions } from './u
14
14
  export {
15
15
  useMixedBoolField, aggregateBool, type MixedBool, type MixedBoolFieldControls, type MixedBoolFieldOptions,
16
16
  } from './useMixedBoolField.js';
17
+ export {
18
+ useOneOfField, type OneOfChoice, type OneOfChoiceControls, type OneOfFieldControls, type OneOfFieldOptions,
19
+ } from './useOneOfField.js';
17
20
  export { InternalLinkContext, useInternalLink, type InternalLinkComponent } from './LinkContext.js';
18
21
  export { MessageSinkContext, useMessageSink, type MessageSink } from './MessageSink.js';
@@ -0,0 +1,103 @@
1
+ import type { ReactNode } from 'react';
2
+ import { calculateEnabled, useFieldCore, type FieldControls, type FieldOptions } from './field.js';
3
+ import { getReason, getVerdict } from './decision.js';
4
+ import { resolve } from './resolve.js';
5
+ import type { Decision, ErrorRoutesRegistry, ValueOrFn } from './types.js';
6
+
7
+ /** One choice of a one-of field (moxb's BindOneOfChoice). */
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
+ }
24
+
25
+ /** A choice as the widgets get it. */
26
+ export interface OneOfChoiceControls<T extends string = string> {
27
+ value: T;
28
+ label: string;
29
+ description: string | undefined;
30
+ icon: ReactNode | undefined;
31
+ enabled: boolean;
32
+ whyDisabled: string | undefined;
33
+ variant: 'default' | 'primary' | 'danger';
34
+ group: string | undefined;
35
+ }
36
+
37
+ export type OneOfFieldOptions<T extends string = string, RouteId extends string = string> =
38
+ FieldOptions<T, T, RouteId> & {
39
+ /** The choices, in order; static or derived. */
40
+ choices: ValueOrFn<OneOfChoice<T>[]>;
41
+ };
42
+
43
+ export interface OneOfFieldControls<T extends string = string> extends FieldControls<T, T> {
44
+ choices: OneOfChoiceControls<T>[];
45
+ /** The choice the value is, if it is one of them. */
46
+ current: OneOfChoiceControls<T> | undefined;
47
+ }
48
+
49
+ function resolveChoice<T extends string>(fieldId: string, choice: OneOfChoice<T>): OneOfChoiceControls<T> {
50
+ const decision = calculateEnabled(`${fieldId}.${choice.value}`, choice.enabled, choice.disabled);
51
+ const enabled = getVerdict(decision, true);
52
+ return {
53
+ value: choice.value,
54
+ label: choice.label ?? choice.value,
55
+ description: choice.description,
56
+ icon: choice.icon,
57
+ enabled,
58
+ whyDisabled: enabled ? undefined : getReason(decision),
59
+ variant: choice.variant ?? 'default',
60
+ group: choice.group,
61
+ };
62
+ }
63
+
64
+ /**
65
+ * A field whose value is one of a list of choices (moxb's OneOf). Rendered by
66
+ * vultus-antd's OneOfSelect and OneOfSegmented, which take the same controls.
67
+ * Each choice may be disabled with a reason; the field's current state is the
68
+ * current choice's description unless `valueDescriptions` words it
69
+ * differently. Requesting a disabled or unknown choice is ignored with a
70
+ * warning, as a disabled field ignores any request. Inside the field the value
71
+ * starts at `initialValue`, else at the first enabled choice.
72
+ */
73
+ export function useOneOfField<T extends string = string, RouteId extends string = string>(
74
+ opts: OneOfFieldOptions<T, RouteId>,
75
+ registry?: ErrorRoutesRegistry<RouteId>,
76
+ ): OneOfFieldControls<T> {
77
+ const choices = resolve<OneOfChoice<T>[]>(opts.choices, []).map((c) => resolveChoice(opts.id, c));
78
+ const inside = !opts.onChange && !opts.commit;
79
+ const initialValue = opts.initialValue ?? choices.find((c) => c.enabled)?.value;
80
+ const field = useFieldCore<T, T, RouteId>('oneOf', inside ? { ...opts, initialValue } : opts, registry);
81
+ const current = choices.find((c) => c.value === field.value);
82
+
83
+ const setValue = (next: T) => {
84
+ const choice = choices.find((c) => c.value === next);
85
+ if (!choice) {
86
+ console.warn(`Field '${opts.id}': '${next}' is not one of its choices`);
87
+ return;
88
+ }
89
+ if (!choice.enabled) {
90
+ console.warn(`Field '${opts.id}': choice '${next}' is disabled — ${choice.whyDisabled ?? '(no reason)'}`);
91
+ return;
92
+ }
93
+ field.setValue(next);
94
+ };
95
+
96
+ return {
97
+ ...field,
98
+ valueDescription: field.valueDescription ?? current?.description,
99
+ setValue,
100
+ choices,
101
+ current,
102
+ };
103
+ }