vultus-core 0.2.1 → 0.4.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 +29 -2
- package/dist/Components.d.ts +30 -0
- package/dist/Components.d.ts.map +1 -0
- package/dist/Components.js +28 -0
- package/dist/Components.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/useOneOfField.d.ts +51 -0
- package/dist/useOneOfField.d.ts.map +1 -0
- package/dist/useOneOfField.js +53 -0
- package/dist/useOneOfField.js.map +1 -0
- package/package.json +1 -1
- package/src/Components.tsx +47 -0
- package/src/index.ts +7 -0
- package/src/useOneOfField.ts +103 -0
package/README.md
CHANGED
|
@@ -22,8 +22,10 @@ npm install vultus-core
|
|
|
22
22
|
`parseApiError`, `routeApiError`, `ActionErrorContext` / `useActionErrorCtx`,
|
|
23
23
|
`useAction`, `useActionList`, the fields (`useBoolField`, `useFieldCore` and
|
|
24
24
|
their types), `InternalLinkContext` / `useInternalLink` /
|
|
25
|
-
`InternalLinkComponent`,
|
|
26
|
-
`MessageSink
|
|
25
|
+
`InternalLinkComponent`, `MessageSinkContext` / `useMessageSink` /
|
|
26
|
+
`MessageSink`, and `VultusComponentsContext` / `useVultusComponents` /
|
|
27
|
+
`SpinnerScope` / `useSpinnerKey` / `VultusComponents` / `SpinnerProps` /
|
|
28
|
+
`SpinnerComponent`.
|
|
27
29
|
- `./link` — `InternalLinkContext`, `useInternalLink`, and the
|
|
28
30
|
`InternalLinkComponent` type on their own, for consumers that only need the
|
|
29
31
|
link-injection seam.
|
|
@@ -61,6 +63,31 @@ and can be asked for `true` or `false` only (the field core separates the shown
|
|
|
61
63
|
requestable one); a toggle from mixed requests `true`. `aggregateBool(values)` derives the
|
|
62
64
|
value; with no items at all, disable the field with a reason instead.
|
|
63
65
|
|
|
66
|
+
`useOneOfField` is a field whose value is one of a list of `choices` (static or derived):
|
|
67
|
+
each `{ value, label?, description?, icon?, enabled?/disabled?, variant? }`. A choice may
|
|
68
|
+
be disabled with a reason (a `Decision`, as a field's), and be `primary` or `danger` like an
|
|
69
|
+
action's variant; `description` (markdown) says what choosing it does and is also the field's
|
|
70
|
+
current state while it is chosen, unless `valueDescriptions` words that state differently.
|
|
71
|
+
Requesting a disabled or unknown choice is ignored with a warning. Storage is as for the
|
|
72
|
+
boolean field (inside: `initialValue`, default the first enabled choice); `confirmation` may
|
|
73
|
+
depend on the requested choice. vultus-antd's `OneOfSelect` and `OneOfSegmented` take the
|
|
74
|
+
same field.
|
|
75
|
+
|
|
76
|
+
## Components
|
|
77
|
+
|
|
78
|
+
`VultusComponentsContext` (default `{}`) / `useVultusComponents()` is how an app lends its own
|
|
79
|
+
UI to vultus/optio in place of their native indicators — today just `Spinner` (`SpinnerProps`:
|
|
80
|
+
`spinnerKey`, `size?`, `label?`; `VultusComponents.Spinner?: SpinnerComponent`). vultus-antd's
|
|
81
|
+
`VultusProvider` merges a `components` prop into the outer context value; without one, the
|
|
82
|
+
outer value (or this default) applies, as `linkComponent` does today.
|
|
83
|
+
|
|
84
|
+
Every call site that may render a lent `Spinner` asks for its key through `useSpinnerKey(localKey)`
|
|
85
|
+
rather than hardcoding it, so an app can scope a whole subtree's keys to one instance:
|
|
86
|
+
`<SpinnerScope name="optio:process:p1">` makes every `useSpinnerKey` call inside it return
|
|
87
|
+
`'optio:process:p1/' + localKey`; scopes nest by joining names with `/`, outermost first.
|
|
88
|
+
Library-local keys are namespaced (`vultus:...`, `optio:...`) so they never collide with an
|
|
89
|
+
app's own keys.
|
|
90
|
+
|
|
64
91
|
## Distribution
|
|
65
92
|
|
|
66
93
|
This package ships compiled ESM (`dist/`) with TypeScript declarations to
|
|
@@ -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"}
|
package/dist/index.d.ts
CHANGED
|
@@ -9,6 +9,8 @@ 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';
|
|
15
|
+
export { VultusComponentsContext, useVultusComponents, SpinnerScope, useSpinnerKey, type VultusComponents, type SpinnerProps, type SpinnerComponent, } from './Components.js';
|
|
14
16
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -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;AACxF,OAAO,EACL,uBAAuB,EAAE,mBAAmB,EAAE,YAAY,EAAE,aAAa,EACzE,KAAK,gBAAgB,EAAE,KAAK,YAAY,EAAE,KAAK,gBAAgB,GAChE,MAAM,iBAAiB,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -9,6 +9,8 @@ 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';
|
|
15
|
+
export { VultusComponentsContext, useVultusComponents, SpinnerScope, useSpinnerKey, } from './Components.js';
|
|
14
16
|
//# 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;AACxF,OAAO,EACL,uBAAuB,EAAE,mBAAmB,EAAE,YAAY,EAAE,aAAa,GAE1E,MAAM,iBAAiB,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
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { createContext, useContext, useMemo, type ComponentType, type ReactNode } from 'react';
|
|
2
|
+
|
|
3
|
+
/** A spinner an app lends to vultus/optio, rendered in place of their native loading indicators. */
|
|
4
|
+
export interface SpinnerProps {
|
|
5
|
+
/** The spinner's identity: stable across re-renders and remounts, unique on the page (after scoping). */
|
|
6
|
+
spinnerKey: string;
|
|
7
|
+
/** antd's names (the height of antd's Spin indicator) or a height in px. */
|
|
8
|
+
size?: 'small' | 'default' | 'large' | number;
|
|
9
|
+
/** Accessible label; the spinner's own default when omitted. */
|
|
10
|
+
label?: string;
|
|
11
|
+
}
|
|
12
|
+
export type SpinnerComponent = ComponentType<SpinnerProps>;
|
|
13
|
+
|
|
14
|
+
/** Components an app lends to the libraries. */
|
|
15
|
+
export interface VultusComponents {
|
|
16
|
+
Spinner?: SpinnerComponent;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* The components an app lends to vultus/optio in place of their own UI. Default is `{}`
|
|
21
|
+
* (none lent) so vultus-core stays framework-neutral; vultus-antd's `VultusProvider` merges
|
|
22
|
+
* in whatever an app passes as `components`.
|
|
23
|
+
*/
|
|
24
|
+
export const VultusComponentsContext = createContext<VultusComponents>({});
|
|
25
|
+
|
|
26
|
+
export function useVultusComponents(): VultusComponents {
|
|
27
|
+
return useContext(VultusComponentsContext);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** The scope path (segment names, outermost first) that `useSpinnerKey` prefixes onto local keys. */
|
|
31
|
+
const SpinnerScopeContext = createContext<string[]>([]);
|
|
32
|
+
|
|
33
|
+
/** Prefixes every spinner key below with `name` (scopes nest: outer/inner). */
|
|
34
|
+
export function SpinnerScope({ name, children }: { name: string; children?: ReactNode }) {
|
|
35
|
+
const outer = useContext(SpinnerScopeContext);
|
|
36
|
+
// Memoized so consumers below (useSpinnerKey, via SpinnerScopeContext) don't re-render
|
|
37
|
+
// on every SpinnerScope re-render — only when the scope path actually changes. Matters
|
|
38
|
+
// once whole conversation views sit inside a SpinnerScope.
|
|
39
|
+
const scope = useMemo(() => [...outer, name], [outer, name]);
|
|
40
|
+
return <SpinnerScopeContext.Provider value={scope}>{children}</SpinnerScopeContext.Provider>;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** The scoped key for a library-local key: 'outer/inner/local', or 'local' outside any scope. */
|
|
44
|
+
export function useSpinnerKey(localKey: string): string {
|
|
45
|
+
const scope = useContext(SpinnerScopeContext);
|
|
46
|
+
return [...scope, localKey].join('/');
|
|
47
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -14,5 +14,12 @@ 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';
|
|
22
|
+
export {
|
|
23
|
+
VultusComponentsContext, useVultusComponents, SpinnerScope, useSpinnerKey,
|
|
24
|
+
type VultusComponents, type SpinnerProps, type SpinnerComponent,
|
|
25
|
+
} from './Components.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
|
+
}
|