@rocapine/react-native-onboarding-ui 1.36.2 → 1.38.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.
Files changed (66) hide show
  1. package/dist/UI/Pages/ComposableScreen/elements/BaseBoxProps.d.ts +26 -0
  2. package/dist/UI/Pages/ComposableScreen/elements/BaseBoxProps.d.ts.map +1 -1
  3. package/dist/UI/Pages/ComposableScreen/elements/BaseBoxProps.js +2 -0
  4. package/dist/UI/Pages/ComposableScreen/elements/BaseBoxProps.js.map +1 -1
  5. package/dist/UI/Pages/ComposableScreen/elements/ButtonElement.d.ts +106 -67
  6. package/dist/UI/Pages/ComposableScreen/elements/ButtonElement.d.ts.map +1 -1
  7. package/dist/UI/Pages/ComposableScreen/elements/ButtonElement.js +11 -59
  8. package/dist/UI/Pages/ComposableScreen/elements/ButtonElement.js.map +1 -1
  9. package/dist/UI/Pages/ComposableScreen/elements/CarouselElement.d.ts +24 -0
  10. package/dist/UI/Pages/ComposableScreen/elements/CarouselElement.d.ts.map +1 -1
  11. package/dist/UI/Pages/ComposableScreen/elements/CheckboxGroupElement.d.ts +24 -0
  12. package/dist/UI/Pages/ComposableScreen/elements/CheckboxGroupElement.d.ts.map +1 -1
  13. package/dist/UI/Pages/ComposableScreen/elements/DatePickerElement.d.ts +24 -0
  14. package/dist/UI/Pages/ComposableScreen/elements/DatePickerElement.d.ts.map +1 -1
  15. package/dist/UI/Pages/ComposableScreen/elements/IconElement.d.ts +24 -0
  16. package/dist/UI/Pages/ComposableScreen/elements/IconElement.d.ts.map +1 -1
  17. package/dist/UI/Pages/ComposableScreen/elements/ImageElement.d.ts +24 -0
  18. package/dist/UI/Pages/ComposableScreen/elements/ImageElement.d.ts.map +1 -1
  19. package/dist/UI/Pages/ComposableScreen/elements/InputElement.d.ts +24 -0
  20. package/dist/UI/Pages/ComposableScreen/elements/InputElement.d.ts.map +1 -1
  21. package/dist/UI/Pages/ComposableScreen/elements/KeyboardAvoidingViewElement.d.ts +24 -0
  22. package/dist/UI/Pages/ComposableScreen/elements/KeyboardAvoidingViewElement.d.ts.map +1 -1
  23. package/dist/UI/Pages/ComposableScreen/elements/LottieElement.d.ts +24 -0
  24. package/dist/UI/Pages/ComposableScreen/elements/LottieElement.d.ts.map +1 -1
  25. package/dist/UI/Pages/ComposableScreen/elements/ProgressIndicatorElement.d.ts +32 -0
  26. package/dist/UI/Pages/ComposableScreen/elements/ProgressIndicatorElement.d.ts.map +1 -1
  27. package/dist/UI/Pages/ComposableScreen/elements/ProgressIndicatorElement.js +71 -50
  28. package/dist/UI/Pages/ComposableScreen/elements/ProgressIndicatorElement.js.map +1 -1
  29. package/dist/UI/Pages/ComposableScreen/elements/ProgressiveBlurImageElement.d.ts +24 -0
  30. package/dist/UI/Pages/ComposableScreen/elements/ProgressiveBlurImageElement.d.ts.map +1 -1
  31. package/dist/UI/Pages/ComposableScreen/elements/RadioGroupElement.d.ts +24 -0
  32. package/dist/UI/Pages/ComposableScreen/elements/RadioGroupElement.d.ts.map +1 -1
  33. package/dist/UI/Pages/ComposableScreen/elements/RichTextElement.d.ts +24 -0
  34. package/dist/UI/Pages/ComposableScreen/elements/RichTextElement.d.ts.map +1 -1
  35. package/dist/UI/Pages/ComposableScreen/elements/RiveElement.d.ts +24 -0
  36. package/dist/UI/Pages/ComposableScreen/elements/RiveElement.d.ts.map +1 -1
  37. package/dist/UI/Pages/ComposableScreen/elements/SafeAreaViewElement.d.ts +24 -0
  38. package/dist/UI/Pages/ComposableScreen/elements/SafeAreaViewElement.d.ts.map +1 -1
  39. package/dist/UI/Pages/ComposableScreen/elements/ScrollViewElement.d.ts +24 -0
  40. package/dist/UI/Pages/ComposableScreen/elements/ScrollViewElement.d.ts.map +1 -1
  41. package/dist/UI/Pages/ComposableScreen/elements/StackElement.d.ts +24 -0
  42. package/dist/UI/Pages/ComposableScreen/elements/StackElement.d.ts.map +1 -1
  43. package/dist/UI/Pages/ComposableScreen/elements/TextElement.d.ts +25 -1
  44. package/dist/UI/Pages/ComposableScreen/elements/TextElement.d.ts.map +1 -1
  45. package/dist/UI/Pages/ComposableScreen/elements/VideoElement.d.ts +24 -0
  46. package/dist/UI/Pages/ComposableScreen/elements/VideoElement.d.ts.map +1 -1
  47. package/dist/UI/Pages/ComposableScreen/elements/ZStackElement.d.ts +24 -0
  48. package/dist/UI/Pages/ComposableScreen/elements/ZStackElement.d.ts.map +1 -1
  49. package/dist/UI/Pages/ComposableScreen/elements/actions.d.ts +72 -0
  50. package/dist/UI/Pages/ComposableScreen/elements/actions.d.ts.map +1 -0
  51. package/dist/UI/Pages/ComposableScreen/elements/actions.js +24 -0
  52. package/dist/UI/Pages/ComposableScreen/elements/actions.js.map +1 -0
  53. package/dist/UI/Pages/ComposableScreen/elements/renderElement.d.ts.map +1 -1
  54. package/dist/UI/Pages/ComposableScreen/elements/renderElement.js +47 -4
  55. package/dist/UI/Pages/ComposableScreen/elements/renderElement.js.map +1 -1
  56. package/dist/UI/Pages/ComposableScreen/elements/runActions.d.ts +4 -0
  57. package/dist/UI/Pages/ComposableScreen/elements/runActions.d.ts.map +1 -0
  58. package/dist/UI/Pages/ComposableScreen/elements/runActions.js +98 -0
  59. package/dist/UI/Pages/ComposableScreen/elements/runActions.js.map +1 -0
  60. package/package.json +1 -1
  61. package/src/UI/Pages/ComposableScreen/elements/BaseBoxProps.ts +3 -0
  62. package/src/UI/Pages/ComposableScreen/elements/ButtonElement.tsx +15 -91
  63. package/src/UI/Pages/ComposableScreen/elements/ProgressIndicatorElement.tsx +61 -34
  64. package/src/UI/Pages/ComposableScreen/elements/actions.ts +53 -0
  65. package/src/UI/Pages/ComposableScreen/elements/renderElement.tsx +59 -4
  66. package/src/UI/Pages/ComposableScreen/elements/runActions.ts +109 -0
@@ -0,0 +1,98 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.runActions = runActions;
4
+ const expression_1 = require("./expression");
5
+ // Decode a multi-select variable's stored value (JSON-encoded string[], as
6
+ // written by CheckboxGroup) into a string array. Tolerates undefined / non-array
7
+ // / unparseable input by returning [].
8
+ function decodeArrayValue(raw) {
9
+ if (!raw)
10
+ return [];
11
+ try {
12
+ const parsed = JSON.parse(raw);
13
+ return Array.isArray(parsed) ? parsed.map(String) : [];
14
+ }
15
+ catch (_a) {
16
+ return [];
17
+ }
18
+ }
19
+ // Sequentially runs a list of press actions against the render context. Shared
20
+ // by `Button` (its `actions`) and the generic `onPress` on every UIElement
21
+ // (wired centrally in renderElement.tsx). Semantics:
22
+ // - "continue" → advance the onboarding; terminal (stops the loop).
23
+ // - {setVariable} → write a variable (expression-evaluated when valueMode === "expression").
24
+ // - {custom} → invoke the host-registered customAction with the requested
25
+ // variables; warns if unregistered, aborts the loop on throw.
26
+ async function runActions(actions, ctx) {
27
+ var _a, _b;
28
+ const { onContinue, setVariable, customActions, variables } = ctx;
29
+ for (const act of actions) {
30
+ if (act === "continue") {
31
+ onContinue();
32
+ return;
33
+ }
34
+ if (act.type === "setVariable") {
35
+ let value;
36
+ let kind;
37
+ if (act.valueMode === "expression") {
38
+ const computed = (0, expression_1.evaluateSetVariableExpression)(act.value, variables);
39
+ value = computed.value;
40
+ kind = computed.kind;
41
+ }
42
+ else {
43
+ value = act.value;
44
+ kind = act.kind;
45
+ }
46
+ if (act.arrayOp) {
47
+ // Multi-select set operation on the JSON-encoded string[] used by
48
+ // CheckboxGroup. value/label are the single member being added/removed.
49
+ const entry = variables[act.name];
50
+ const curValues = decodeArrayValue(entry === null || entry === void 0 ? void 0 : entry.value);
51
+ const curLabels = (entry === null || entry === void 0 ? void 0 : entry.label) ? entry.label.split(", ") : [];
52
+ const memberLabel = (_a = act.label) !== null && _a !== void 0 ? _a : value;
53
+ const idx = curValues.indexOf(value);
54
+ const present = idx !== -1;
55
+ const add = act.arrayOp === "append" || (act.arrayOp === "toggle" && !present);
56
+ let nextValues;
57
+ let nextLabels;
58
+ if (add) {
59
+ // Dedup: appending an already-present member is a no-op.
60
+ nextValues = present ? curValues : [...curValues, value];
61
+ nextLabels = present ? curLabels : [...curLabels, memberLabel];
62
+ }
63
+ else {
64
+ // remove, or toggle-when-present
65
+ nextValues = curValues.filter((_, i) => i !== idx);
66
+ nextLabels =
67
+ present && idx < curLabels.length
68
+ ? curLabels.filter((_, i) => i !== idx)
69
+ : curLabels.filter((l) => l !== memberLabel);
70
+ }
71
+ setVariable(act.name, {
72
+ value: JSON.stringify(nextValues),
73
+ label: nextLabels.join(", "),
74
+ });
75
+ continue;
76
+ }
77
+ setVariable(act.name, { value, label: act.label, kind });
78
+ continue;
79
+ }
80
+ const handler = customActions[act.function];
81
+ if (!handler) {
82
+ console.warn(`[ComposableScreen] No customAction registered for "${act.function}"`);
83
+ continue;
84
+ }
85
+ const requested = (_b = act.variables) !== null && _b !== void 0 ? _b : [];
86
+ const vars = {};
87
+ for (const name of requested)
88
+ vars[name] = variables[name];
89
+ try {
90
+ await handler({ variables: vars });
91
+ }
92
+ catch (err) {
93
+ console.error(`[ComposableScreen] customAction "${act.function}" threw:`, err);
94
+ return;
95
+ }
96
+ }
97
+ }
98
+ //# sourceMappingURL=runActions.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"runActions.js","sourceRoot":"","sources":["../../../../../src/UI/Pages/ComposableScreen/elements/runActions.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rocapine/react-native-onboarding-ui",
3
- "version": "1.36.2",
3
+ "version": "1.38.0",
4
4
  "description": "UI components and renderers for Rocapine Onboarding Studio - Built on top of the headless SDK",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -1,4 +1,5 @@
1
1
  import { z } from "zod";
2
+ import { type ButtonAction, ButtonActionSchema } from "./actions";
2
3
 
3
4
  export type GradientStop = {
4
5
  color: string;
@@ -289,6 +290,7 @@ export type BaseBoxProps = {
289
290
  elevation?: number;
290
291
  transform?: ElementTransform;
291
292
  animation?: ElementAnimation;
293
+ onPress?: ButtonAction[];
292
294
  };
293
295
 
294
296
  export const BaseBoxPropsSchema = z.object({
@@ -323,4 +325,5 @@ export const BaseBoxPropsSchema = z.object({
323
325
  elevation: z.number().min(0).optional(),
324
326
  transform: TransformSchema.optional(),
325
327
  animation: ElementAnimationSchema.optional(),
328
+ onPress: z.array(ButtonActionSchema).optional(),
326
329
  });
@@ -6,7 +6,6 @@ import {
6
6
  evaluateCondition,
7
7
  type LeafCondition,
8
8
  type ConditionGroup,
9
- type ComposableVariableKind,
10
9
  LeafConditionSchema,
11
10
  ConditionGroupSchema,
12
11
  } from "@rocapine/react-native-onboarding";
@@ -14,59 +13,23 @@ import { BaseBoxProps, BaseBoxPropsSchema } from "./BaseBoxProps";
14
13
  import { UIElement } from "../types";
15
14
  import { RenderContext, buildShadowStyle, dim, resolveInheritedFontFamily } from "./shared";
16
15
  import { GradientBox } from "./GradientBox";
17
- import { ComposableVariableEntry } from "../../../Provider/OnboardingProgressProvider";
18
- import { evaluateSetVariableExpression } from "./expression";
19
16
  import { triggerHaptic, type HapticStyle } from "./haptics";
17
+ import {
18
+ type ButtonAction,
19
+ ButtonActionSchema,
20
+ } from "./actions";
21
+ import { runActions } from "./runActions";
20
22
 
21
- export type CustomButtonAction = {
22
- type: "custom";
23
- function: string;
24
- variables?: string[];
25
- };
26
-
27
- export const CustomButtonActionSchema = z.object({
28
- type: z.literal("custom"),
29
- function: z.string().min(1, "function must not be empty"),
30
- variables: z.array(z.string()).optional(),
31
- });
32
-
33
- export type SetVariableButtonAction = {
34
- type: "setVariable";
35
- name: string;
36
- value: string;
37
- label?: string;
38
- /**
39
- * When `"expression"`, `value` is parsed as an arithmetic expression with
40
- * `{{var}}` references, numeric literals, and `+ - * /` (parens supported).
41
- * On parse failure, falls back to plain interpolation (string).
42
- * Defaults to `"literal"` — `value` stored verbatim.
43
- */
44
- valueMode?: "literal" | "expression";
45
- /**
46
- * Tags the stored variable's underlying type. In `"literal"` mode this is
47
- * used as-is. In `"expression"` mode the inferred result kind is used
48
- * unless `kind` is explicitly set (ignored — expression mode derives kind
49
- * from evaluation).
50
- */
51
- kind?: ComposableVariableKind;
52
- };
53
-
54
- export const SetVariableButtonActionSchema = z.object({
55
- type: z.literal("setVariable"),
56
- name: z.string().min(1, "name must not be empty"),
57
- value: z.string(),
58
- label: z.string().optional(),
59
- valueMode: z.enum(["literal", "expression"]).optional(),
60
- kind: z.enum(["int", "float", "string"]).optional(),
61
- });
62
-
63
- export type ButtonAction = "continue" | CustomButtonAction | SetVariableButtonAction;
64
-
65
- export const ButtonActionSchema = z.union([
66
- z.literal("continue"),
23
+ // `ButtonAction` and its variants live in `./actions` (shared with the generic
24
+ // `onPress` on BaseBoxProps). Re-exported for back-compat.
25
+ export {
26
+ type CustomButtonAction,
67
27
  CustomButtonActionSchema,
28
+ type SetVariableButtonAction,
68
29
  SetVariableButtonActionSchema,
69
- ]);
30
+ } from "./actions";
31
+ export type { ButtonAction };
32
+ export { ButtonActionSchema };
70
33
 
71
34
  type ButtonOverridableProps = BaseBoxProps & {
72
35
  variant?: "filled" | "outlined" | "ghost";
@@ -145,7 +108,7 @@ type Props = {
145
108
  };
146
109
 
147
110
  export const ButtonElementComponent = ({ element, ctx }: Props): React.ReactElement => {
148
- const { theme, onContinue, customActions, variables, setVariable } = ctx;
111
+ const { theme, variables } = ctx;
149
112
  const flatVariables = useMemo(
150
113
  () =>
151
114
  Object.fromEntries(
@@ -164,46 +127,7 @@ export const ButtonElementComponent = ({ element, ctx }: Props): React.ReactElem
164
127
  const { actions, action } = element.props;
165
128
  const effective: ButtonAction[] =
166
129
  actions ?? (action === "continue" ? ["continue"] : []);
167
-
168
- for (const act of effective) {
169
- if (act === "continue") {
170
- onContinue();
171
- return;
172
- }
173
- if (act.type === "setVariable") {
174
- let value: string;
175
- let kind: ComposableVariableKind | undefined;
176
- if (act.valueMode === "expression") {
177
- const computed = evaluateSetVariableExpression(act.value, variables);
178
- value = computed.value;
179
- kind = computed.kind;
180
- } else {
181
- value = act.value;
182
- kind = act.kind;
183
- }
184
- setVariable(act.name, { value, label: act.label, kind });
185
- continue;
186
- }
187
- const handler = customActions[act.function];
188
- if (!handler) {
189
- console.warn(
190
- `[ComposableScreen] No customAction registered for "${act.function}"`
191
- );
192
- continue;
193
- }
194
- const requested = act.variables ?? [];
195
- const vars: Record<string, ComposableVariableEntry | undefined> = {};
196
- for (const name of requested) vars[name] = variables[name];
197
- try {
198
- await handler({ variables: vars });
199
- } catch (err) {
200
- console.error(
201
- `[ComposableScreen] customAction "${act.function}" threw:`,
202
- err
203
- );
204
- return;
205
- }
206
- }
130
+ await runActions(effective, ctx);
207
131
  };
208
132
 
209
133
  // State overrides are merged over base props. disabledStyle wins over the
@@ -27,6 +27,10 @@ export type ProgressIndicatorElementProps = BaseBoxProps & {
27
27
  autoplay?: boolean;
28
28
  loop?: boolean;
29
29
  initialValue?: number;
30
+ minValue?: number;
31
+ maxValue?: number;
32
+ step?: number;
33
+ labelSuffix?: string;
30
34
  duration?: number;
31
35
  delay?: number;
32
36
  easing?: ProgressEasing;
@@ -43,10 +47,14 @@ const ProgressEasingSchema = z.enum(["linear", "ease-in", "ease-out", "ease-in-o
43
47
  export const ProgressIndicatorElementPropsSchema = BaseBoxPropsSchema.extend({
44
48
  variant: z.enum(["linear", "circular"]).optional(),
45
49
  variableName: z.string().min(1).optional(),
46
- value: z.number().min(0).max(100).optional(),
50
+ value: z.number().optional(),
47
51
  autoplay: z.boolean().optional(),
48
52
  loop: z.boolean().optional(),
49
- initialValue: z.number().min(0).max(100).optional(),
53
+ initialValue: z.number().optional(),
54
+ minValue: z.number().optional(),
55
+ maxValue: z.number().optional(),
56
+ step: z.number().gt(0).optional(),
57
+ labelSuffix: z.string().optional(),
50
58
  duration: z.number().min(0).optional(),
51
59
  delay: z.number().min(0).optional(),
52
60
  easing: ProgressEasingSchema.optional(),
@@ -60,7 +68,7 @@ export const ProgressIndicatorElementPropsSchema = BaseBoxPropsSchema.extend({
60
68
 
61
69
  const AnimatedCircle = Animated.createAnimatedComponent(Circle);
62
70
 
63
- const clamp = (n: number): number => Math.max(0, Math.min(100, n));
71
+ const clamp = (n: number, min: number, max: number): number => Math.max(min, Math.min(max, n));
64
72
 
65
73
  type ProgressUIElement = Extract<UIElement, { type: "ProgressIndicator" }>;
66
74
 
@@ -74,7 +82,12 @@ export const ProgressIndicatorElementComponent = ({ element, ctx }: Props): Reac
74
82
  const { props } = element;
75
83
 
76
84
  const variant = props.variant ?? "linear";
77
- const initialValue = clamp(props.initialValue ?? 0);
85
+ const minValue = props.minValue ?? 0;
86
+ const maxValue = props.maxValue ?? 100;
87
+ const range = maxValue - minValue;
88
+ const step = props.step && props.step > 0 ? props.step : 1;
89
+ const labelSuffix = props.labelSuffix ?? "%";
90
+ const initialValue = clamp(props.initialValue ?? minValue, minValue, maxValue);
78
91
  const duration = props.duration ?? 1000;
79
92
  const delay = props.delay ?? 0;
80
93
  const easing = EASING_MAP[props.easing ?? "ease-in-out"];
@@ -85,55 +98,67 @@ export const ProgressIndicatorElementComponent = ({ element, ctx }: Props): Reac
85
98
  const trackColor = props.trackColor ?? theme.colors.neutral.lower;
86
99
  const labelColor = props.labelColor ?? theme.colors.text.primary;
87
100
 
88
- // Bound variable value (input mode, non-autoplay) or static value.
101
+ // Snap a raw value to `step` within [minValue, maxValue]. The label and the
102
+ // written variable carry the snapped value, not a percentage.
103
+ const snap = (v: number): number =>
104
+ clamp(minValue + Math.round((v - minValue) / step) * step, minValue, maxValue);
105
+
106
+ // Bound variable value (input mode, non-autoplay) or static value. `progress`
107
+ // is the value in [minValue, maxValue]; the fill fraction is derived from it.
89
108
  const boundRaw = props.variableName ? variables[props.variableName]?.value : undefined;
90
- const boundValue = boundRaw !== undefined ? clamp(Number(boundRaw) || 0) : undefined;
91
- const target = autoplay ? 100 : clamp(boundValue ?? props.value ?? initialValue);
109
+ const boundValue = boundRaw !== undefined ? clamp(Number(boundRaw) || 0, minValue, maxValue) : undefined;
110
+ const target = autoplay ? maxValue : clamp(boundValue ?? props.value ?? initialValue, minValue, maxValue);
92
111
 
93
112
  const progress = useSharedValue(initialValue);
94
- const [percentage, setPercentage] = useState(Math.round(initialValue));
113
+ const [displayValue, setDisplayValue] = useState(snap(initialValue));
95
114
 
96
115
  // Mirror the animated value into a label + (autoplay) the bound variable.
97
- // Reaction input is the *rounded* percent, so the JS callback fires only when
98
- // the integer changes (≤100 hops/sweep) rather than every frame — avoids a
99
- // per-frame context write storm (setVariable re-renders all variable consumers).
116
+ // Reaction input is the *step-snapped* value, so the JS callback fires only
117
+ // when the snapped value changes ((maxValue-minValue)/step hops/sweep) rather
118
+ // than every frame — avoids a per-frame context write storm (setVariable
119
+ // re-renders all variable consumers). Coarse `step` for large ranges.
100
120
  const showLabel = props.showLabel ?? false;
101
121
  const variableName = props.variableName;
102
122
  const writeVariable = (v: number) => {
103
123
  if (autoplay && variableName) {
104
- setVariable(variableName, { value: String(v), kind: "int" });
124
+ setVariable(variableName, { value: String(v), kind: Number.isInteger(v) ? "int" : "float" });
105
125
  }
106
126
  };
107
127
  const writesVariable = autoplay && !!variableName;
108
128
  // The dependency array is REQUIRED. Without it reanimated tears down and
109
129
  // rebuilds this mapper on EVERY render. A looping `showLabel` indicator
110
- // re-renders ~40×/s forever (one setPercentage per frame), so its mapper would
111
- // be start/stopped ~40×/s indefinitely — perpetual churn on the UI-thread mapper
130
+ // re-renders continuously (one setDisplayValue per step hop), so its mapper
131
+ // would be start/stopped indefinitely — perpetual churn on the UI-thread mapper
112
132
  // scheduler that destabilizes *other* running animations on the same screen
113
133
  // (they visibly reset mid/after a sweep — "finishes then resets for no reason").
114
- // Recreating also resets `prev` to undefined, defeating the `rounded === prev`
134
+ // Recreating also resets `prev` to undefined, defeating the `snapped === prev`
115
135
  // guard so the JS callbacks over-fire. Keying on the values the worklet branches
116
- // on keeps the mapper stable; the JS fns it calls (setPercentage, setVariable via
117
- // writeVariable) are already stable across renders.
136
+ // on (incl. minValue/maxValue/step) keeps the mapper stable; the JS fns it calls
137
+ // (setDisplayValue, setVariable via writeVariable) are already stable across renders.
118
138
  useAnimatedReaction(
119
- () => Math.round(progress.value),
120
- (rounded, prev) => {
121
- if (rounded === prev) return;
122
- if (showLabel) runOnJS(setPercentage)(rounded);
123
- if (writesVariable) runOnJS(writeVariable)(rounded);
139
+ () => {
140
+ // Inline snap (worklet — can't call the JS `snap` closure). Captures the
141
+ // primitive minValue/maxValue/step (re-keyed via the deps array below).
142
+ const snapped = minValue + Math.round((progress.value - minValue) / step) * step;
143
+ return Math.max(minValue, Math.min(maxValue, snapped));
144
+ },
145
+ (snapped, prev) => {
146
+ if (snapped === prev) return;
147
+ if (showLabel) runOnJS(setDisplayValue)(snapped);
148
+ if (writesVariable) runOnJS(writeVariable)(snapped);
124
149
  },
125
- [showLabel, writesVariable, variableName]
150
+ [showLabel, writesVariable, variableName, minValue, maxValue, step]
126
151
  );
127
152
 
128
- // Autoplay: animate initialValue -> 100, optionally looping, after `delay`.
153
+ // Autoplay: animate initialValue -> maxValue, optionally looping, after `delay`.
129
154
  useEffect(() => {
130
155
  if (!autoplay) return;
131
156
  progress.value = initialValue;
132
- const anim = withTiming(100, { duration, easing });
157
+ const anim = withTiming(maxValue, { duration, easing });
133
158
  const looped = loop ? withRepeat(anim, -1, false) : anim;
134
159
  progress.value = delay > 0 ? withDelay(delay, looped) : looped;
135
160
  return () => cancelAnimation(progress);
136
- }, [autoplay, loop, duration, delay, easing, initialValue]);
161
+ }, [autoplay, loop, duration, delay, easing, initialValue, maxValue]);
137
162
 
138
163
  // Bound / static (non-autoplay): animate toward the current target after `delay`.
139
164
  useEffect(() => {
@@ -149,12 +174,14 @@ export const ProgressIndicatorElementComponent = ({ element, ctx }: Props): Reac
149
174
  const radius = (size - strokeWidth) / 2;
150
175
  const circumference = 2 * Math.PI * radius;
151
176
 
152
- const animatedCircleProps = useAnimatedProps(() => ({
153
- strokeDashoffset: circumference * (1 - progress.value / 100),
154
- }));
155
- const animatedFillStyle = useAnimatedStyle(() => ({
156
- width: `${progress.value}%`,
157
- }));
177
+ const animatedCircleProps = useAnimatedProps(() => {
178
+ const frac = range > 0 ? (progress.value - minValue) / range : 0;
179
+ return { strokeDashoffset: circumference * (1 - frac) };
180
+ });
181
+ const animatedFillStyle = useAnimatedStyle(() => {
182
+ const frac = range > 0 ? (progress.value - minValue) / range : 0;
183
+ return { width: `${frac * 100}%` };
184
+ });
158
185
 
159
186
  const containerStyle = {
160
187
  alignSelf: props.alignSelf,
@@ -212,7 +239,7 @@ export const ProgressIndicatorElementComponent = ({ element, ctx }: Props): Reac
212
239
  fontFamily: theme.typography.textStyles.heading2.fontFamily,
213
240
  }}
214
241
  >
215
- {percentage}%
242
+ {displayValue}{labelSuffix}
216
243
  </Text>
217
244
  </View>
218
245
  ) : null}
@@ -253,7 +280,7 @@ export const ProgressIndicatorElementComponent = ({ element, ctx }: Props): Reac
253
280
  textAlign: "right",
254
281
  }}
255
282
  >
256
- {percentage}%
283
+ {displayValue}{labelSuffix}
257
284
  </Text>
258
285
  ) : null}
259
286
  </View>
@@ -0,0 +1,53 @@
1
+ import { z } from "zod";
2
+
3
+ // Self-contained UI mirror of the headless `ButtonAction` (packages/onboarding
4
+ // src/steps/common.types.ts). Kept here as a leaf module (imports only zod) so
5
+ // both BaseBoxProps.ts (schema) and shared.ts (runActions dispatch) can depend
6
+ // on it without an import cycle. Shared by `Button.actions` and the generic
7
+ // `onPress` on every UIElement.
8
+
9
+ export type CustomButtonAction = {
10
+ type: "custom";
11
+ function: string;
12
+ variables?: string[];
13
+ };
14
+
15
+ export const CustomButtonActionSchema = z.object({
16
+ type: z.literal("custom"),
17
+ function: z.string().min(1, "function must not be empty"),
18
+ variables: z.array(z.string()).optional(),
19
+ });
20
+
21
+ export type SetVariableButtonAction = {
22
+ type: "setVariable";
23
+ name: string;
24
+ value: string;
25
+ label?: string;
26
+ valueMode?: "literal" | "expression";
27
+ kind?: "int" | "float" | "string";
28
+ /**
29
+ * Treat the target variable as the JSON-encoded `string[]` multi-select
30
+ * collection used by `CheckboxGroup` and apply `value` as a set operation
31
+ * (`"append"` / `"remove"` / `"toggle"`) instead of overwriting. `kind` is
32
+ * ignored in this mode. Omit for the default overwrite behavior.
33
+ */
34
+ arrayOp?: "append" | "remove" | "toggle";
35
+ };
36
+
37
+ export const SetVariableButtonActionSchema = z.object({
38
+ type: z.literal("setVariable"),
39
+ name: z.string().min(1, "name must not be empty"),
40
+ value: z.string(),
41
+ label: z.string().optional(),
42
+ valueMode: z.enum(["literal", "expression"]).optional(),
43
+ kind: z.enum(["int", "float", "string"]).optional(),
44
+ arrayOp: z.enum(["append", "remove", "toggle"]).optional(),
45
+ });
46
+
47
+ export type ButtonAction = "continue" | CustomButtonAction | SetVariableButtonAction;
48
+
49
+ export const ButtonActionSchema = z.union([
50
+ z.literal("continue"),
51
+ CustomButtonActionSchema,
52
+ SetVariableButtonActionSchema,
53
+ ]);
@@ -1,8 +1,10 @@
1
1
  import React from "react";
2
+ import { Pressable } from "react-native";
2
3
  import { evaluateCondition } from "@rocapine/react-native-onboarding";
3
4
  import { UIElement } from "../types";
4
5
  import { BaseBoxProps } from "./BaseBoxProps";
5
6
  import { RenderContext } from "./shared";
7
+ import { runActions } from "./runActions";
6
8
  import { StackElementComponent } from "./StackElement";
7
9
  import { TextElementComponent } from "./TextElement";
8
10
  import { RichTextElementComponent } from "./RichTextElement";
@@ -26,6 +28,19 @@ import { KeyboardAvoidingViewElementComponent } from "./KeyboardAvoidingViewElem
26
28
  import { ProgressIndicatorElementComponent } from "./ProgressIndicatorElement";
27
29
  import { AnimatedBox } from "./AnimatedBox";
28
30
 
31
+ // Element types that own their own press / focus / scroll handling. The generic
32
+ // `onPress` (BaseBoxProps) is NOT wired for these — Button/RadioGroup/Checkbox/
33
+ // DatePicker already dispatch actions or selections; Input/WheelPicker would have
34
+ // their focus / scroll-selection intercepted by an outer Pressable.
35
+ const PRESS_HANDLED_TYPES = new Set<UIElement["type"]>([
36
+ "Button",
37
+ "RadioGroup",
38
+ "CheckboxGroup",
39
+ "DatePicker",
40
+ "Input",
41
+ "WheelPicker",
42
+ ]);
43
+
29
44
  export const renderElement = (
30
45
  element: UIElement,
31
46
  ctx: RenderContext,
@@ -128,12 +143,52 @@ export const renderElement = (
128
143
  return null;
129
144
  })();
130
145
 
146
+ // Cast to BaseBoxProps: not every element's props type extends it (e.g.
147
+ // WheelPicker), but the onPress/animation/transform/flex/alignSelf fields are
148
+ // all optional BaseBoxProps members and simply read as undefined when absent.
149
+ const p = element.props as BaseBoxProps;
150
+
151
+ // Generic onPress: make any non-pressable element tappable, dispatching the
152
+ // same action list as Button via runActions. Skipped for PRESS_HANDLED_TYPES.
153
+ // A Pressable around a scroll/carousel keeps inner scrolling working — RN's
154
+ // gesture responder gives the scroll the touch when it pans.
155
+ let content: React.ReactNode = node;
156
+ if (
157
+ content !== null &&
158
+ p.onPress &&
159
+ p.onPress.length > 0 &&
160
+ !PRESS_HANDLED_TYPES.has(element.type)
161
+ ) {
162
+ const onPress = p.onPress;
163
+ // The Pressable must be layout-transparent (like AnimatedBox): forward the
164
+ // element's flex sizing so the wrapper participates in its parent's flex
165
+ // context exactly as the element would. Without this, the style-less
166
+ // Pressable sizes to content while the element's `flex`/`flexShrink` sit on
167
+ // the inner node — breaking row splits (cards overflow) and column flow.
168
+ // Mirror StackElement's `parentType === "XStack"` flexShrink:1 default.
169
+ content = (
170
+ <Pressable
171
+ key={element.id}
172
+ onPress={() => {
173
+ void runActions(onPress, ctx);
174
+ }}
175
+ style={{
176
+ flex: p.flex,
177
+ flexGrow: p.flexGrow,
178
+ flexShrink: p.flexShrink ?? (parentType === "XStack" ? 1 : undefined),
179
+ alignSelf: p.alignSelf,
180
+ }}
181
+ >
182
+ {content}
183
+ </Pressable>
184
+ );
185
+ }
186
+
131
187
  // Wrap only when motion is requested — zero overhead (no extra view) otherwise.
132
188
  // Cast to BaseBoxProps: not every element's props type extends it (e.g.
133
189
  // WheelPicker), but the animation/transform/flex/alignSelf fields are all
134
190
  // optional BaseBoxProps members and simply read as undefined when absent.
135
- const p = element.props as BaseBoxProps;
136
- if (node !== null && (p.animation || p.transform)) {
191
+ if (content !== null && (p.animation || p.transform)) {
137
192
  return (
138
193
  <AnimatedBox
139
194
  key={element.id}
@@ -142,10 +197,10 @@ export const renderElement = (
142
197
  flex={p.flex}
143
198
  alignSelf={p.alignSelf}
144
199
  >
145
- {node}
200
+ {content}
146
201
  </AnimatedBox>
147
202
  );
148
203
  }
149
204
 
150
- return node;
205
+ return content;
151
206
  };