@getrheo/flow-ui-state 1.0.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/dist/index.d.ts +7 -0
- package/dist/index.js +23 -0
- package/dist/index.js.map +1 -0
- package/dist/publish-exports.json +14 -0
- package/dist/screenCheckboxAck.d.ts +23 -0
- package/dist/screenCheckboxAck.js +80 -0
- package/dist/screenCheckboxAck.js.map +1 -0
- package/dist/screenInputDraft.d.ts +74 -0
- package/dist/screenInputDraft.js +106 -0
- package/dist/screenInputDraft.js.map +1 -0
- package/package.json +60 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { InputDraft, InputValidity, ScreenInputDraftProvider, useScreenInputDraft, useScreenInputValidity } from './screenInputDraft.js';
|
|
2
|
+
export { ScreenCheckboxAckProvider, computeCheckboxContinueBlocked, listBlockingCheckboxes, useCheckboxContinueBlocked, useScreenCheckboxAck } from './screenCheckboxAck.js';
|
|
3
|
+
import 'react/jsx-runtime';
|
|
4
|
+
import 'react';
|
|
5
|
+
import '@getrheo/contracts/screens';
|
|
6
|
+
import '@getrheo/flow-runtime/stateMachine';
|
|
7
|
+
import '@getrheo/contracts/layers';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ScreenInputDraftProvider,
|
|
3
|
+
useScreenInputDraft,
|
|
4
|
+
useScreenInputValidity
|
|
5
|
+
} from "./screenInputDraft";
|
|
6
|
+
import {
|
|
7
|
+
ScreenCheckboxAckProvider,
|
|
8
|
+
useScreenCheckboxAck,
|
|
9
|
+
useCheckboxContinueBlocked,
|
|
10
|
+
computeCheckboxContinueBlocked,
|
|
11
|
+
listBlockingCheckboxes
|
|
12
|
+
} from "./screenCheckboxAck";
|
|
13
|
+
export {
|
|
14
|
+
ScreenCheckboxAckProvider,
|
|
15
|
+
ScreenInputDraftProvider,
|
|
16
|
+
computeCheckboxContinueBlocked,
|
|
17
|
+
listBlockingCheckboxes,
|
|
18
|
+
useCheckboxContinueBlocked,
|
|
19
|
+
useScreenCheckboxAck,
|
|
20
|
+
useScreenInputDraft,
|
|
21
|
+
useScreenInputValidity
|
|
22
|
+
};
|
|
23
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["export {\n ScreenInputDraftProvider,\n useScreenInputDraft,\n useScreenInputValidity,\n} from './screenInputDraft';\nexport type { InputDraft, InputValidity } from './screenInputDraft';\nexport {\n ScreenCheckboxAckProvider,\n useScreenCheckboxAck,\n useCheckboxContinueBlocked,\n computeCheckboxContinueBlocked,\n listBlockingCheckboxes,\n} from './screenCheckboxAck';\n"],"mappings":"AAAA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAEP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;","names":[]}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
+
import { ReactNode } from 'react';
|
|
3
|
+
import { Screen } from '@getrheo/contracts/screens';
|
|
4
|
+
import { CheckboxLayer } from '@getrheo/contracts/layers';
|
|
5
|
+
|
|
6
|
+
/** True when any `blocking` checkbox on the screen is unchecked. */
|
|
7
|
+
declare const computeCheckboxContinueBlocked: (screen: Screen, checked: Record<string, boolean>) => boolean;
|
|
8
|
+
type ScreenCheckboxAckCtxValue = {
|
|
9
|
+
checked: Record<string, boolean>;
|
|
10
|
+
toggle: (fieldKey: string) => void;
|
|
11
|
+
/** Snapshot all checkbox values on this screen for `screen_commit`. */
|
|
12
|
+
snapshotValues: () => Record<string, boolean>;
|
|
13
|
+
blockingContinue: boolean;
|
|
14
|
+
};
|
|
15
|
+
declare const ScreenCheckboxAckProvider: ({ screen, children, }: {
|
|
16
|
+
screen: Screen;
|
|
17
|
+
children: ReactNode;
|
|
18
|
+
}) => react_jsx_runtime.JSX.Element;
|
|
19
|
+
declare const useScreenCheckboxAck: () => ScreenCheckboxAckCtxValue | null;
|
|
20
|
+
declare const useCheckboxContinueBlocked: () => boolean;
|
|
21
|
+
declare const listBlockingCheckboxes: (screen: Screen) => CheckboxLayer[];
|
|
22
|
+
|
|
23
|
+
export { ScreenCheckboxAckProvider, computeCheckboxContinueBlocked, listBlockingCheckboxes, useCheckboxContinueBlocked, useScreenCheckboxAck };
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { jsx } from "react/jsx-runtime";
|
|
2
|
+
import {
|
|
3
|
+
createContext,
|
|
4
|
+
useCallback,
|
|
5
|
+
useContext,
|
|
6
|
+
useEffect,
|
|
7
|
+
useMemo,
|
|
8
|
+
useState
|
|
9
|
+
} from "react";
|
|
10
|
+
import { walkScreen } from "@getrheo/flow-runtime/layers";
|
|
11
|
+
const collectCheckboxFieldKeys = (screen) => {
|
|
12
|
+
const out = [];
|
|
13
|
+
walkScreen(screen, (l) => {
|
|
14
|
+
if (l.kind === "checkbox") out.push(l.fieldKey);
|
|
15
|
+
});
|
|
16
|
+
return out;
|
|
17
|
+
};
|
|
18
|
+
const computeCheckboxContinueBlocked = (screen, checked) => {
|
|
19
|
+
let blocked = false;
|
|
20
|
+
walkScreen(screen, (l) => {
|
|
21
|
+
if (l.kind !== "checkbox" || !l.blocking) return;
|
|
22
|
+
if (!checked[l.fieldKey]) blocked = true;
|
|
23
|
+
});
|
|
24
|
+
return blocked;
|
|
25
|
+
};
|
|
26
|
+
const ScreenCheckboxAckCtx = createContext(null);
|
|
27
|
+
const ScreenCheckboxAckProvider = ({
|
|
28
|
+
screen,
|
|
29
|
+
children
|
|
30
|
+
}) => {
|
|
31
|
+
const keys = useMemo(() => collectCheckboxFieldKeys(screen), [screen]);
|
|
32
|
+
const keysSig = keys.join("\0");
|
|
33
|
+
const [checked, setChecked] = useState(
|
|
34
|
+
() => Object.fromEntries(keys.map((k) => [k, false]))
|
|
35
|
+
);
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
setChecked(Object.fromEntries(keys.map((k) => [k, false])));
|
|
38
|
+
}, [screen.id, keysSig]);
|
|
39
|
+
const blockingContinue = useMemo(
|
|
40
|
+
() => computeCheckboxContinueBlocked(screen, checked),
|
|
41
|
+
[screen, checked]
|
|
42
|
+
);
|
|
43
|
+
const toggle = useCallback((fieldKey) => {
|
|
44
|
+
setChecked((prev) => ({ ...prev, [fieldKey]: !(prev[fieldKey] ?? false) }));
|
|
45
|
+
}, []);
|
|
46
|
+
const snapshotValues = useCallback(() => {
|
|
47
|
+
const out = {};
|
|
48
|
+
walkScreen(screen, (l) => {
|
|
49
|
+
if (l.kind === "checkbox") {
|
|
50
|
+
out[l.fieldKey] = !!checked[l.fieldKey];
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
return out;
|
|
54
|
+
}, [screen, checked]);
|
|
55
|
+
const value = useMemo(
|
|
56
|
+
() => ({ checked, toggle, snapshotValues, blockingContinue }),
|
|
57
|
+
[checked, toggle, snapshotValues, blockingContinue]
|
|
58
|
+
);
|
|
59
|
+
return /* @__PURE__ */ jsx(ScreenCheckboxAckCtx.Provider, { value, children });
|
|
60
|
+
};
|
|
61
|
+
const useScreenCheckboxAck = () => useContext(ScreenCheckboxAckCtx);
|
|
62
|
+
const useCheckboxContinueBlocked = () => {
|
|
63
|
+
const ctx = useContext(ScreenCheckboxAckCtx);
|
|
64
|
+
return ctx?.blockingContinue ?? false;
|
|
65
|
+
};
|
|
66
|
+
const listBlockingCheckboxes = (screen) => {
|
|
67
|
+
const out = [];
|
|
68
|
+
walkScreen(screen, (l) => {
|
|
69
|
+
if (l.kind === "checkbox" && l.blocking) out.push(l);
|
|
70
|
+
});
|
|
71
|
+
return out;
|
|
72
|
+
};
|
|
73
|
+
export {
|
|
74
|
+
ScreenCheckboxAckProvider,
|
|
75
|
+
computeCheckboxContinueBlocked,
|
|
76
|
+
listBlockingCheckboxes,
|
|
77
|
+
useCheckboxContinueBlocked,
|
|
78
|
+
useScreenCheckboxAck
|
|
79
|
+
};
|
|
80
|
+
//# sourceMappingURL=screenCheckboxAck.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/screenCheckboxAck.tsx"],"sourcesContent":["import {\n createContext,\n useCallback,\n useContext,\n useEffect,\n useMemo,\n useState,\n type ReactNode,\n} from 'react';\nimport type { Screen } from '@getrheo/contracts/screens';\nimport { walkScreen } from '@getrheo/flow-runtime/layers';\nimport type { CheckboxLayer } from '@getrheo/contracts/layers';\n\nconst collectCheckboxFieldKeys = (screen: Screen): string[] => {\n const out: string[] = [];\n walkScreen(screen, (l) => {\n if (l.kind === 'checkbox') out.push(l.fieldKey);\n });\n return out;\n};\n\n/** True when any `blocking` checkbox on the screen is unchecked. */\nexport const computeCheckboxContinueBlocked = (\n screen: Screen,\n checked: Record<string, boolean>,\n): boolean => {\n let blocked = false;\n walkScreen(screen, (l) => {\n if (l.kind !== 'checkbox' || !l.blocking) return;\n if (!checked[l.fieldKey]) blocked = true;\n });\n return blocked;\n};\n\ntype ScreenCheckboxAckCtxValue = {\n checked: Record<string, boolean>;\n toggle: (fieldKey: string) => void;\n /** Snapshot all checkbox values on this screen for `screen_commit`. */\n snapshotValues: () => Record<string, boolean>;\n blockingContinue: boolean;\n};\n\nconst ScreenCheckboxAckCtx = createContext<ScreenCheckboxAckCtxValue | null>(null);\n\nexport const ScreenCheckboxAckProvider = ({\n screen,\n children,\n}: {\n screen: Screen;\n children: ReactNode;\n}) => {\n const keys = useMemo(() => collectCheckboxFieldKeys(screen), [screen]);\n const keysSig = keys.join('\\0');\n\n const [checked, setChecked] = useState<Record<string, boolean>>(() =>\n Object.fromEntries(keys.map((k) => [k, false])),\n );\n\n useEffect(() => {\n setChecked(Object.fromEntries(keys.map((k) => [k, false])));\n }, [screen.id, keysSig]);\n\n const blockingContinue = useMemo(\n () => computeCheckboxContinueBlocked(screen, checked),\n [screen, checked],\n );\n\n const toggle = useCallback((fieldKey: string) => {\n setChecked((prev) => ({ ...prev, [fieldKey]: !(prev[fieldKey] ?? false) }));\n }, []);\n\n const snapshotValues = useCallback((): Record<string, boolean> => {\n const out: Record<string, boolean> = {};\n walkScreen(screen, (l) => {\n if (l.kind === 'checkbox') {\n out[l.fieldKey] = !!checked[l.fieldKey];\n }\n });\n return out;\n }, [screen, checked]);\n\n const value = useMemo<ScreenCheckboxAckCtxValue>(\n () => ({ checked, toggle, snapshotValues, blockingContinue }),\n [checked, toggle, snapshotValues, blockingContinue],\n );\n\n return <ScreenCheckboxAckCtx.Provider value={value}>{children}</ScreenCheckboxAckCtx.Provider>;\n};\n\nexport const useScreenCheckboxAck = (): ScreenCheckboxAckCtxValue | null =>\n useContext(ScreenCheckboxAckCtx);\n\nexport const useCheckboxContinueBlocked = (): boolean => {\n const ctx = useContext(ScreenCheckboxAckCtx);\n return ctx?.blockingContinue ?? false;\n};\n\nexport const listBlockingCheckboxes = (screen: Screen): CheckboxLayer[] => {\n const out: CheckboxLayer[] = [];\n walkScreen(screen, (l) => {\n if (l.kind === 'checkbox' && l.blocking) out.push(l);\n });\n return out;\n};\n"],"mappings":"AAsFS;AAtFT;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAEK;AAEP,SAAS,kBAAkB;AAG3B,MAAM,2BAA2B,CAAC,WAA6B;AAC7D,QAAM,MAAgB,CAAC;AACvB,aAAW,QAAQ,CAAC,MAAM;AACxB,QAAI,EAAE,SAAS,WAAY,KAAI,KAAK,EAAE,QAAQ;AAAA,EAChD,CAAC;AACD,SAAO;AACT;AAGO,MAAM,iCAAiC,CAC5C,QACA,YACY;AACZ,MAAI,UAAU;AACd,aAAW,QAAQ,CAAC,MAAM;AACxB,QAAI,EAAE,SAAS,cAAc,CAAC,EAAE,SAAU;AAC1C,QAAI,CAAC,QAAQ,EAAE,QAAQ,EAAG,WAAU;AAAA,EACtC,CAAC;AACD,SAAO;AACT;AAUA,MAAM,uBAAuB,cAAgD,IAAI;AAE1E,MAAM,4BAA4B,CAAC;AAAA,EACxC;AAAA,EACA;AACF,MAGM;AACJ,QAAM,OAAO,QAAQ,MAAM,yBAAyB,MAAM,GAAG,CAAC,MAAM,CAAC;AACrE,QAAM,UAAU,KAAK,KAAK,IAAI;AAE9B,QAAM,CAAC,SAAS,UAAU,IAAI;AAAA,IAAkC,MAC9D,OAAO,YAAY,KAAK,IAAI,CAAC,MAAM,CAAC,GAAG,KAAK,CAAC,CAAC;AAAA,EAChD;AAEA,YAAU,MAAM;AACd,eAAW,OAAO,YAAY,KAAK,IAAI,CAAC,MAAM,CAAC,GAAG,KAAK,CAAC,CAAC,CAAC;AAAA,EAC5D,GAAG,CAAC,OAAO,IAAI,OAAO,CAAC;AAEvB,QAAM,mBAAmB;AAAA,IACvB,MAAM,+BAA+B,QAAQ,OAAO;AAAA,IACpD,CAAC,QAAQ,OAAO;AAAA,EAClB;AAEA,QAAM,SAAS,YAAY,CAAC,aAAqB;AAC/C,eAAW,CAAC,UAAU,EAAE,GAAG,MAAM,CAAC,QAAQ,GAAG,EAAE,KAAK,QAAQ,KAAK,OAAO,EAAE;AAAA,EAC5E,GAAG,CAAC,CAAC;AAEL,QAAM,iBAAiB,YAAY,MAA+B;AAChE,UAAM,MAA+B,CAAC;AACtC,eAAW,QAAQ,CAAC,MAAM;AACxB,UAAI,EAAE,SAAS,YAAY;AACzB,YAAI,EAAE,QAAQ,IAAI,CAAC,CAAC,QAAQ,EAAE,QAAQ;AAAA,MACxC;AAAA,IACF,CAAC;AACD,WAAO;AAAA,EACT,GAAG,CAAC,QAAQ,OAAO,CAAC;AAEpB,QAAM,QAAQ;AAAA,IACZ,OAAO,EAAE,SAAS,QAAQ,gBAAgB,iBAAiB;AAAA,IAC3D,CAAC,SAAS,QAAQ,gBAAgB,gBAAgB;AAAA,EACpD;AAEA,SAAO,oBAAC,qBAAqB,UAArB,EAA8B,OAAe,UAAS;AAChE;AAEO,MAAM,uBAAuB,MAClC,WAAW,oBAAoB;AAE1B,MAAM,6BAA6B,MAAe;AACvD,QAAM,MAAM,WAAW,oBAAoB;AAC3C,SAAO,KAAK,oBAAoB;AAClC;AAEO,MAAM,yBAAyB,CAAC,WAAoC;AACzE,QAAM,MAAuB,CAAC;AAC9B,aAAW,QAAQ,CAAC,MAAM;AACxB,QAAI,EAAE,SAAS,cAAc,EAAE,SAAU,KAAI,KAAK,CAAC;AAAA,EACrD,CAAC;AACD,SAAO;AACT;","names":[]}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
+
import { ReactNode } from 'react';
|
|
3
|
+
import { Screen } from '@getrheo/contracts/screens';
|
|
4
|
+
import { StepResponseCore } from '@getrheo/flow-runtime/stateMachine';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* In-progress draft value for the screen's lone input layer. Mirrors the
|
|
8
|
+
* `StepResponse` variants the input layers used to emit themselves but
|
|
9
|
+
* without the `text`'s `classification` (added at submit time from the
|
|
10
|
+
* input layer's own `classification`).
|
|
11
|
+
*/
|
|
12
|
+
type InputDraft = {
|
|
13
|
+
kind: 'choice';
|
|
14
|
+
choiceId: string;
|
|
15
|
+
} | {
|
|
16
|
+
kind: 'multiChoice';
|
|
17
|
+
choiceIds: string[];
|
|
18
|
+
} | {
|
|
19
|
+
kind: 'text';
|
|
20
|
+
value: string;
|
|
21
|
+
} | {
|
|
22
|
+
kind: 'scale';
|
|
23
|
+
value: number;
|
|
24
|
+
};
|
|
25
|
+
type InputValidity = {
|
|
26
|
+
valid: boolean;
|
|
27
|
+
reason?: string;
|
|
28
|
+
};
|
|
29
|
+
type ScreenInputDraftCtxValue = {
|
|
30
|
+
draft: InputDraft | null;
|
|
31
|
+
setDraft: (next: InputDraft | null) => void;
|
|
32
|
+
validity: InputValidity;
|
|
33
|
+
/**
|
|
34
|
+
* Materialise the current draft into a `StepResponseCore` ready to be
|
|
35
|
+
* fed into the host flow's state machine (or wrapped in `screen_commit`).
|
|
36
|
+
* Returns `null` when the draft is empty or the screen has no input.
|
|
37
|
+
*/
|
|
38
|
+
toResponse: () => StepResponseCore | null;
|
|
39
|
+
};
|
|
40
|
+
/**
|
|
41
|
+
* Compute validity for a draft against the screen's input layer
|
|
42
|
+
* constraints. Centralised here so web sim and RN SDK stay in lockstep.
|
|
43
|
+
*
|
|
44
|
+
* Exported for unit tests; production code should consume validity via
|
|
45
|
+
* `useScreenInputValidity()`.
|
|
46
|
+
*/
|
|
47
|
+
declare const computeValidity: (screen: Screen, draft: InputDraft | null) => InputValidity;
|
|
48
|
+
/**
|
|
49
|
+
* Convert a draft to a fully-populated `StepResponseCore` using the screen's
|
|
50
|
+
* input layer for context (e.g. text classification).
|
|
51
|
+
*
|
|
52
|
+
* Exported for unit tests; production code should call `toResponse()`
|
|
53
|
+
* from the context value.
|
|
54
|
+
*/
|
|
55
|
+
declare const draftToResponse: (screen: Screen, draft: InputDraft | null) => StepResponseCore | null;
|
|
56
|
+
declare const ScreenInputDraftProvider: ({ screen, children, }: {
|
|
57
|
+
screen: Screen;
|
|
58
|
+
children: ReactNode;
|
|
59
|
+
}) => react_jsx_runtime.JSX.Element;
|
|
60
|
+
/**
|
|
61
|
+
* Read the screen-level input draft. Returns `null` when called outside
|
|
62
|
+
* a `ScreenInputDraftProvider` so consumers can degrade gracefully
|
|
63
|
+
* (e.g. when an input view is rendered standalone in a builder preview).
|
|
64
|
+
*/
|
|
65
|
+
declare const useScreenInputDraft: () => ScreenInputDraftCtxValue | null;
|
|
66
|
+
/**
|
|
67
|
+
* Convenience hook for callers that only care whether the current
|
|
68
|
+
* screen's input is in a submittable state. Treats absence of a
|
|
69
|
+
* provider as "valid" so non-input screens (or standalone previews)
|
|
70
|
+
* never block submission.
|
|
71
|
+
*/
|
|
72
|
+
declare const useScreenInputValidity: () => InputValidity;
|
|
73
|
+
|
|
74
|
+
export { type InputDraft, type InputValidity, ScreenInputDraftProvider, computeValidity, draftToResponse, useScreenInputDraft, useScreenInputValidity };
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { jsx } from "react/jsx-runtime";
|
|
2
|
+
import {
|
|
3
|
+
createContext,
|
|
4
|
+
useCallback,
|
|
5
|
+
useContext,
|
|
6
|
+
useEffect,
|
|
7
|
+
useMemo,
|
|
8
|
+
useState
|
|
9
|
+
} from "react";
|
|
10
|
+
import { findInputLayer } from "@getrheo/flow-runtime/layers";
|
|
11
|
+
import { snapScaleValue, scaleValueInRange, scaleValueIsOnStep } from "@getrheo/flow-runtime/scaleValidation";
|
|
12
|
+
import { validateTextInputValue } from "@getrheo/flow-runtime/textInputValidation";
|
|
13
|
+
const ScreenInputDraftCtx = createContext(null);
|
|
14
|
+
const computeValidity = (screen, draft) => {
|
|
15
|
+
const input = findInputLayer(screen);
|
|
16
|
+
if (!input) return { valid: true };
|
|
17
|
+
if (!draft) return { valid: false, reason: "No input provided yet" };
|
|
18
|
+
switch (input.kind) {
|
|
19
|
+
case "text_input": {
|
|
20
|
+
if (draft.kind !== "text") return { valid: false, reason: "Wrong draft kind" };
|
|
21
|
+
const v = validateTextInputValue(input, draft.value);
|
|
22
|
+
return v.ok ? { valid: true } : { valid: false, reason: v.reason };
|
|
23
|
+
}
|
|
24
|
+
case "scale_input": {
|
|
25
|
+
if (draft.kind !== "scale") return { valid: false, reason: "Wrong draft kind" };
|
|
26
|
+
if (!scaleValueInRange(input, draft.value)) {
|
|
27
|
+
return { valid: false, reason: "Value out of range" };
|
|
28
|
+
}
|
|
29
|
+
if (!scaleValueIsOnStep(input, draft.value)) {
|
|
30
|
+
return { valid: false, reason: "Value does not align with step" };
|
|
31
|
+
}
|
|
32
|
+
return { valid: true };
|
|
33
|
+
}
|
|
34
|
+
case "multiple_choice": {
|
|
35
|
+
if (draft.kind !== "multiChoice") return { valid: false, reason: "Wrong draft kind" };
|
|
36
|
+
const min = input.minSelections ?? 1;
|
|
37
|
+
if (draft.choiceIds.length < min) {
|
|
38
|
+
return { valid: false, reason: `Select at least ${min}` };
|
|
39
|
+
}
|
|
40
|
+
const max = input.maxSelections;
|
|
41
|
+
if (max !== void 0 && draft.choiceIds.length > max) {
|
|
42
|
+
return { valid: false, reason: `Select at most ${max}` };
|
|
43
|
+
}
|
|
44
|
+
return { valid: true };
|
|
45
|
+
}
|
|
46
|
+
case "single_choice": {
|
|
47
|
+
if (draft.kind !== "choice") return { valid: false, reason: "Wrong draft kind" };
|
|
48
|
+
return { valid: true };
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
const draftToResponse = (screen, draft) => {
|
|
53
|
+
if (!draft) return null;
|
|
54
|
+
const input = findInputLayer(screen);
|
|
55
|
+
if (!input) return null;
|
|
56
|
+
if (draft.kind === "choice") return { kind: "choice", choiceId: draft.choiceId };
|
|
57
|
+
if (draft.kind === "multiChoice") return { kind: "multiChoice", choiceIds: draft.choiceIds };
|
|
58
|
+
if (draft.kind === "text") {
|
|
59
|
+
if (input.kind !== "text_input") return null;
|
|
60
|
+
return { kind: "text", value: draft.value, classification: input.classification };
|
|
61
|
+
}
|
|
62
|
+
if (draft.kind === "scale") {
|
|
63
|
+
if (input.kind !== "scale_input") return null;
|
|
64
|
+
return { kind: "scale", value: draft.value };
|
|
65
|
+
}
|
|
66
|
+
return null;
|
|
67
|
+
};
|
|
68
|
+
const initialDraftForScreen = (screen) => {
|
|
69
|
+
const input = findInputLayer(screen);
|
|
70
|
+
if (input?.kind === "scale_input") {
|
|
71
|
+
return {
|
|
72
|
+
kind: "scale",
|
|
73
|
+
value: snapScaleValue(input, input.defaultValue ?? input.min)
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
return null;
|
|
77
|
+
};
|
|
78
|
+
const ScreenInputDraftProvider = ({
|
|
79
|
+
screen,
|
|
80
|
+
children
|
|
81
|
+
}) => {
|
|
82
|
+
const [draft, setDraft] = useState(() => initialDraftForScreen(screen));
|
|
83
|
+
useEffect(() => {
|
|
84
|
+
setDraft(initialDraftForScreen(screen));
|
|
85
|
+
}, [screen.id]);
|
|
86
|
+
const validity = useMemo(() => computeValidity(screen, draft), [screen, draft]);
|
|
87
|
+
const toResponse = useCallback(() => draftToResponse(screen, draft), [screen, draft]);
|
|
88
|
+
const value = useMemo(
|
|
89
|
+
() => ({ draft, setDraft, validity, toResponse }),
|
|
90
|
+
[draft, validity, toResponse]
|
|
91
|
+
);
|
|
92
|
+
return /* @__PURE__ */ jsx(ScreenInputDraftCtx.Provider, { value, children });
|
|
93
|
+
};
|
|
94
|
+
const useScreenInputDraft = () => useContext(ScreenInputDraftCtx);
|
|
95
|
+
const useScreenInputValidity = () => {
|
|
96
|
+
const ctx = useContext(ScreenInputDraftCtx);
|
|
97
|
+
return ctx?.validity ?? { valid: true };
|
|
98
|
+
};
|
|
99
|
+
export {
|
|
100
|
+
ScreenInputDraftProvider,
|
|
101
|
+
computeValidity,
|
|
102
|
+
draftToResponse,
|
|
103
|
+
useScreenInputDraft,
|
|
104
|
+
useScreenInputValidity
|
|
105
|
+
};
|
|
106
|
+
//# sourceMappingURL=screenInputDraft.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/screenInputDraft.tsx"],"sourcesContent":["import {\n createContext,\n useCallback,\n useContext,\n useEffect,\n useMemo,\n useState,\n} from 'react';\nimport type { ReactNode } from 'react';\nimport type { Screen } from '@getrheo/contracts/screens';\nimport { findInputLayer } from '@getrheo/flow-runtime/layers';\nimport { snapScaleValue, scaleValueInRange, scaleValueIsOnStep } from '@getrheo/flow-runtime/scaleValidation';\nimport { validateTextInputValue } from '@getrheo/flow-runtime/textInputValidation';\nimport type { StepResponseCore } from '@getrheo/flow-runtime/stateMachine';\n\n/**\n * In-progress draft value for the screen's lone input layer. Mirrors the\n * `StepResponse` variants the input layers used to emit themselves but\n * without the `text`'s `classification` (added at submit time from the\n * input layer's own `classification`).\n */\nexport type InputDraft =\n | { kind: 'choice'; choiceId: string }\n | { kind: 'multiChoice'; choiceIds: string[] }\n | { kind: 'text'; value: string }\n | { kind: 'scale'; value: number };\n\nexport type InputValidity = { valid: boolean; reason?: string };\n\ntype ScreenInputDraftCtxValue = {\n draft: InputDraft | null;\n setDraft: (next: InputDraft | null) => void;\n validity: InputValidity;\n /**\n * Materialise the current draft into a `StepResponseCore` ready to be\n * fed into the host flow's state machine (or wrapped in `screen_commit`).\n * Returns `null` when the draft is empty or the screen has no input.\n */\n toResponse: () => StepResponseCore | null;\n};\n\nconst ScreenInputDraftCtx = createContext<ScreenInputDraftCtxValue | null>(null);\n\n/**\n * Compute validity for a draft against the screen's input layer\n * constraints. Centralised here so web sim and RN SDK stay in lockstep.\n *\n * Exported for unit tests; production code should consume validity via\n * `useScreenInputValidity()`.\n */\nexport const computeValidity = (screen: Screen, draft: InputDraft | null): InputValidity => {\n const input = findInputLayer(screen);\n if (!input) return { valid: true };\n if (!draft) return { valid: false, reason: 'No input provided yet' };\n\n switch (input.kind) {\n case 'text_input': {\n if (draft.kind !== 'text') return { valid: false, reason: 'Wrong draft kind' };\n const v = validateTextInputValue(input, draft.value);\n return v.ok ? { valid: true } : { valid: false, reason: v.reason };\n }\n case 'scale_input': {\n if (draft.kind !== 'scale') return { valid: false, reason: 'Wrong draft kind' };\n if (!scaleValueInRange(input, draft.value)) {\n return { valid: false, reason: 'Value out of range' };\n }\n if (!scaleValueIsOnStep(input, draft.value)) {\n return { valid: false, reason: 'Value does not align with step' };\n }\n return { valid: true };\n }\n case 'multiple_choice': {\n if (draft.kind !== 'multiChoice') return { valid: false, reason: 'Wrong draft kind' };\n const min = input.minSelections ?? 1;\n if (draft.choiceIds.length < min) {\n return { valid: false, reason: `Select at least ${min}` };\n }\n const max = input.maxSelections;\n if (max !== undefined && draft.choiceIds.length > max) {\n return { valid: false, reason: `Select at most ${max}` };\n }\n return { valid: true };\n }\n case 'single_choice': {\n if (draft.kind !== 'choice') return { valid: false, reason: 'Wrong draft kind' };\n return { valid: true };\n }\n }\n};\n\n/**\n * Convert a draft to a fully-populated `StepResponseCore` using the screen's\n * input layer for context (e.g. text classification).\n *\n * Exported for unit tests; production code should call `toResponse()`\n * from the context value.\n */\nexport const draftToResponse = (\n screen: Screen,\n draft: InputDraft | null,\n): StepResponseCore | null => {\n if (!draft) return null;\n const input = findInputLayer(screen);\n if (!input) return null;\n\n if (draft.kind === 'choice') return { kind: 'choice', choiceId: draft.choiceId };\n if (draft.kind === 'multiChoice') return { kind: 'multiChoice', choiceIds: draft.choiceIds };\n if (draft.kind === 'text') {\n if (input.kind !== 'text_input') return null;\n return { kind: 'text', value: draft.value, classification: input.classification };\n }\n if (draft.kind === 'scale') {\n if (input.kind !== 'scale_input') return null;\n return { kind: 'scale', value: draft.value };\n }\n return null;\n};\n\nconst initialDraftForScreen = (screen: Screen): InputDraft | null => {\n const input = findInputLayer(screen);\n if (input?.kind === 'scale_input') {\n return {\n kind: 'scale',\n value: snapScaleValue(input, input.defaultValue ?? input.min),\n };\n }\n return null;\n};\n\nexport const ScreenInputDraftProvider = ({\n screen,\n children,\n}: {\n screen: Screen;\n children: ReactNode;\n}) => {\n const [draft, setDraft] = useState<InputDraft | null>(() => initialDraftForScreen(screen));\n\n useEffect(() => {\n setDraft(initialDraftForScreen(screen));\n }, [screen.id]);\n\n const validity = useMemo(() => computeValidity(screen, draft), [screen, draft]);\n const toResponse = useCallback(() => draftToResponse(screen, draft), [screen, draft]);\n\n const value = useMemo<ScreenInputDraftCtxValue>(\n () => ({ draft, setDraft, validity, toResponse }),\n [draft, validity, toResponse],\n );\n\n return <ScreenInputDraftCtx.Provider value={value}>{children}</ScreenInputDraftCtx.Provider>;\n};\n\n/**\n * Read the screen-level input draft. Returns `null` when called outside\n * a `ScreenInputDraftProvider` so consumers can degrade gracefully\n * (e.g. when an input view is rendered standalone in a builder preview).\n */\nexport const useScreenInputDraft = (): ScreenInputDraftCtxValue | null =>\n useContext(ScreenInputDraftCtx);\n\n/**\n * Convenience hook for callers that only care whether the current\n * screen's input is in a submittable state. Treats absence of a\n * provider as \"valid\" so non-input screens (or standalone previews)\n * never block submission.\n */\nexport const useScreenInputValidity = (): InputValidity => {\n const ctx = useContext(ScreenInputDraftCtx);\n return ctx?.validity ?? { valid: true };\n};\n"],"mappings":"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","names":[]}
|
package/package.json
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@getrheo/flow-ui-state",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "./dist/index.js",
|
|
6
|
+
"types": "./dist/index.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"import": "./dist/index.js",
|
|
11
|
+
"default": "./dist/index.js"
|
|
12
|
+
},
|
|
13
|
+
"./draft": {
|
|
14
|
+
"types": "./dist/screenInputDraft.d.ts",
|
|
15
|
+
"import": "./dist/screenInputDraft.js",
|
|
16
|
+
"default": "./dist/screenInputDraft.js"
|
|
17
|
+
},
|
|
18
|
+
"./checkbox": {
|
|
19
|
+
"types": "./dist/screenCheckboxAck.d.ts",
|
|
20
|
+
"import": "./dist/screenCheckboxAck.js",
|
|
21
|
+
"default": "./dist/screenCheckboxAck.js"
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"@getrheo/contracts": "1.0.0",
|
|
26
|
+
"@getrheo/flow-runtime": "1.0.0"
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"@types/node": "^22.10.1",
|
|
30
|
+
"@types/react": "^19.0.1",
|
|
31
|
+
"@types/react-dom": "^19.0.1",
|
|
32
|
+
"react": "^19.0.0",
|
|
33
|
+
"react-dom": "^19.0.0",
|
|
34
|
+
"typescript": "^5.6.3",
|
|
35
|
+
"vitest": "^3.2.6",
|
|
36
|
+
"@rheo/config": "0.1.0"
|
|
37
|
+
},
|
|
38
|
+
"peerDependencies": {
|
|
39
|
+
"react": ">=18"
|
|
40
|
+
},
|
|
41
|
+
"license": "MIT",
|
|
42
|
+
"repository": {
|
|
43
|
+
"type": "git",
|
|
44
|
+
"url": "git+https://github.com/madeinusmate/onboardly.git",
|
|
45
|
+
"directory": "packages/flow-ui-state"
|
|
46
|
+
},
|
|
47
|
+
"files": [
|
|
48
|
+
"dist",
|
|
49
|
+
"README.md"
|
|
50
|
+
],
|
|
51
|
+
"publishConfig": {
|
|
52
|
+
"access": "public"
|
|
53
|
+
},
|
|
54
|
+
"scripts": {
|
|
55
|
+
"lint": "eslint .",
|
|
56
|
+
"typecheck": "tsc --noEmit",
|
|
57
|
+
"test": "vitest run",
|
|
58
|
+
"build": "node ../../scripts/build-publishable-package.mjs"
|
|
59
|
+
}
|
|
60
|
+
}
|