@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.
@@ -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,14 @@
1
+ [
2
+ {
3
+ "exportKey": ".",
4
+ "distFile": "./dist/index.js"
5
+ },
6
+ {
7
+ "exportKey": "./draft",
8
+ "distFile": "./dist/screenInputDraft.js"
9
+ },
10
+ {
11
+ "exportKey": "./checkbox",
12
+ "distFile": "./dist/screenCheckboxAck.js"
13
+ }
14
+ ]
@@ -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
+ }