arlo-react-native 0.1.1 → 0.1.3

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,41 +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, }) {
677
+ // ─── Main Renderer ──────────────────────────────────────────────────────────
678
+ function ArloFlowRenderer({ session, handlers, componentRenderers, registry, iconRenderer, autoStart = true, emptyState = null, unsupportedComponent, unsupportedScreen, onSnapshotChange, }) {
213
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]);
214
686
  (0, react_1.useEffect)(() => {
215
687
  const nextSnapshot = session.getSnapshot();
216
- setSnapshot(nextSnapshot);
217
- onSnapshotChange?.(nextSnapshot);
688
+ (0, react_1.startTransition)(() => {
689
+ setSnapshot(nextSnapshot);
690
+ });
691
+ savedOnSnapshotChange.current?.(nextSnapshot);
218
692
  if (autoStart && session.getSnapshot().status === "idle") {
219
693
  const effect = session.start();
220
694
  const startedSnapshot = session.getSnapshot();
221
- setSnapshot(startedSnapshot);
222
- onSnapshotChange?.(startedSnapshot);
223
- void (0, arlo_sdk_1.applyFlowSessionEffect)(session, effect, handlers);
695
+ (0, react_1.startTransition)(() => {
696
+ setSnapshot(startedSnapshot);
697
+ });
698
+ savedOnSnapshotChange.current?.(startedSnapshot);
699
+ void (0, arlo_sdk_1.applyFlowSessionEffect)(session, effect, savedHandlers.current);
224
700
  }
225
- }, [autoStart, handlers, onSnapshotChange, session]);
701
+ }, [autoStart, session]);
226
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]);
227
704
  const context = {
228
705
  session,
229
706
  snapshot,
230
707
  handlers,
708
+ iconRenderer,
231
709
  onValueChange: (fieldKey, value) => {
232
710
  const nextSnapshot = session.setValue(fieldKey, value);
233
711
  setSnapshot(nextSnapshot);
234
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
+ }
235
717
  },
236
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
+ }
237
725
  const effect = session.pressButton(componentId);
238
726
  const immediateSnapshot = session.getSnapshot();
239
727
  setSnapshot(immediateSnapshot);
240
728
  onSnapshotChange?.(immediateSnapshot);
241
- await (0, arlo_sdk_1.applyFlowSessionEffect)(session, effect, handlers);
729
+ await (0, arlo_sdk_1.applyFlowSessionEffect)(session, effect, currentHandlers);
242
730
  const finalSnapshot = session.getSnapshot();
243
731
  setSnapshot(finalSnapshot);
244
732
  onSnapshotChange?.(finalSnapshot);
245
733
  },
246
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
+ };
247
779
  if (!snapshot.currentScreen) {
248
780
  return (0, jsx_runtime_1.jsx)(jsx_runtime_1.Fragment, { children: emptyState });
249
781
  }
@@ -259,63 +791,49 @@ function ArloFlowRenderer({ session, handlers, componentRenderers, registry, aut
259
791
  }
260
792
  if (importedPreviewScreen) {
261
793
  const previewComponents = [...(importedPreviewScreen.components ?? [])].sort((a, b) => a.order - b.order);
794
+ const previewTheme = getThemeForScreen(importedPreviewScreen);
262
795
  const previewContext = {
263
796
  ...context,
264
797
  onValueChange: () => undefined,
265
798
  onPressButton: async () => undefined,
266
799
  };
267
- return ((0, jsx_runtime_1.jsx)(react_native_1.ScrollView, { contentContainerStyle: [
268
- styles.container,
269
- getScreenContainerStyle(importedPreviewScreen),
270
- ], children: previewComponents.map((component) => {
271
- const customRenderer = componentRenderers?.[component.type];
272
- const content = customRenderer
273
- ? customRenderer(component, previewContext)
274
- : renderDefaultComponent(component, previewContext, registry);
275
- if (content === null) {
276
- return null;
277
- }
278
- return (0, jsx_runtime_1.jsx)(react_native_1.View, { children: content }, component.id);
279
- }) }));
800
+ return renderAutoLayoutScreen(importedPreviewScreen, previewComponents, previewContext, previewTheme);
280
801
  }
281
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] }) })) }));
282
803
  }
283
804
  if (snapshot.currentScreen.layoutMode === "absolute") {
284
- return ((0, jsx_runtime_1.jsx)(react_native_1.View, { style: [
285
- styles.absoluteContainer,
286
- getScreenContainerStyle(snapshot.currentScreen),
287
- ], children: sortedComponents.map((component) => {
288
- const customRenderer = componentRenderers?.[component.type];
289
- const content = customRenderer
290
- ? customRenderer(component, context)
291
- : renderDefaultComponent(component, context, registry);
292
- if (content === null) {
293
- 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));
294
- }
295
- return ((0, jsx_runtime_1.jsx)(react_native_1.View, { style: getComponentWrapperStyle(component, true), children: content }, component.id));
296
- }) }));
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)) }) }));
297
810
  }
298
- return ((0, jsx_runtime_1.jsx)(react_native_1.ScrollView, { contentContainerStyle: [
299
- styles.container,
300
- getScreenContainerStyle(snapshot.currentScreen),
301
- ], children: sortedComponents.map((component) => {
302
- const customRenderer = componentRenderers?.[component.type];
303
- const content = customRenderer
304
- ? customRenderer(component, context)
305
- : renderDefaultComponent(component, context, registry);
306
- if (content === null) {
307
- 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));
308
- }
309
- return ((0, jsx_runtime_1.jsx)(react_native_1.View, { style: getComponentWrapperStyle(component, false), children: content }, component.id));
310
- }) }));
811
+ return renderAutoLayoutScreen(snapshot.currentScreen, sortedComponents, context, theme);
311
812
  }
813
+ // ─── Styles ─────────────────────────────────────────────────────────────────
312
814
  const styles = react_native_1.StyleSheet.create({
313
815
  container: {
314
816
  flexGrow: 1,
315
- gap: 16,
817
+ alignItems: "stretch",
316
818
  },
317
819
  componentBlock: {
820
+ alignSelf: "stretch",
821
+ },
822
+ autoLayoutRoot: {
823
+ flexGrow: 1,
318
824
  width: "100%",
825
+ alignSelf: "stretch",
826
+ },
827
+ autoLayoutMain: {
828
+ flexGrow: 1,
829
+ width: "100%",
830
+ alignSelf: "stretch",
831
+ gap: 16,
832
+ },
833
+ autoLayoutBottom: {
834
+ width: "100%",
835
+ alignSelf: "stretch",
836
+ gap: 12,
319
837
  },
320
838
  absoluteContainer: {
321
839
  flex: 1,
@@ -326,24 +844,16 @@ const styles = react_native_1.StyleSheet.create({
326
844
  gap: 8,
327
845
  },
328
846
  fieldLabel: {
329
- color: "#f3f3f5",
330
847
  fontSize: 14,
331
848
  fontWeight: "600",
332
849
  },
333
850
  input: {
334
851
  borderWidth: 1,
335
- borderColor: "#2c2c34",
336
852
  borderRadius: 14,
337
853
  paddingHorizontal: 14,
338
854
  paddingVertical: 12,
339
- color: "#ffffff",
340
- backgroundColor: "#141419",
341
- },
342
- inputError: {
343
- borderColor: "#f36b8d",
344
855
  },
345
856
  fieldError: {
346
- color: "#f59cb3",
347
857
  fontSize: 12,
348
858
  fontWeight: "500",
349
859
  },
@@ -351,13 +861,30 @@ const styles = react_native_1.StyleSheet.create({
351
861
  minHeight: 52,
352
862
  alignItems: "center",
353
863
  justifyContent: "center",
354
- paddingHorizontal: 16,
355
- paddingVertical: 12,
864
+ },
865
+ buttonContent: {
866
+ flexDirection: "row",
867
+ alignItems: "center",
868
+ justifyContent: "center",
356
869
  },
357
870
  buttonText: {
358
871
  fontSize: 16,
359
872
  fontWeight: "700",
360
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
+ },
361
888
  optionGroup: {
362
889
  flexDirection: "row",
363
890
  flexWrap: "wrap",
@@ -368,37 +895,45 @@ const styles = react_native_1.StyleSheet.create({
368
895
  paddingVertical: 10,
369
896
  borderRadius: 999,
370
897
  borderWidth: 1,
371
- borderColor: "#30303a",
372
- backgroundColor: "#15151b",
373
- },
374
- optionPillSelected: {
375
- backgroundColor: "#ffffff",
376
- borderColor: "#ffffff",
377
898
  },
378
899
  optionPillText: {
379
- color: "#f1f1f3",
380
900
  fontSize: 14,
381
901
  fontWeight: "600",
382
902
  },
383
- optionPillTextSelected: {
384
- color: "#111111",
903
+ sliderField: {
904
+ gap: 6,
385
905
  },
386
- sliderCard: {
387
- borderRadius: 18,
388
- backgroundColor: "#15151b",
389
- borderWidth: 1,
390
- borderColor: "#2b2b34",
391
- padding: 14,
392
- gap: 12,
906
+ sliderTouchArea: {
907
+ height: 28,
908
+ justifyContent: "center",
909
+ position: "relative",
393
910
  },
394
- sliderValue: {
395
- color: "#ffffff",
396
- fontSize: 28,
397
- fontWeight: "700",
911
+ sliderTrack: {
912
+ height: 6,
913
+ borderRadius: 999,
914
+ overflow: "hidden",
398
915
  },
399
- sliderActions: {
916
+ sliderFill: {
917
+ position: "absolute",
918
+ left: 0,
919
+ top: 11,
920
+ height: 6,
921
+ borderRadius: 999,
922
+ },
923
+ sliderThumb: {
924
+ position: "absolute",
925
+ top: 5,
926
+ width: 18,
927
+ height: 18,
928
+ borderRadius: 999,
929
+ borderWidth: 3,
930
+ },
931
+ sliderLabels: {
400
932
  flexDirection: "row",
401
- gap: 10,
933
+ justifyContent: "space-between",
934
+ },
935
+ sliderLabel: {
936
+ fontSize: 11,
402
937
  },
403
938
  progressTrack: {
404
939
  width: "100%",