arlo-react-native 0.1.2 → 0.1.4

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.
@@ -4,7 +4,135 @@ exports.ArloFlowRenderer = ArloFlowRenderer;
4
4
  const jsx_runtime_1 = require("react/jsx-runtime");
5
5
  const react_1 = require("react");
6
6
  const react_native_1 = require("react-native");
7
+ const react_native_safe_area_context_1 = require("react-native-safe-area-context");
8
+ const SafeView = react_native_safe_area_context_1.SafeAreaView;
7
9
  const arlo_sdk_1 = require("arlo-sdk");
10
+ const LIGHT_THEME = {
11
+ isLight: true,
12
+ textPrimary: "#111111",
13
+ textSecondary: "#555555",
14
+ inputBackground: "#ffffff",
15
+ inputBorder: "#d4d4d8",
16
+ inputText: "#111111",
17
+ pillBackground: "#ffffff",
18
+ pillBorder: "#d4d4d8",
19
+ pillText: "#111111",
20
+ pillSelectedBackground: "#111111",
21
+ pillSelectedBorder: "#111111",
22
+ pillSelectedText: "#ffffff",
23
+ sliderCardBackground: "#f4f4f5",
24
+ sliderCardBorder: "#e4e4e7",
25
+ sliderValueText: "#111111",
26
+ progressTrackBackground: "#e4e4e7",
27
+ progressFillColor: "#111111",
28
+ indicatorActive: "#111111",
29
+ indicatorInactive: "#d4d4d8",
30
+ placeholderText: "#a1a1aa",
31
+ errorText: "#dc2626",
32
+ errorBorder: "#f87171",
33
+ };
34
+ const DARK_THEME = {
35
+ isLight: false,
36
+ textPrimary: "#ffffff",
37
+ textSecondary: "#a1a1aa",
38
+ inputBackground: "#141419",
39
+ inputBorder: "#2c2c34",
40
+ inputText: "#ffffff",
41
+ pillBackground: "#15151b",
42
+ pillBorder: "#30303a",
43
+ pillText: "#f1f1f3",
44
+ pillSelectedBackground: "#ffffff",
45
+ pillSelectedBorder: "#ffffff",
46
+ pillSelectedText: "#111111",
47
+ sliderCardBackground: "#15151b",
48
+ sliderCardBorder: "#2b2b34",
49
+ sliderValueText: "#ffffff",
50
+ progressTrackBackground: "#26262b",
51
+ progressFillColor: "#ffffff",
52
+ indicatorActive: "#ffffff",
53
+ indicatorInactive: "#4b4b55",
54
+ placeholderText: "#7a7a85",
55
+ errorText: "#f59cb3",
56
+ errorBorder: "#f36b8d",
57
+ };
58
+ function findInteractiveComponent(screen, fieldKey) {
59
+ if (!screen) {
60
+ return null;
61
+ }
62
+ return (screen.components.find((component) => {
63
+ if (component.type !== "TEXT_INPUT" &&
64
+ component.type !== "SINGLE_SELECT" &&
65
+ component.type !== "MULTI_SELECT" &&
66
+ component.type !== "SLIDER") {
67
+ return false;
68
+ }
69
+ return component.props.fieldKey === fieldKey;
70
+ }) ?? null);
71
+ }
72
+ function findButtonComponent(screen, componentId) {
73
+ if (!screen) {
74
+ return null;
75
+ }
76
+ return (screen.components.find((component) => component.type === "BUTTON" && component.id === componentId) ?? null);
77
+ }
78
+ function emitAnalyticsEvent(handlers, session, snapshot, event) {
79
+ if (!handlers?.onAnalyticsEvent) {
80
+ return;
81
+ }
82
+ void handlers.onAnalyticsEvent({
83
+ session,
84
+ snapshot,
85
+ event,
86
+ });
87
+ }
88
+ /**
89
+ * Compute relative luminance of a color.
90
+ * Returns a value between 0 (black) and 1 (white).
91
+ */
92
+ function getRelativeLuminance(color) {
93
+ if (!color || color === "transparent")
94
+ return 1;
95
+ const hex = color;
96
+ // Handle rgba/rgb
97
+ if (color.startsWith("rgb")) {
98
+ const matches = color.match(/\d+(\.\d+)?/g);
99
+ if (!matches || matches.length < 3)
100
+ return 1;
101
+ const r = parseInt(matches[0], 10) / 255;
102
+ const g = parseInt(matches[1], 10) / 255;
103
+ const b = parseInt(matches[2], 10) / 255;
104
+ if (matches.length >= 4 && parseFloat(matches[3]) === 0)
105
+ return 1;
106
+ const toLinear = (c) => (c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4));
107
+ return 0.2126 * toLinear(r) + 0.7152 * toLinear(g) + 0.0722 * toLinear(b);
108
+ }
109
+ // Handle named colors (basic)
110
+ if (color === "white")
111
+ return 1;
112
+ if (color === "black")
113
+ return 0;
114
+ let clean = hex.replace("#", "");
115
+ if (clean.length === 3) {
116
+ clean = clean[0] + clean[0] + clean[1] + clean[1] + clean[2] + clean[2];
117
+ }
118
+ if (clean.length < 6)
119
+ return 1;
120
+ const r = parseInt(clean.slice(0, 2), 16) / 255;
121
+ const g = parseInt(clean.slice(2, 4), 16) / 255;
122
+ const b = parseInt(clean.slice(4, 6), 16) / 255;
123
+ if (isNaN(r) || isNaN(g) || isNaN(b))
124
+ return 1;
125
+ const toLinear = (c) => (c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4));
126
+ return 0.2126 * toLinear(r) + 0.7152 * toLinear(g) + 0.0722 * toLinear(b);
127
+ }
128
+ function getThemeForScreen(screen) {
129
+ const bg = screen.style?.backgroundColor;
130
+ if (!bg || bg === "transparent")
131
+ return LIGHT_THEME;
132
+ const luminance = getRelativeLuminance(bg);
133
+ return luminance > 0.5 ? LIGHT_THEME : DARK_THEME;
134
+ }
135
+ // ─── Helpers ────────────────────────────────────────────────────────────────
8
136
  const IMPORTED_SCREEN_KEYS = new Set([
9
137
  "__arlo_imported_code__",
10
138
  "__arlo_imported_figma__",
@@ -24,19 +152,133 @@ function getImportedPreviewScreen(screen) {
24
152
  : null;
25
153
  }
26
154
  function getScreenContainerStyle(screen) {
155
+ const padding = getScreenPadding(screen);
27
156
  return {
28
- backgroundColor: screen.style?.backgroundColor ?? "#0b0b0d",
29
- paddingTop: screen.style?.paddingTop ?? screen.style?.padding ?? 24,
30
- paddingBottom: screen.style?.paddingBottom ?? screen.style?.padding ?? 24,
31
- paddingHorizontal: screen.style?.paddingHorizontal ?? screen.style?.padding ?? 20,
157
+ backgroundColor: screen.style?.backgroundColor ?? "#FFFFFF",
158
+ paddingTop: padding.top,
159
+ paddingBottom: padding.bottom,
160
+ paddingRight: padding.right,
161
+ paddingLeft: padding.left,
32
162
  justifyContent: screen.style?.justifyContent ?? "flex-start",
33
163
  alignItems: screen.style?.alignItems ?? "stretch",
34
164
  };
35
165
  }
36
- function getComponentWrapperStyle(component, isAbsoluteScreen) {
166
+ function getScreenPadding(screen) {
167
+ const p = screen.style?.padding;
168
+ const pV = screen.style?.paddingVertical ?? p ?? (screen.layoutMode === "absolute" ? 0 : 24);
169
+ const pH = screen.style?.paddingHorizontal ?? p ?? (screen.layoutMode === "absolute" ? 0 : 24);
170
+ return {
171
+ top: screen.style?.paddingTop ?? pV,
172
+ right: screen.style?.paddingRight ?? pH,
173
+ bottom: screen.style?.paddingBottom ?? pV,
174
+ left: screen.style?.paddingLeft ?? pH,
175
+ };
176
+ }
177
+ function splitAutoLayoutComponents(components) {
178
+ return {
179
+ main: components.filter((component) => {
180
+ const props = component.props;
181
+ return props.position !== "bottom";
182
+ }),
183
+ bottom: components.filter((component) => {
184
+ const props = component.props;
185
+ return props.position === "bottom";
186
+ }),
187
+ };
188
+ }
189
+ /** Components that size to their own content rather than stretching to fill width */
190
+ const SELF_SIZING_TYPES = new Set(["ICON_LIBRARY", "ICON", "PAGE_INDICATOR"]);
191
+ function getSpacing(props, prefix) {
192
+ const top = props[`${prefix}Top`];
193
+ const right = props[`${prefix}Right`];
194
+ const bottom = props[`${prefix}Bottom`];
195
+ const left = props[`${prefix}Left`];
196
+ if (typeof top === "number" ||
197
+ typeof right === "number" ||
198
+ typeof bottom === "number" ||
199
+ typeof left === "number") {
200
+ return {
201
+ top: typeof top === "number" ? top : 0,
202
+ right: typeof right === "number" ? right : 0,
203
+ bottom: typeof bottom === "number" ? bottom : 0,
204
+ left: typeof left === "number" ? left : 0,
205
+ };
206
+ }
207
+ const vertical = props[`${prefix}Vertical`];
208
+ const horizontal = props[`${prefix}Horizontal`];
209
+ return {
210
+ top: typeof vertical === "number" ? vertical : 0,
211
+ right: typeof horizontal === "number" ? horizontal : 0,
212
+ bottom: typeof vertical === "number" ? vertical : 0,
213
+ left: typeof horizontal === "number" ? horizontal : 0,
214
+ };
215
+ }
216
+ function getComponentMarginStyle(component) {
217
+ const props = component.props;
218
+ const margin = getSpacing(props, "margin");
219
+ return {
220
+ marginTop: margin.top || undefined,
221
+ marginRight: margin.right || undefined,
222
+ marginBottom: margin.bottom || undefined,
223
+ marginLeft: margin.left || undefined,
224
+ };
225
+ }
226
+ function getComponentWidthMode(component) {
227
+ const props = component.props;
228
+ if (props.widthMode === "fill" || props.widthMode === "fit" || props.widthMode === "fixed") {
229
+ return props.widthMode;
230
+ }
231
+ if (component.type === "BUTTON" || SELF_SIZING_TYPES.has(component.type)) {
232
+ return "fit";
233
+ }
234
+ return "fill";
235
+ }
236
+ function getFixedComponentWidth(component) {
237
+ const props = component.props;
238
+ if (typeof props.width === "number")
239
+ return props.width;
240
+ if (typeof props.fixedWidth === "number")
241
+ return props.fixedWidth;
242
+ if (component.type === "BUTTON")
243
+ return 300;
244
+ if (component.type === "TEXT")
245
+ return 200;
246
+ return undefined;
247
+ }
248
+ function getAutoLayoutWrapperStyle(component, parentAlignItems = "stretch") {
249
+ const widthMode = getComponentWidthMode(component);
250
+ if (widthMode === "fill") {
251
+ return {
252
+ alignSelf: "stretch",
253
+ };
254
+ }
255
+ const alignSelf = parentAlignItems === "center"
256
+ ? "center"
257
+ : parentAlignItems === "flex-end"
258
+ ? "flex-end"
259
+ : "flex-start";
260
+ if (widthMode === "fixed") {
261
+ return {
262
+ width: getFixedComponentWidth(component),
263
+ alignSelf,
264
+ };
265
+ }
266
+ return {
267
+ alignSelf,
268
+ };
269
+ }
270
+ function getComponentWrapperStyle(component, isAbsoluteScreen, parentAlignItems = "stretch") {
37
271
  const layout = component.layout;
272
+ const blockStyle = SELF_SIZING_TYPES.has(component.type) ? {} : styles.componentBlock;
273
+ const marginStyle = getComponentMarginStyle(component);
38
274
  if (!layout) {
39
- return isAbsoluteScreen ? { position: "absolute", zIndex: component.order } : styles.componentBlock;
275
+ return isAbsoluteScreen
276
+ ? { position: "absolute", zIndex: component.order }
277
+ : {
278
+ ...blockStyle,
279
+ ...marginStyle,
280
+ ...getAutoLayoutWrapperStyle(component, parentAlignItems),
281
+ };
40
282
  }
41
283
  const baseStyle = {
42
284
  display: layout.visible === false ? "none" : "flex",
@@ -44,15 +286,17 @@ function getComponentWrapperStyle(component, isAbsoluteScreen) {
44
286
  };
45
287
  if (!isAbsoluteScreen && layout.position !== "absolute") {
46
288
  return {
47
- ...styles.componentBlock,
289
+ ...blockStyle,
290
+ ...marginStyle,
291
+ ...getAutoLayoutWrapperStyle(component, parentAlignItems),
48
292
  ...baseStyle,
49
293
  };
50
294
  }
51
295
  return {
52
296
  ...baseStyle,
53
297
  position: "absolute",
54
- left: layout.x ?? 0,
55
- top: layout.y ?? 0,
298
+ left: layout.x ?? marginStyle.marginLeft ?? 0,
299
+ top: layout.y ?? marginStyle.marginTop ?? 0,
56
300
  width: layout.width,
57
301
  height: layout.height,
58
302
  transform: layout.rotation ? [{ rotate: `${layout.rotation}deg` }] : undefined,
@@ -67,15 +311,48 @@ function coerceStringArrayValue(value) {
67
311
  function coerceNumberValue(value, fallback) {
68
312
  return typeof value === "number" ? value : fallback;
69
313
  }
314
+ function renderFallbackIcon(name, size, color) {
315
+ const normalized = name.toLowerCase().replace(/[-_]/g, "");
316
+ let glyph = "\u2022";
317
+ if (normalized.includes("chevronleft") || normalized === "back")
318
+ glyph = "\u2039";
319
+ else if (normalized.includes("arrowleft") || normalized.endsWith("left"))
320
+ glyph = "\u2190";
321
+ else if (normalized.includes("chevronright") || normalized === "next")
322
+ glyph = "\u203A";
323
+ else if (normalized.includes("arrowright") || normalized.endsWith("right"))
324
+ glyph = "\u2192";
325
+ else if (normalized === "x" || normalized.includes("close"))
326
+ glyph = "\u2715";
327
+ else if (normalized.includes("check"))
328
+ glyph = "\u2713";
329
+ else if (normalized.includes("plus"))
330
+ glyph = "+";
331
+ else if (normalized.includes("minus"))
332
+ glyph = "\u2212";
333
+ return ((0, jsx_runtime_1.jsx)(react_native_1.Text, { style: {
334
+ fontSize: size,
335
+ color,
336
+ textAlign: "center",
337
+ includeFontPadding: false,
338
+ lineHeight: size,
339
+ }, children: glyph }));
340
+ }
70
341
  function getFieldError(snapshot, fieldKey) {
71
342
  return snapshot.validationErrorsByField[fieldKey] ?? null;
72
343
  }
73
- function DefaultTextComponent({ component }) {
344
+ // ─── Default Components ─────────────────────────────────────────────────────
345
+ function DefaultTextComponent({ component, theme, parentAlignItems = "stretch", }) {
346
+ const defaultTextAlign = parentAlignItems === "center"
347
+ ? "center"
348
+ : parentAlignItems === "flex-end"
349
+ ? "right"
350
+ : "left";
74
351
  return ((0, jsx_runtime_1.jsx)(react_native_1.Text, { style: {
75
- color: component.props.color ?? "#ffffff",
352
+ color: component.props.color ?? theme.textPrimary,
76
353
  fontSize: component.props.fontSize ?? 16,
77
354
  fontWeight: component.props.fontWeight ?? "normal",
78
- textAlign: component.props.textAlign ?? "left",
355
+ textAlign: component.props.textAlign ?? defaultTextAlign,
79
356
  lineHeight: component.props.lineHeight && component.props.fontSize
80
357
  ? component.props.lineHeight * component.props.fontSize
81
358
  : undefined,
@@ -83,117 +360,305 @@ function DefaultTextComponent({ component }) {
83
360
  }, children: component.props.content }));
84
361
  }
85
362
  function DefaultImageComponent({ component }) {
86
- return ((0, jsx_runtime_1.jsx)(react_native_1.Image, { source: { uri: component.props.src }, accessibilityLabel: component.props.alt, resizeMode: component.props.resizeMode ?? "cover", style: {
363
+ return ((0, jsx_runtime_1.jsx)(react_native_1.Image, { source: { uri: component.props.src }, alt: component.props.alt ?? "", accessibilityLabel: component.props.alt, resizeMode: component.props.resizeMode ?? "cover", style: {
87
364
  width: component.props.width ?? "100%",
88
365
  height: component.props.height ?? 220,
89
366
  borderRadius: component.props.borderRadius ?? 0,
90
367
  } }));
91
368
  }
92
- function DefaultButtonComponent({ component, onPress, }) {
369
+ function DefaultIconLibraryComponent({ component, iconRenderer, }) {
370
+ const props = component.props;
371
+ const iconName = props.iconName ?? "";
372
+ const size = props.size ?? 24;
373
+ const color = props.color ?? "#ffffff";
374
+ const bgColor = props.backgroundColor ?? undefined;
375
+ const width = props.width ?? size + 16;
376
+ const height = props.height ?? size + 16;
377
+ const opacity = props.opacity ?? 1;
378
+ const borderRadius = Math.min(width, height) / 2;
379
+ const iconContent = iconRenderer
380
+ ? iconRenderer(iconName, size, color)
381
+ : renderFallbackIcon(iconName, size, color);
382
+ return ((0, jsx_runtime_1.jsx)(react_native_1.View, { style: {
383
+ width,
384
+ height,
385
+ borderRadius,
386
+ backgroundColor: bgColor,
387
+ opacity,
388
+ alignItems: "center",
389
+ justifyContent: "center",
390
+ paddingVertical: props.paddingVertical ?? 0,
391
+ paddingHorizontal: props.paddingHorizontal ?? 0,
392
+ marginVertical: props.marginVertical ?? 0,
393
+ marginHorizontal: props.marginHorizontal ?? 0,
394
+ }, children: iconContent }));
395
+ }
396
+ function DefaultButtonComponent({ component, onPress, iconRenderer, }) {
397
+ const props = component.props;
398
+ const padding = getSpacing(props, "padding");
399
+ const showIcon = props.showIcon === true;
400
+ const iconName = props.iconName ?? "";
401
+ const iconPosition = props.iconPosition ?? "left";
402
+ const iconSize = props.iconSize ?? 20;
403
+ const textColor = props.textColor ?? component.props.style?.textColor ?? "#ffffff";
404
+ const iconColor = props.iconColor ?? textColor;
405
+ const iconSpacing = props.iconSpacing ?? 8;
406
+ const fontSize = props.fontSize ?? 16;
407
+ const fontWeight = props.fontWeight ?? "600";
408
+ const widthMode = getComponentWidthMode(component);
409
+ const heightMode = props.heightMode === "fit" || props.heightMode === "fill" || props.heightMode === "fixed"
410
+ ? props.heightMode
411
+ : "fit";
412
+ const backgroundColor = props.backgroundColor ?? component.props.style?.backgroundColor ?? "#007AFF";
413
+ const borderRadius = props.shape === "circle"
414
+ ? 999
415
+ : props.shape === "pill"
416
+ ? 999
417
+ : props.shape === "rounded"
418
+ ? 16
419
+ : props.borderRadius ?? component.props.style?.borderRadius ?? 14;
420
+ const borderColor = props.borderColor ?? component.props.style?.borderColor ?? "transparent";
421
+ const borderWidth = props.borderWidth ?? component.props.style?.borderWidth ?? 0;
422
+ const fixedHeight = typeof props.height === "number"
423
+ ? props.height
424
+ : typeof props.fixedHeight === "number"
425
+ ? props.fixedHeight
426
+ : 48;
427
+ const iconElement = showIcon && iconName
428
+ ? (iconRenderer
429
+ ? iconRenderer(iconName, iconSize, iconColor)
430
+ : renderFallbackIcon(iconName, iconSize, iconColor))
431
+ : null;
432
+ const isIconOnly = showIcon && iconPosition === "only";
433
+ const isCircularIconOnlyButton = props.shape === "circle" && isIconOnly;
93
434
  return ((0, jsx_runtime_1.jsx)(react_native_1.Pressable, { onPress: () => {
94
435
  void onPress();
95
436
  }, style: [
96
437
  styles.button,
97
438
  {
98
- backgroundColor: component.props.style?.backgroundColor ?? "#ffffff",
99
- borderRadius: component.props.style?.borderRadius ?? 14,
100
- borderColor: component.props.style?.borderColor ?? "transparent",
101
- borderWidth: component.props.style?.borderWidth ?? 0,
439
+ width: isCircularIconOnlyButton ? fixedHeight : widthMode === "fit" ? undefined : "100%",
440
+ minHeight: isCircularIconOnlyButton ? fixedHeight : heightMode === "fit" ? 48 : undefined,
441
+ height: isCircularIconOnlyButton ? fixedHeight : heightMode === "fixed" ? fixedHeight : undefined,
442
+ backgroundColor,
443
+ borderRadius,
444
+ borderColor,
445
+ borderWidth,
446
+ paddingTop: isCircularIconOnlyButton ? 0 : padding.top || 14,
447
+ paddingRight: isCircularIconOnlyButton ? 0 : padding.right || 16,
448
+ paddingBottom: isCircularIconOnlyButton ? 0 : padding.bottom || 14,
449
+ paddingLeft: isCircularIconOnlyButton ? 0 : padding.left || 16,
102
450
  },
103
- ], children: (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: [
104
- styles.buttonText,
451
+ ], children: (0, jsx_runtime_1.jsxs)(react_native_1.View, { style: [
452
+ styles.buttonContent,
105
453
  {
106
- color: component.props.style?.textColor ?? "#111111",
454
+ width: widthMode === "fit" ? undefined : "100%",
455
+ gap: isIconOnly ? 0 : iconSpacing,
107
456
  },
108
- ], children: component.props.label }) }));
457
+ ], children: [iconElement && (iconPosition === "left" || isIconOnly) ? iconElement : null, !isIconOnly ? ((0, jsx_runtime_1.jsx)(react_native_1.Text, { style: [
458
+ styles.buttonText,
459
+ {
460
+ color: textColor,
461
+ fontSize,
462
+ fontWeight,
463
+ textAlign: props.textAlign ?? "center",
464
+ },
465
+ ], children: component.props.label })) : null, iconElement && iconPosition === "right" ? iconElement : null] }) }));
109
466
  }
110
- function DefaultTextInputComponent({ component, context, }) {
467
+ function DefaultTextInputComponent({ component, context, theme, parentAlignItems = "stretch", }) {
111
468
  const value = coerceStringValue(context.snapshot.values[component.props.fieldKey]);
112
469
  const error = getFieldError(context.snapshot, component.props.fieldKey);
113
- return ((0, jsx_runtime_1.jsxs)(react_native_1.View, { style: styles.fieldGroup, children: [component.props.label ? (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.fieldLabel, children: component.props.label }) : null, (0, jsx_runtime_1.jsx)(react_native_1.TextInput, { value: value, onChangeText: (nextValue) => context.onValueChange(component.props.fieldKey, nextValue), placeholder: component.props.placeholder, placeholderTextColor: "#7a7a85", keyboardType: component.props.keyboardType === "email"
470
+ return ((0, jsx_runtime_1.jsxs)(react_native_1.View, { style: styles.fieldGroup, children: [component.props.label ? ((0, jsx_runtime_1.jsx)(react_native_1.Text, { style: [
471
+ styles.fieldLabel,
472
+ {
473
+ color: theme.textSecondary,
474
+ textAlign: parentAlignItems === "center"
475
+ ? "center"
476
+ : parentAlignItems === "flex-end"
477
+ ? "right"
478
+ : "left",
479
+ },
480
+ ], children: component.props.label })) : null, (0, jsx_runtime_1.jsx)(react_native_1.TextInput, { value: value, onChangeText: (nextValue) => context.onValueChange(component.props.fieldKey, nextValue), placeholder: component.props.placeholder, placeholderTextColor: theme.placeholderText, keyboardType: component.props.keyboardType === "email"
114
481
  ? "email-address"
115
482
  : component.props.keyboardType === "numeric"
116
483
  ? "numeric"
117
484
  : component.props.keyboardType === "phone"
118
485
  ? "phone-pad"
119
- : "default", maxLength: component.props.maxLength, style: [styles.input, error ? styles.inputError : undefined] }), error ? (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.fieldError, children: error }) : null] }));
486
+ : "default", maxLength: component.props.maxLength, style: [
487
+ styles.input,
488
+ {
489
+ width: "100%",
490
+ backgroundColor: theme.inputBackground,
491
+ borderColor: error ? theme.errorBorder : theme.inputBorder,
492
+ color: theme.inputText,
493
+ },
494
+ ] }), error ? (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: [styles.fieldError, { color: theme.errorText }], children: error }) : null] }));
120
495
  }
121
- function OptionPill({ label, selected, onPress, }) {
122
- return ((0, jsx_runtime_1.jsx)(react_native_1.Pressable, { onPress: onPress, style: [styles.optionPill, selected ? styles.optionPillSelected : undefined], children: (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: [styles.optionPillText, selected ? styles.optionPillTextSelected : undefined], children: label }) }));
496
+ function OptionPill({ label, selected, theme, onPress, }) {
497
+ return ((0, jsx_runtime_1.jsx)(react_native_1.Pressable, { onPress: onPress, style: [
498
+ styles.optionPill,
499
+ {
500
+ backgroundColor: selected ? theme.pillSelectedBackground : theme.pillBackground,
501
+ borderColor: selected ? theme.pillSelectedBorder : theme.pillBorder,
502
+ },
503
+ ], children: (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: [
504
+ styles.optionPillText,
505
+ { color: selected ? theme.pillSelectedText : theme.pillText },
506
+ ], children: label }) }));
123
507
  }
124
- function DefaultSingleSelectComponent({ component, context, }) {
508
+ function DefaultSingleSelectComponent({ component, context, theme, parentAlignItems = "stretch", }) {
125
509
  const value = coerceStringValue(context.snapshot.values[component.props.fieldKey]);
126
510
  const error = getFieldError(context.snapshot, component.props.fieldKey);
127
- return ((0, jsx_runtime_1.jsxs)(react_native_1.View, { style: styles.fieldGroup, children: [component.props.label ? (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.fieldLabel, children: component.props.label }) : null, (0, jsx_runtime_1.jsx)(react_native_1.View, { style: styles.optionGroup, children: component.props.options.map((option) => ((0, jsx_runtime_1.jsx)(OptionPill, { label: option.label, selected: value === option.id, onPress: () => context.onValueChange(component.props.fieldKey, option.id) }, option.id))) }), error ? (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.fieldError, children: error }) : null] }));
511
+ return ((0, jsx_runtime_1.jsxs)(react_native_1.View, { style: styles.fieldGroup, children: [component.props.label ? ((0, jsx_runtime_1.jsx)(react_native_1.Text, { style: [
512
+ styles.fieldLabel,
513
+ {
514
+ color: theme.textSecondary,
515
+ textAlign: parentAlignItems === "center"
516
+ ? "center"
517
+ : parentAlignItems === "flex-end"
518
+ ? "right"
519
+ : "left",
520
+ },
521
+ ], children: component.props.label })) : null, (0, jsx_runtime_1.jsx)(react_native_1.View, { style: styles.selectList, children: component.props.options.map((option) => ((0, jsx_runtime_1.jsx)(react_native_1.Pressable, { style: [
522
+ styles.selectOption,
523
+ {
524
+ borderColor: value === option.id ? theme.textPrimary : theme.inputBorder,
525
+ backgroundColor: value === option.id ? theme.textPrimary : theme.inputBackground,
526
+ },
527
+ ], onPress: () => context.onValueChange(component.props.fieldKey, option.id), children: (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: [
528
+ styles.selectOptionText,
529
+ { color: value === option.id ? "#ffffff" : theme.textPrimary },
530
+ ], children: option.label }) }, option.id))) }), error ? (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: [styles.fieldError, { color: theme.errorText }], children: error }) : null] }));
128
531
  }
129
- function DefaultMultiSelectComponent({ component, context, }) {
532
+ function DefaultMultiSelectComponent({ component, context, theme, parentAlignItems = "stretch", }) {
130
533
  const values = coerceStringArrayValue(context.snapshot.values[component.props.fieldKey]);
131
534
  const error = getFieldError(context.snapshot, component.props.fieldKey);
132
- return ((0, jsx_runtime_1.jsxs)(react_native_1.View, { style: styles.fieldGroup, children: [component.props.label ? (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.fieldLabel, children: component.props.label }) : null, (0, jsx_runtime_1.jsx)(react_native_1.View, { style: styles.optionGroup, children: component.props.options.map((option) => {
535
+ return ((0, jsx_runtime_1.jsxs)(react_native_1.View, { style: styles.fieldGroup, children: [component.props.label ? ((0, jsx_runtime_1.jsx)(react_native_1.Text, { style: [
536
+ styles.fieldLabel,
537
+ {
538
+ color: theme.textSecondary,
539
+ textAlign: parentAlignItems === "center"
540
+ ? "center"
541
+ : parentAlignItems === "flex-end"
542
+ ? "right"
543
+ : "left",
544
+ },
545
+ ], children: component.props.label })) : null, (0, jsx_runtime_1.jsx)(react_native_1.View, { style: styles.optionGroup, children: component.props.options.map((option) => {
133
546
  const selected = values.includes(option.id);
134
- return ((0, jsx_runtime_1.jsx)(OptionPill, { label: option.label, selected: selected, onPress: () => {
547
+ return ((0, jsx_runtime_1.jsx)(OptionPill, { label: option.label, selected: selected, theme: theme, onPress: () => {
135
548
  const nextValues = selected
136
549
  ? values.filter((value) => value !== option.id)
137
550
  : [...values, option.id];
138
551
  context.onValueChange(component.props.fieldKey, nextValues);
139
552
  } }, option.id));
140
- }) }), error ? (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.fieldError, children: error }) : null] }));
553
+ }) }), error ? (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: [styles.fieldError, { color: theme.errorText }], children: error }) : null] }));
141
554
  }
142
- function DefaultSliderComponent({ component, context, }) {
555
+ function DefaultSliderComponent({ component, context, theme, parentAlignItems = "stretch", }) {
556
+ const [trackWidth, setTrackWidth] = (0, react_1.useState)(0);
143
557
  const currentValue = coerceNumberValue(context.snapshot.values[component.props.fieldKey], component.props.defaultValue ?? component.props.min);
144
558
  const step = component.props.step ?? 1;
145
- const nextValue = Math.min(component.props.max, currentValue + step);
146
- const previousValue = Math.max(component.props.min, currentValue - step);
147
- return ((0, jsx_runtime_1.jsxs)(react_native_1.View, { style: styles.fieldGroup, children: [component.props.label ? (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.fieldLabel, children: component.props.label }) : null, (0, jsx_runtime_1.jsxs)(react_native_1.View, { style: styles.sliderCard, children: [(0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.sliderValue, children: String(currentValue) }), (0, jsx_runtime_1.jsxs)(react_native_1.View, { style: styles.sliderActions, children: [(0, jsx_runtime_1.jsx)(OptionPill, { label: component.props.minLabel ?? "-", selected: false, onPress: () => context.onValueChange(component.props.fieldKey, previousValue) }), (0, jsx_runtime_1.jsx)(OptionPill, { label: component.props.maxLabel ?? "+", selected: false, onPress: () => context.onValueChange(component.props.fieldKey, nextValue) })] })] })] }));
559
+ const clampedValue = Math.min(component.props.max, Math.max(component.props.min, currentValue));
560
+ const range = component.props.max - component.props.min;
561
+ const progress = range <= 0 ? 0 : ((clampedValue - component.props.min) / range) * 100;
562
+ const thumbSize = 18;
563
+ const thumbLeft = trackWidth <= 0
564
+ ? 0
565
+ : Math.min(Math.max((progress / 100) * trackWidth - thumbSize / 2, 0), Math.max(trackWidth - thumbSize, 0));
566
+ const updateFromLocation = (locationX) => {
567
+ if (trackWidth <= 0 || range <= 0)
568
+ return;
569
+ const ratio = Math.min(Math.max(locationX / trackWidth, 0), 1);
570
+ const rawValue = component.props.min + ratio * range;
571
+ const steppedValue = Math.round((rawValue - component.props.min) / step) * step + component.props.min;
572
+ const nextValue = Math.min(component.props.max, Math.max(component.props.min, Number(steppedValue.toFixed(4))));
573
+ context.onValueChange(component.props.fieldKey, nextValue);
574
+ };
575
+ const handleTrackLayout = (event) => {
576
+ setTrackWidth(event.nativeEvent.layout.width);
577
+ };
578
+ const handleTrackInteraction = (event) => {
579
+ updateFromLocation(event.nativeEvent.locationX);
580
+ };
581
+ return ((0, jsx_runtime_1.jsxs)(react_native_1.View, { style: styles.fieldGroup, children: [component.props.label ? ((0, jsx_runtime_1.jsx)(react_native_1.Text, { style: [
582
+ styles.fieldLabel,
583
+ {
584
+ color: theme.textSecondary,
585
+ textAlign: parentAlignItems === "center"
586
+ ? "center"
587
+ : parentAlignItems === "flex-end"
588
+ ? "right"
589
+ : "left",
590
+ },
591
+ ], children: component.props.label })) : null, (0, jsx_runtime_1.jsxs)(react_native_1.View, { style: styles.sliderField, children: [(0, jsx_runtime_1.jsxs)(react_native_1.View, { onLayout: handleTrackLayout, onStartShouldSetResponder: () => true, onResponderGrant: handleTrackInteraction, onResponderMove: handleTrackInteraction, style: styles.sliderTouchArea, children: [(0, jsx_runtime_1.jsx)(react_native_1.View, { style: [
592
+ styles.sliderTrack,
593
+ {
594
+ backgroundColor: theme.progressTrackBackground,
595
+ },
596
+ ] }), (0, jsx_runtime_1.jsx)(react_native_1.View, { style: [
597
+ styles.sliderFill,
598
+ {
599
+ width: `${progress}%`,
600
+ backgroundColor: "#3B82F6",
601
+ },
602
+ ] }), (0, jsx_runtime_1.jsx)(react_native_1.View, { style: [
603
+ styles.sliderThumb,
604
+ {
605
+ left: thumbLeft,
606
+ backgroundColor: "#3B82F6",
607
+ borderColor: theme.inputBackground,
608
+ },
609
+ ] })] }), (0, jsx_runtime_1.jsxs)(react_native_1.View, { style: styles.sliderLabels, children: [(0, jsx_runtime_1.jsx)(react_native_1.Text, { style: [styles.sliderLabel, { color: theme.placeholderText }], children: String(component.props.minLabel ?? component.props.min) }), (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: [styles.sliderLabel, { color: theme.placeholderText }], children: String(component.props.maxLabel ?? component.props.max) })] })] })] }));
148
610
  }
149
- function DefaultProgressBarComponent({ snapshot, component, }) {
611
+ function DefaultProgressBarComponent({ snapshot, component, theme, }) {
150
612
  const progress = snapshot.totalScreens > 1
151
613
  ? ((snapshot.currentScreenIndex + 1) / snapshot.totalScreens) * 100
152
614
  : 100;
153
615
  return ((0, jsx_runtime_1.jsx)(react_native_1.View, { style: [
154
616
  styles.progressTrack,
155
617
  {
156
- backgroundColor: component.props.backgroundColor ?? "#26262b",
618
+ backgroundColor: component.props.backgroundColor ?? theme.progressTrackBackground,
157
619
  height: component.props.height ?? 6,
158
620
  },
159
621
  ], children: (0, jsx_runtime_1.jsx)(react_native_1.View, { style: {
160
622
  width: `${progress}%`,
161
- backgroundColor: component.props.color ?? "#ffffff",
623
+ backgroundColor: component.props.color ?? theme.progressFillColor,
162
624
  height: "100%",
163
625
  borderRadius: 999,
164
626
  } }) }));
165
627
  }
166
- function DefaultPageIndicatorComponent({ snapshot, component, }) {
628
+ function DefaultPageIndicatorComponent({ snapshot, component, theme, }) {
167
629
  const size = component.props.size ?? 8;
168
630
  return ((0, jsx_runtime_1.jsx)(react_native_1.View, { style: styles.pageIndicatorRow, children: Array.from({ length: snapshot.totalScreens }).map((_, index) => ((0, jsx_runtime_1.jsx)(react_native_1.View, { style: {
169
631
  width: size,
170
632
  height: size,
171
633
  borderRadius: size / 2,
172
634
  backgroundColor: index === snapshot.currentScreenIndex
173
- ? component.props.activeColor ?? "#ffffff"
174
- : component.props.inactiveColor ?? "#4b4b55",
635
+ ? component.props.activeColor ?? theme.indicatorActive
636
+ : component.props.inactiveColor ?? theme.indicatorInactive,
175
637
  } }, index))) }));
176
638
  }
177
- function renderDefaultComponent(component, context, registry) {
639
+ // ─── Render Dispatch ────────────────────────────────────────────────────────
640
+ function renderDefaultComponent(component, context, theme, registry, iconRenderer, parentAlignItems = "stretch") {
178
641
  switch (component.type) {
179
642
  case "TEXT":
180
- return (0, jsx_runtime_1.jsx)(DefaultTextComponent, { component: component });
643
+ return ((0, jsx_runtime_1.jsx)(DefaultTextComponent, { component: component, theme: theme, parentAlignItems: parentAlignItems }));
181
644
  case "IMAGE":
182
645
  return (0, jsx_runtime_1.jsx)(DefaultImageComponent, { component: component });
646
+ case "ICON_LIBRARY":
647
+ return (0, jsx_runtime_1.jsx)(DefaultIconLibraryComponent, { component: component, iconRenderer: iconRenderer });
183
648
  case "BUTTON":
184
- return (0, jsx_runtime_1.jsx)(DefaultButtonComponent, { component: component, onPress: () => context.onPressButton(component.id) });
649
+ return ((0, jsx_runtime_1.jsx)(DefaultButtonComponent, { component: component, onPress: () => context.onPressButton(component.id), iconRenderer: iconRenderer }));
185
650
  case "TEXT_INPUT":
186
- return (0, jsx_runtime_1.jsx)(DefaultTextInputComponent, { component: component, context: context });
651
+ return ((0, jsx_runtime_1.jsx)(DefaultTextInputComponent, { component: component, context: context, theme: theme, parentAlignItems: parentAlignItems }));
187
652
  case "SINGLE_SELECT":
188
- return (0, jsx_runtime_1.jsx)(DefaultSingleSelectComponent, { component: component, context: context });
653
+ return ((0, jsx_runtime_1.jsx)(DefaultSingleSelectComponent, { component: component, context: context, theme: theme, parentAlignItems: parentAlignItems }));
189
654
  case "MULTI_SELECT":
190
- return (0, jsx_runtime_1.jsx)(DefaultMultiSelectComponent, { component: component, context: context });
655
+ return ((0, jsx_runtime_1.jsx)(DefaultMultiSelectComponent, { component: component, context: context, theme: theme, parentAlignItems: parentAlignItems }));
191
656
  case "SLIDER":
192
- return (0, jsx_runtime_1.jsx)(DefaultSliderComponent, { component: component, context: context });
657
+ return ((0, jsx_runtime_1.jsx)(DefaultSliderComponent, { component: component, context: context, theme: theme, parentAlignItems: parentAlignItems }));
193
658
  case "PROGRESS_BAR":
194
- return (0, jsx_runtime_1.jsx)(DefaultProgressBarComponent, { component: component, snapshot: context.snapshot });
659
+ return (0, jsx_runtime_1.jsx)(DefaultProgressBarComponent, { component: component, snapshot: context.snapshot, theme: theme });
195
660
  case "PAGE_INDICATOR":
196
- return (0, jsx_runtime_1.jsx)(DefaultPageIndicatorComponent, { component: component, snapshot: context.snapshot });
661
+ return (0, jsx_runtime_1.jsx)(DefaultPageIndicatorComponent, { component: component, snapshot: context.snapshot, theme: theme });
197
662
  case "CUSTOM_COMPONENT": {
198
663
  const registered = registry?.getComponent(component.props.registryKey);
199
664
  return registered
@@ -209,47 +674,108 @@ function renderDefaultComponent(component, context, registry) {
209
674
  return null;
210
675
  }
211
676
  }
212
- function ArloFlowRenderer({ session, handlers, componentRenderers, registry, autoStart = true, emptyState = null, unsupportedComponent, unsupportedScreen, onSnapshotChange, }) {
213
- const handlersRef = (0, react_1.useRef)(handlers);
214
- const onSnapshotChangeRef = (0, react_1.useRef)(onSnapshotChange);
215
- (0, react_1.useEffect)(() => {
216
- handlersRef.current = handlers;
217
- onSnapshotChangeRef.current = onSnapshotChange;
218
- });
677
+ // ─── Main Renderer ──────────────────────────────────────────────────────────
678
+ function ArloFlowRenderer({ session, handlers, componentRenderers, registry, iconRenderer, autoStart = true, emptyState = null, unsupportedComponent, unsupportedScreen, onSnapshotChange, }) {
219
679
  const [snapshot, setSnapshot] = (0, react_1.useState)(() => session.getSnapshot());
680
+ const savedOnSnapshotChange = (0, react_1.useRef)(onSnapshotChange);
681
+ const savedHandlers = (0, react_1.useRef)(handlers);
682
+ (0, react_1.useEffect)(() => {
683
+ savedOnSnapshotChange.current = onSnapshotChange;
684
+ savedHandlers.current = handlers;
685
+ }, [onSnapshotChange, handlers]);
220
686
  (0, react_1.useEffect)(() => {
221
687
  const nextSnapshot = session.getSnapshot();
222
- setSnapshot(nextSnapshot);
223
- onSnapshotChangeRef.current?.(nextSnapshot);
688
+ (0, react_1.startTransition)(() => {
689
+ setSnapshot(nextSnapshot);
690
+ });
691
+ savedOnSnapshotChange.current?.(nextSnapshot);
224
692
  if (autoStart && session.getSnapshot().status === "idle") {
225
693
  const effect = session.start();
226
694
  const startedSnapshot = session.getSnapshot();
227
- setSnapshot(startedSnapshot);
228
- onSnapshotChangeRef.current?.(startedSnapshot);
229
- void (0, arlo_sdk_1.applyFlowSessionEffect)(session, effect, handlersRef.current);
695
+ (0, react_1.startTransition)(() => {
696
+ setSnapshot(startedSnapshot);
697
+ });
698
+ savedOnSnapshotChange.current?.(startedSnapshot);
699
+ void (0, arlo_sdk_1.applyFlowSessionEffect)(session, effect, savedHandlers.current);
230
700
  }
231
701
  }, [autoStart, session]);
232
702
  const sortedComponents = (0, react_1.useMemo)(() => [...(snapshot.currentScreen?.components ?? [])].sort((a, b) => a.order - b.order), [snapshot.currentScreen]);
703
+ const theme = (0, react_1.useMemo)(() => (snapshot.currentScreen ? getThemeForScreen(snapshot.currentScreen) : LIGHT_THEME), [snapshot.currentScreen]);
233
704
  const context = {
234
705
  session,
235
706
  snapshot,
236
707
  handlers,
708
+ iconRenderer,
237
709
  onValueChange: (fieldKey, value) => {
238
710
  const nextSnapshot = session.setValue(fieldKey, value);
239
711
  setSnapshot(nextSnapshot);
240
712
  onSnapshotChange?.(nextSnapshot);
713
+ const component = findInteractiveComponent(nextSnapshot.currentScreen, fieldKey);
714
+ if (component) {
715
+ emitAnalyticsEvent(handlers, session, nextSnapshot, (0, arlo_sdk_1.createComponentInteractionAnalyticsEvent)(nextSnapshot, component, value));
716
+ }
241
717
  },
242
718
  onPressButton: async (componentId) => {
719
+ const currentSnapshot = session.getSnapshot();
720
+ const currentHandlers = handlers;
721
+ const buttonComponent = findButtonComponent(currentSnapshot.currentScreen, componentId);
722
+ if (buttonComponent) {
723
+ emitAnalyticsEvent(currentHandlers, session, currentSnapshot, (0, arlo_sdk_1.createButtonPressedAnalyticsEvent)(currentSnapshot, buttonComponent));
724
+ }
243
725
  const effect = session.pressButton(componentId);
244
726
  const immediateSnapshot = session.getSnapshot();
245
727
  setSnapshot(immediateSnapshot);
246
728
  onSnapshotChange?.(immediateSnapshot);
247
- await (0, arlo_sdk_1.applyFlowSessionEffect)(session, effect, handlers);
729
+ await (0, arlo_sdk_1.applyFlowSessionEffect)(session, effect, currentHandlers);
248
730
  const finalSnapshot = session.getSnapshot();
249
731
  setSnapshot(finalSnapshot);
250
732
  onSnapshotChange?.(finalSnapshot);
251
733
  },
252
734
  };
735
+ const renderComponentNode = (component, renderContext, renderTheme, isAbsoluteScreen, parentAlignItems = "stretch") => {
736
+ const customRenderer = componentRenderers?.[component.type];
737
+ const content = customRenderer
738
+ ? customRenderer(component, renderContext)
739
+ : renderDefaultComponent(component, renderContext, renderTheme, registry, iconRenderer, parentAlignItems);
740
+ if (content === null) {
741
+ return ((0, jsx_runtime_1.jsx)(react_native_1.View, { style: styles.unsupported, children: unsupportedComponent ? (unsupportedComponent(component)) : ((0, jsx_runtime_1.jsxs)(react_native_1.Text, { style: styles.unsupportedText, children: ["Unsupported component: ", component.type] })) }, component.id));
742
+ }
743
+ return ((0, jsx_runtime_1.jsx)(react_native_1.View, { style: getComponentWrapperStyle(component, isAbsoluteScreen, parentAlignItems), children: content }, component.id));
744
+ };
745
+ const renderAutoLayoutScreen = (screen, components, renderContext, renderTheme) => {
746
+ const padding = getScreenPadding(screen);
747
+ const backgroundColor = screen.style?.backgroundColor ?? "#FFFFFF";
748
+ const justifyContent = screen.style?.justifyContent ?? "flex-start";
749
+ const alignItems = screen.style?.alignItems ?? "stretch";
750
+ const { main, bottom } = splitAutoLayoutComponents(components);
751
+ return ((0, jsx_runtime_1.jsx)(SafeView, { style: { flex: 1, backgroundColor }, children: (0, jsx_runtime_1.jsx)(react_native_1.ScrollView, { contentInsetAdjustmentBehavior: "automatic", automaticallyAdjustKeyboardInsets: true, keyboardShouldPersistTaps: "handled", contentContainerStyle: [
752
+ styles.container,
753
+ {
754
+ backgroundColor,
755
+ },
756
+ ], children: (0, jsx_runtime_1.jsxs)(react_native_1.View, { style: styles.autoLayoutRoot, children: [(0, jsx_runtime_1.jsx)(react_native_1.View, { style: [
757
+ styles.autoLayoutMain,
758
+ {
759
+ backgroundColor,
760
+ justifyContent,
761
+ alignItems,
762
+ paddingTop: padding.top,
763
+ paddingRight: padding.right,
764
+ paddingBottom: bottom.length > 0 ? 12 : padding.bottom,
765
+ paddingLeft: padding.left,
766
+ },
767
+ ], children: main.map((component) => renderComponentNode(component, renderContext, renderTheme, false, alignItems)) }), bottom.length > 0 ? ((0, jsx_runtime_1.jsx)(react_native_1.View, { style: [
768
+ styles.autoLayoutBottom,
769
+ {
770
+ backgroundColor,
771
+ alignItems,
772
+ paddingTop: 12,
773
+ paddingRight: padding.right,
774
+ paddingBottom: padding.bottom,
775
+ paddingLeft: padding.left,
776
+ },
777
+ ], children: bottom.map((component) => renderComponentNode(component, renderContext, renderTheme, false, alignItems)) })) : null] }) }) }));
778
+ };
253
779
  if (!snapshot.currentScreen) {
254
780
  return (0, jsx_runtime_1.jsx)(jsx_runtime_1.Fragment, { children: emptyState });
255
781
  }
@@ -265,63 +791,49 @@ function ArloFlowRenderer({ session, handlers, componentRenderers, registry, aut
265
791
  }
266
792
  if (importedPreviewScreen) {
267
793
  const previewComponents = [...(importedPreviewScreen.components ?? [])].sort((a, b) => a.order - b.order);
794
+ const previewTheme = getThemeForScreen(importedPreviewScreen);
268
795
  const previewContext = {
269
796
  ...context,
270
797
  onValueChange: () => undefined,
271
798
  onPressButton: async () => undefined,
272
799
  };
273
- return ((0, jsx_runtime_1.jsx)(react_native_1.ScrollView, { contentContainerStyle: [
274
- styles.container,
275
- getScreenContainerStyle(importedPreviewScreen),
276
- ], children: previewComponents.map((component) => {
277
- const customRenderer = componentRenderers?.[component.type];
278
- const content = customRenderer
279
- ? customRenderer(component, previewContext)
280
- : renderDefaultComponent(component, previewContext, registry);
281
- if (content === null) {
282
- return null;
283
- }
284
- return (0, jsx_runtime_1.jsx)(react_native_1.View, { children: content }, component.id);
285
- }) }));
800
+ return renderAutoLayoutScreen(importedPreviewScreen, previewComponents, previewContext, previewTheme);
286
801
  }
287
802
  return ((0, jsx_runtime_1.jsx)(jsx_runtime_1.Fragment, { children: unsupportedScreen ? (unsupportedScreen(snapshot.currentScreen)) : ((0, jsx_runtime_1.jsx)(react_native_1.View, { style: styles.unsupported, children: (0, jsx_runtime_1.jsxs)(react_native_1.Text, { style: styles.unsupportedText, children: ["Unsupported screen: ", snapshot.currentScreen.customScreenKey] }) })) }));
288
803
  }
289
804
  if (snapshot.currentScreen.layoutMode === "absolute") {
290
- return ((0, jsx_runtime_1.jsx)(react_native_1.View, { style: [
291
- styles.absoluteContainer,
292
- getScreenContainerStyle(snapshot.currentScreen),
293
- ], children: sortedComponents.map((component) => {
294
- const customRenderer = componentRenderers?.[component.type];
295
- const content = customRenderer
296
- ? customRenderer(component, context)
297
- : renderDefaultComponent(component, context, registry);
298
- if (content === null) {
299
- return ((0, jsx_runtime_1.jsx)(react_native_1.View, { style: styles.unsupported, children: unsupportedComponent ? (unsupportedComponent(component)) : ((0, jsx_runtime_1.jsxs)(react_native_1.Text, { style: styles.unsupportedText, children: ["Unsupported component: ", component.type] })) }, component.id));
300
- }
301
- return ((0, jsx_runtime_1.jsx)(react_native_1.View, { style: getComponentWrapperStyle(component, true), children: content }, component.id));
302
- }) }));
805
+ const bgColor = snapshot.currentScreen.style?.backgroundColor ?? "#FFFFFF";
806
+ return ((0, jsx_runtime_1.jsx)(SafeView, { style: { flex: 1, backgroundColor: bgColor }, children: (0, jsx_runtime_1.jsx)(react_native_1.View, { style: [
807
+ styles.absoluteContainer,
808
+ getScreenContainerStyle(snapshot.currentScreen),
809
+ ], children: sortedComponents.map((component) => renderComponentNode(component, context, theme, true)) }) }));
303
810
  }
304
- return ((0, jsx_runtime_1.jsx)(react_native_1.ScrollView, { contentContainerStyle: [
305
- styles.container,
306
- getScreenContainerStyle(snapshot.currentScreen),
307
- ], children: sortedComponents.map((component) => {
308
- const customRenderer = componentRenderers?.[component.type];
309
- const content = customRenderer
310
- ? customRenderer(component, context)
311
- : renderDefaultComponent(component, context, registry);
312
- if (content === null) {
313
- return ((0, jsx_runtime_1.jsx)(react_native_1.View, { style: styles.unsupported, children: unsupportedComponent ? (unsupportedComponent(component)) : ((0, jsx_runtime_1.jsxs)(react_native_1.Text, { style: styles.unsupportedText, children: ["Unsupported component: ", component.type] })) }, component.id));
314
- }
315
- return ((0, jsx_runtime_1.jsx)(react_native_1.View, { style: getComponentWrapperStyle(component, false), children: content }, component.id));
316
- }) }));
811
+ return renderAutoLayoutScreen(snapshot.currentScreen, sortedComponents, context, theme);
317
812
  }
813
+ // ─── Styles ─────────────────────────────────────────────────────────────────
318
814
  const styles = react_native_1.StyleSheet.create({
319
815
  container: {
320
816
  flexGrow: 1,
321
- gap: 16,
817
+ alignItems: "stretch",
322
818
  },
323
819
  componentBlock: {
820
+ alignSelf: "stretch",
821
+ },
822
+ autoLayoutRoot: {
823
+ flexGrow: 1,
824
+ width: "100%",
825
+ alignSelf: "stretch",
826
+ },
827
+ autoLayoutMain: {
828
+ flexGrow: 1,
829
+ width: "100%",
830
+ alignSelf: "stretch",
831
+ gap: 16,
832
+ },
833
+ autoLayoutBottom: {
324
834
  width: "100%",
835
+ alignSelf: "stretch",
836
+ gap: 12,
325
837
  },
326
838
  absoluteContainer: {
327
839
  flex: 1,
@@ -332,24 +844,16 @@ const styles = react_native_1.StyleSheet.create({
332
844
  gap: 8,
333
845
  },
334
846
  fieldLabel: {
335
- color: "#f3f3f5",
336
847
  fontSize: 14,
337
848
  fontWeight: "600",
338
849
  },
339
850
  input: {
340
851
  borderWidth: 1,
341
- borderColor: "#2c2c34",
342
852
  borderRadius: 14,
343
853
  paddingHorizontal: 14,
344
854
  paddingVertical: 12,
345
- color: "#ffffff",
346
- backgroundColor: "#141419",
347
- },
348
- inputError: {
349
- borderColor: "#f36b8d",
350
855
  },
351
856
  fieldError: {
352
- color: "#f59cb3",
353
857
  fontSize: 12,
354
858
  fontWeight: "500",
355
859
  },
@@ -357,13 +861,30 @@ const styles = react_native_1.StyleSheet.create({
357
861
  minHeight: 52,
358
862
  alignItems: "center",
359
863
  justifyContent: "center",
360
- paddingHorizontal: 16,
361
- paddingVertical: 12,
864
+ },
865
+ buttonContent: {
866
+ flexDirection: "row",
867
+ alignItems: "center",
868
+ justifyContent: "center",
362
869
  },
363
870
  buttonText: {
364
871
  fontSize: 16,
365
872
  fontWeight: "700",
366
873
  },
874
+ selectList: {
875
+ gap: 8,
876
+ },
877
+ selectOption: {
878
+ width: "100%",
879
+ borderWidth: 1,
880
+ borderRadius: 16,
881
+ paddingHorizontal: 16,
882
+ paddingVertical: 12,
883
+ },
884
+ selectOptionText: {
885
+ fontSize: 14,
886
+ fontWeight: "600",
887
+ },
367
888
  optionGroup: {
368
889
  flexDirection: "row",
369
890
  flexWrap: "wrap",
@@ -374,37 +895,45 @@ const styles = react_native_1.StyleSheet.create({
374
895
  paddingVertical: 10,
375
896
  borderRadius: 999,
376
897
  borderWidth: 1,
377
- borderColor: "#30303a",
378
- backgroundColor: "#15151b",
379
- },
380
- optionPillSelected: {
381
- backgroundColor: "#ffffff",
382
- borderColor: "#ffffff",
383
898
  },
384
899
  optionPillText: {
385
- color: "#f1f1f3",
386
900
  fontSize: 14,
387
901
  fontWeight: "600",
388
902
  },
389
- optionPillTextSelected: {
390
- color: "#111111",
903
+ sliderField: {
904
+ gap: 6,
391
905
  },
392
- sliderCard: {
393
- borderRadius: 18,
394
- backgroundColor: "#15151b",
395
- borderWidth: 1,
396
- borderColor: "#2b2b34",
397
- padding: 14,
398
- gap: 12,
906
+ sliderTouchArea: {
907
+ height: 28,
908
+ justifyContent: "center",
909
+ position: "relative",
399
910
  },
400
- sliderValue: {
401
- color: "#ffffff",
402
- fontSize: 28,
403
- fontWeight: "700",
911
+ sliderTrack: {
912
+ height: 6,
913
+ borderRadius: 999,
914
+ overflow: "hidden",
915
+ },
916
+ sliderFill: {
917
+ position: "absolute",
918
+ left: 0,
919
+ top: 11,
920
+ height: 6,
921
+ borderRadius: 999,
404
922
  },
405
- sliderActions: {
923
+ sliderThumb: {
924
+ position: "absolute",
925
+ top: 5,
926
+ width: 18,
927
+ height: 18,
928
+ borderRadius: 999,
929
+ borderWidth: 3,
930
+ },
931
+ sliderLabels: {
406
932
  flexDirection: "row",
407
- gap: 10,
933
+ justifyContent: "space-between",
934
+ },
935
+ sliderLabel: {
936
+ fontSize: 11,
408
937
  },
409
938
  progressTrack: {
410
939
  width: "100%",