@pushframe/sdk 0.1.5 → 0.1.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (288) hide show
  1. package/lib/commonjs/PushframeProvider.js +69 -0
  2. package/lib/commonjs/PushframeProvider.js.map +1 -0
  3. package/lib/commonjs/PushframeScreen.js +69 -0
  4. package/lib/commonjs/PushframeScreen.js.map +1 -0
  5. package/lib/commonjs/bindings.js +73 -0
  6. package/lib/commonjs/bindings.js.map +1 -0
  7. package/lib/commonjs/components/ButtonComponent.js +72 -0
  8. package/lib/commonjs/components/ButtonComponent.js.map +1 -0
  9. package/lib/commonjs/components/FlatListComponent.js +73 -0
  10. package/lib/commonjs/components/FlatListComponent.js.map +1 -0
  11. package/lib/commonjs/components/ImageComponent.js +67 -0
  12. package/lib/commonjs/components/ImageComponent.js.map +1 -0
  13. package/lib/commonjs/components/PushFrameComponent.js +179 -0
  14. package/lib/commonjs/components/PushFrameComponent.js.map +1 -0
  15. package/lib/commonjs/components/PushFrameProvider.js +115 -0
  16. package/lib/commonjs/components/PushFrameProvider.js.map +1 -0
  17. package/lib/commonjs/components/PushFrameScreen.js +39 -0
  18. package/lib/commonjs/components/PushFrameScreen.js.map +1 -0
  19. package/lib/commonjs/components/ScrollViewComponent.js +64 -0
  20. package/lib/commonjs/components/ScrollViewComponent.js.map +1 -0
  21. package/lib/commonjs/components/StackComponent.js +61 -0
  22. package/lib/commonjs/components/StackComponent.js.map +1 -0
  23. package/lib/commonjs/components/TextComponent.js +62 -0
  24. package/lib/commonjs/components/TextComponent.js.map +1 -0
  25. package/lib/commonjs/conditions.js +44 -0
  26. package/lib/commonjs/conditions.js.map +1 -0
  27. package/lib/commonjs/context/PushFrameContext.js +33 -0
  28. package/lib/commonjs/context/PushFrameContext.js.map +1 -0
  29. package/lib/commonjs/index.js +200 -0
  30. package/lib/commonjs/index.js.map +1 -0
  31. package/lib/commonjs/overlays/BottomSheetHost.js +144 -0
  32. package/lib/commonjs/overlays/BottomSheetHost.js.map +1 -0
  33. package/lib/commonjs/overlays/ToastHost.js +135 -0
  34. package/lib/commonjs/overlays/ToastHost.js.map +1 -0
  35. package/lib/commonjs/package.json +1 -0
  36. package/lib/commonjs/primitives/ActivityIndicator.js +24 -0
  37. package/lib/commonjs/primitives/ActivityIndicator.js.map +1 -0
  38. package/lib/commonjs/primitives/FlatList.js +34 -0
  39. package/lib/commonjs/primitives/FlatList.js.map +1 -0
  40. package/lib/commonjs/primitives/Image.js +33 -0
  41. package/lib/commonjs/primitives/Image.js.map +1 -0
  42. package/lib/commonjs/primitives/KeyboardAvoidingView.js +24 -0
  43. package/lib/commonjs/primitives/KeyboardAvoidingView.js.map +1 -0
  44. package/lib/commonjs/primitives/Modal.js +24 -0
  45. package/lib/commonjs/primitives/Modal.js.map +1 -0
  46. package/lib/commonjs/primitives/Pressable.js +26 -0
  47. package/lib/commonjs/primitives/Pressable.js.map +1 -0
  48. package/lib/commonjs/primitives/SafeAreaView.js +38 -0
  49. package/lib/commonjs/primitives/SafeAreaView.js.map +1 -0
  50. package/lib/commonjs/primitives/ScrollView.js +26 -0
  51. package/lib/commonjs/primitives/ScrollView.js.map +1 -0
  52. package/lib/commonjs/primitives/StatusBar.js +24 -0
  53. package/lib/commonjs/primitives/StatusBar.js.map +1 -0
  54. package/lib/commonjs/primitives/Switch.js +28 -0
  55. package/lib/commonjs/primitives/Switch.js.map +1 -0
  56. package/lib/commonjs/primitives/Text.js +37 -0
  57. package/lib/commonjs/primitives/Text.js.map +1 -0
  58. package/lib/commonjs/primitives/TextInput.js +31 -0
  59. package/lib/commonjs/primitives/TextInput.js.map +1 -0
  60. package/lib/commonjs/primitives/View.js +24 -0
  61. package/lib/commonjs/primitives/View.js.map +1 -0
  62. package/lib/commonjs/primitives/index.js +97 -0
  63. package/lib/commonjs/primitives/index.js.map +1 -0
  64. package/lib/commonjs/registry/ComponentRegistry.js +70 -0
  65. package/lib/commonjs/registry/ComponentRegistry.js.map +1 -0
  66. package/lib/commonjs/registry.js +94 -0
  67. package/lib/commonjs/registry.js.map +1 -0
  68. package/lib/commonjs/renderer/RecursiveRenderer.js +202 -0
  69. package/lib/commonjs/renderer/RecursiveRenderer.js.map +1 -0
  70. package/lib/commonjs/renderer/bindingResolver.js +98 -0
  71. package/lib/commonjs/renderer/bindingResolver.js.map +1 -0
  72. package/lib/commonjs/renderer/conditionalEvaluator.js +31 -0
  73. package/lib/commonjs/renderer/conditionalEvaluator.js.map +1 -0
  74. package/lib/commonjs/renderer.js +107 -0
  75. package/lib/commonjs/renderer.js.map +1 -0
  76. package/lib/commonjs/schema.js +79 -0
  77. package/lib/commonjs/schema.js.map +1 -0
  78. package/lib/commonjs/transformer/index.js +1055 -0
  79. package/lib/commonjs/transformer/index.js.map +1 -0
  80. package/lib/commonjs/transport.js +86 -0
  81. package/lib/commonjs/transport.js.map +1 -0
  82. package/lib/module/PushframeProvider.js +62 -0
  83. package/lib/module/PushframeProvider.js.map +1 -0
  84. package/lib/module/PushframeScreen.js +65 -0
  85. package/lib/module/PushframeScreen.js.map +1 -0
  86. package/lib/module/bindings.js +68 -0
  87. package/lib/module/bindings.js.map +1 -0
  88. package/lib/module/components/ButtonComponent.js +67 -0
  89. package/lib/module/components/ButtonComponent.js.map +1 -0
  90. package/lib/module/components/FlatListComponent.js +68 -0
  91. package/lib/module/components/FlatListComponent.js.map +1 -0
  92. package/lib/module/components/ImageComponent.js +62 -0
  93. package/lib/module/components/ImageComponent.js.map +1 -0
  94. package/lib/module/components/PushFrameComponent.js +174 -0
  95. package/lib/module/components/PushFrameComponent.js.map +1 -0
  96. package/lib/module/components/PushFrameProvider.js +110 -0
  97. package/lib/module/components/PushFrameProvider.js.map +1 -0
  98. package/lib/module/components/PushFrameScreen.js +34 -0
  99. package/lib/module/components/PushFrameScreen.js.map +1 -0
  100. package/lib/module/components/ScrollViewComponent.js +59 -0
  101. package/lib/module/components/ScrollViewComponent.js.map +1 -0
  102. package/lib/module/components/StackComponent.js +56 -0
  103. package/lib/module/components/StackComponent.js.map +1 -0
  104. package/lib/module/components/TextComponent.js +57 -0
  105. package/lib/module/components/TextComponent.js.map +1 -0
  106. package/lib/module/conditions.js +40 -0
  107. package/lib/module/conditions.js.map +1 -0
  108. package/lib/module/context/PushFrameContext.js +29 -0
  109. package/lib/module/context/PushFrameContext.js.map +1 -0
  110. package/lib/module/index.js +99 -0
  111. package/lib/module/index.js.map +1 -0
  112. package/lib/module/overlays/BottomSheetHost.js +139 -0
  113. package/lib/module/overlays/BottomSheetHost.js.map +1 -0
  114. package/lib/module/overlays/ToastHost.js +130 -0
  115. package/lib/module/overlays/ToastHost.js.map +1 -0
  116. package/lib/module/primitives/ActivityIndicator.js +19 -0
  117. package/lib/module/primitives/ActivityIndicator.js.map +1 -0
  118. package/lib/module/primitives/FlatList.js +29 -0
  119. package/lib/module/primitives/FlatList.js.map +1 -0
  120. package/lib/module/primitives/Image.js +28 -0
  121. package/lib/module/primitives/Image.js.map +1 -0
  122. package/lib/module/primitives/KeyboardAvoidingView.js +19 -0
  123. package/lib/module/primitives/KeyboardAvoidingView.js.map +1 -0
  124. package/lib/module/primitives/Modal.js +19 -0
  125. package/lib/module/primitives/Modal.js.map +1 -0
  126. package/lib/module/primitives/Pressable.js +21 -0
  127. package/lib/module/primitives/Pressable.js.map +1 -0
  128. package/lib/module/primitives/SafeAreaView.js +33 -0
  129. package/lib/module/primitives/SafeAreaView.js.map +1 -0
  130. package/lib/module/primitives/ScrollView.js +21 -0
  131. package/lib/module/primitives/ScrollView.js.map +1 -0
  132. package/lib/module/primitives/StatusBar.js +19 -0
  133. package/lib/module/primitives/StatusBar.js.map +1 -0
  134. package/lib/module/primitives/Switch.js +23 -0
  135. package/lib/module/primitives/Switch.js.map +1 -0
  136. package/lib/module/primitives/Text.js +32 -0
  137. package/lib/module/primitives/Text.js.map +1 -0
  138. package/lib/module/primitives/TextInput.js +26 -0
  139. package/lib/module/primitives/TextInput.js.map +1 -0
  140. package/lib/module/primitives/View.js +19 -0
  141. package/lib/module/primitives/View.js.map +1 -0
  142. package/lib/module/primitives/index.js +16 -0
  143. package/lib/module/primitives/index.js.map +1 -0
  144. package/lib/module/registry/ComponentRegistry.js +66 -0
  145. package/lib/module/registry/ComponentRegistry.js.map +1 -0
  146. package/lib/module/registry.js +88 -0
  147. package/lib/module/registry.js.map +1 -0
  148. package/lib/module/renderer/RecursiveRenderer.js +197 -0
  149. package/lib/module/renderer/RecursiveRenderer.js.map +1 -0
  150. package/lib/module/renderer/bindingResolver.js +92 -0
  151. package/lib/module/renderer/bindingResolver.js.map +1 -0
  152. package/lib/module/renderer/conditionalEvaluator.js +28 -0
  153. package/lib/module/renderer/conditionalEvaluator.js.map +1 -0
  154. package/lib/module/renderer.js +103 -0
  155. package/lib/module/renderer.js.map +1 -0
  156. package/lib/module/schema.js +74 -0
  157. package/lib/module/schema.js.map +1 -0
  158. package/lib/module/transformer/index.js +1051 -0
  159. package/lib/module/transformer/index.js.map +1 -0
  160. package/lib/module/transport.js +82 -0
  161. package/lib/module/transport.js.map +1 -0
  162. package/lib/typescript/PushframeProvider.d.ts +58 -0
  163. package/lib/typescript/PushframeProvider.d.ts.map +1 -0
  164. package/lib/typescript/PushframeScreen.d.ts +36 -0
  165. package/lib/typescript/PushframeScreen.d.ts.map +1 -0
  166. package/lib/typescript/bindings.d.ts +29 -0
  167. package/lib/typescript/bindings.d.ts.map +1 -0
  168. package/lib/typescript/components/ButtonComponent.d.ts +11 -0
  169. package/lib/typescript/components/ButtonComponent.d.ts.map +1 -0
  170. package/lib/typescript/components/FlatListComponent.d.ts +28 -0
  171. package/lib/typescript/components/FlatListComponent.d.ts.map +1 -0
  172. package/lib/typescript/components/ImageComponent.d.ts +12 -0
  173. package/lib/typescript/components/ImageComponent.d.ts.map +1 -0
  174. package/lib/typescript/components/PushFrameComponent.d.ts +48 -0
  175. package/lib/typescript/components/PushFrameComponent.d.ts.map +1 -0
  176. package/lib/typescript/components/PushFrameProvider.d.ts +51 -0
  177. package/lib/typescript/components/PushFrameProvider.d.ts.map +1 -0
  178. package/lib/typescript/components/PushFrameScreen.d.ts +15 -0
  179. package/lib/typescript/components/PushFrameScreen.d.ts.map +1 -0
  180. package/lib/typescript/components/ScrollViewComponent.d.ts +19 -0
  181. package/lib/typescript/components/ScrollViewComponent.d.ts.map +1 -0
  182. package/lib/typescript/components/StackComponent.d.ts +16 -0
  183. package/lib/typescript/components/StackComponent.d.ts.map +1 -0
  184. package/lib/typescript/components/TextComponent.d.ts +13 -0
  185. package/lib/typescript/components/TextComponent.d.ts.map +1 -0
  186. package/lib/typescript/conditions.d.ts +12 -0
  187. package/lib/typescript/conditions.d.ts.map +1 -0
  188. package/lib/typescript/context/PushFrameContext.d.ts +57 -0
  189. package/lib/typescript/context/PushFrameContext.d.ts.map +1 -0
  190. package/lib/typescript/index.d.ts +74 -0
  191. package/lib/typescript/index.d.ts.map +1 -0
  192. package/lib/typescript/overlays/BottomSheetHost.d.ts +21 -0
  193. package/lib/typescript/overlays/BottomSheetHost.d.ts.map +1 -0
  194. package/lib/typescript/overlays/ToastHost.d.ts +12 -0
  195. package/lib/typescript/overlays/ToastHost.d.ts.map +1 -0
  196. package/lib/typescript/primitives/ActivityIndicator.d.ts +12 -0
  197. package/lib/typescript/primitives/ActivityIndicator.d.ts.map +1 -0
  198. package/lib/typescript/primitives/FlatList.d.ts +29 -0
  199. package/lib/typescript/primitives/FlatList.d.ts.map +1 -0
  200. package/lib/typescript/primitives/Image.d.ts +20 -0
  201. package/lib/typescript/primitives/Image.d.ts.map +1 -0
  202. package/lib/typescript/primitives/KeyboardAvoidingView.d.ts +12 -0
  203. package/lib/typescript/primitives/KeyboardAvoidingView.d.ts.map +1 -0
  204. package/lib/typescript/primitives/Modal.d.ts +12 -0
  205. package/lib/typescript/primitives/Modal.d.ts.map +1 -0
  206. package/lib/typescript/primitives/Pressable.d.ts +14 -0
  207. package/lib/typescript/primitives/Pressable.d.ts.map +1 -0
  208. package/lib/typescript/primitives/SafeAreaView.d.ts +20 -0
  209. package/lib/typescript/primitives/SafeAreaView.d.ts.map +1 -0
  210. package/lib/typescript/primitives/ScrollView.d.ts +15 -0
  211. package/lib/typescript/primitives/ScrollView.d.ts.map +1 -0
  212. package/lib/typescript/primitives/StatusBar.d.ts +12 -0
  213. package/lib/typescript/primitives/StatusBar.d.ts.map +1 -0
  214. package/lib/typescript/primitives/Switch.d.ts +19 -0
  215. package/lib/typescript/primitives/Switch.d.ts.map +1 -0
  216. package/lib/typescript/primitives/Text.d.ts +25 -0
  217. package/lib/typescript/primitives/Text.d.ts.map +1 -0
  218. package/lib/typescript/primitives/TextInput.d.ts +25 -0
  219. package/lib/typescript/primitives/TextInput.d.ts.map +1 -0
  220. package/lib/typescript/primitives/View.d.ts +12 -0
  221. package/lib/typescript/primitives/View.d.ts.map +1 -0
  222. package/lib/typescript/primitives/index.d.ts +27 -0
  223. package/lib/typescript/primitives/index.d.ts.map +1 -0
  224. package/lib/typescript/registry/ComponentRegistry.d.ts +21 -0
  225. package/lib/typescript/registry/ComponentRegistry.d.ts.map +1 -0
  226. package/lib/typescript/registry.d.ts +57 -0
  227. package/lib/typescript/registry.d.ts.map +1 -0
  228. package/lib/typescript/renderer/RecursiveRenderer.d.ts +32 -0
  229. package/lib/typescript/renderer/RecursiveRenderer.d.ts.map +1 -0
  230. package/lib/typescript/renderer/bindingResolver.d.ts +26 -0
  231. package/lib/typescript/renderer/bindingResolver.d.ts.map +1 -0
  232. package/lib/typescript/renderer/conditionalEvaluator.d.ts +15 -0
  233. package/lib/typescript/renderer/conditionalEvaluator.d.ts.map +1 -0
  234. package/lib/typescript/renderer.d.ts +29 -0
  235. package/lib/typescript/renderer.d.ts.map +1 -0
  236. package/lib/typescript/schema.d.ts +84 -0
  237. package/lib/typescript/schema.d.ts.map +1 -0
  238. package/lib/typescript/transformer/index.d.ts +49 -0
  239. package/lib/typescript/transformer/index.d.ts.map +1 -0
  240. package/lib/typescript/transport.d.ts +19 -0
  241. package/lib/typescript/transport.d.ts.map +1 -0
  242. package/package.json +20 -18
  243. package/src/PushframeProvider.tsx +119 -0
  244. package/src/PushframeScreen.tsx +107 -0
  245. package/src/bindings.ts +72 -0
  246. package/src/components/ButtonComponent.tsx +87 -0
  247. package/src/components/FlatListComponent.tsx +86 -0
  248. package/src/components/ImageComponent.tsx +70 -0
  249. package/src/components/PushFrameComponent.tsx +221 -0
  250. package/src/components/PushFrameProvider.tsx +177 -0
  251. package/src/components/PushFrameScreen.tsx +30 -0
  252. package/src/components/ScrollViewComponent.tsx +65 -0
  253. package/src/components/StackComponent.tsx +69 -0
  254. package/src/components/TextComponent.tsx +60 -0
  255. package/src/conditions.ts +46 -0
  256. package/src/context/PushFrameContext.ts +89 -0
  257. package/src/index.ts +119 -0
  258. package/src/overlays/BottomSheetHost.tsx +175 -0
  259. package/src/overlays/ToastHost.tsx +147 -0
  260. package/src/primitives/ActivityIndicator.tsx +21 -0
  261. package/src/primitives/FlatList.tsx +49 -0
  262. package/src/primitives/Image.tsx +26 -0
  263. package/src/primitives/KeyboardAvoidingView.tsx +21 -0
  264. package/src/primitives/Modal.tsx +17 -0
  265. package/src/primitives/Pressable.tsx +19 -0
  266. package/src/primitives/SafeAreaView.tsx +42 -0
  267. package/src/primitives/ScrollView.tsx +21 -0
  268. package/src/primitives/StatusBar.tsx +17 -0
  269. package/src/primitives/Switch.tsx +24 -0
  270. package/src/primitives/Text.tsx +43 -0
  271. package/src/primitives/TextInput.tsx +42 -0
  272. package/src/primitives/View.tsx +17 -0
  273. package/src/primitives/index.ts +38 -0
  274. package/src/registry/ComponentRegistry.ts +99 -0
  275. package/src/registry.ts +99 -0
  276. package/src/renderer/RecursiveRenderer.tsx +242 -0
  277. package/src/renderer/bindingResolver.ts +94 -0
  278. package/src/renderer/conditionalEvaluator.ts +29 -0
  279. package/src/renderer.tsx +124 -0
  280. package/src/schema.ts +132 -0
  281. package/src/transformer/index.ts +1016 -0
  282. package/src/transport.ts +104 -0
  283. package/dist/index.d.mts +0 -534
  284. package/dist/index.d.ts +0 -534
  285. package/dist/index.js +0 -1572
  286. package/dist/index.js.map +0 -1
  287. package/dist/index.mjs +0 -1541
  288. package/dist/index.mjs.map +0 -1
@@ -0,0 +1,1016 @@
1
+ /**
2
+ * Craft.js → PushFrame SDUI transformer
3
+ *
4
+ * Converts a Craft.js editor schema (produced by the dashboard) into the
5
+ * SDUINode tree consumed by the SDK's RecursiveRenderer at runtime.
6
+ *
7
+ * This module is intended for use by the backend/service layer or the
8
+ * dashboard — it is NOT executed inside the React Native app at runtime.
9
+ */
10
+
11
+ import type { Action, SchemaNode } from '../context/PushFrameContext';
12
+
13
+ // ─── Craft input types ────────────────────────────────────────────────────────
14
+
15
+ export type CraftNode = {
16
+ type: { resolvedName: string };
17
+ props?: Record<string, unknown>;
18
+ nodes?: string[];
19
+ parent?: string;
20
+ };
21
+
22
+ export type CraftSchema = Record<string, CraftNode>;
23
+
24
+ // ─── Output types ─────────────────────────────────────────────────────────────
25
+
26
+ type SpacingValue = number | { top?: number; right?: number; bottom?: number; left?: number };
27
+ type RadiusValue = number | { tl?: number; tr?: number; br?: number; bl?: number };
28
+ type SizeModeValue = 'fill' | 'hug' | number;
29
+
30
+ interface ShadowValue {
31
+ color: string;
32
+ offsetX: number;
33
+ offsetY: number;
34
+ blur: number;
35
+ /** 0–1 */
36
+ opacity: number;
37
+ }
38
+
39
+ interface BorderValue {
40
+ width?: number;
41
+ color?: string;
42
+ style?: 'solid' | 'dotted' | 'dashed';
43
+ widths?: { top?: number; right?: number; bottom?: number; left?: number };
44
+ }
45
+
46
+ interface SafeAreaValue {
47
+ top?: boolean;
48
+ right?: boolean;
49
+ bottom?: boolean;
50
+ left?: boolean;
51
+ }
52
+
53
+ /**
54
+ * The SDUI node shape produced by this transformer and consumed by the SDK's
55
+ * RecursiveRenderer. Aligned with `SchemaNode` from the SDK context.
56
+ */
57
+ export interface SDUINode extends SchemaNode {
58
+ id: string;
59
+ type: string;
60
+ props: Record<string, unknown>;
61
+ children?: SDUINode[];
62
+ /** FlatList per-item render template. */
63
+ itemTemplate?: SDUINode;
64
+ /** Conditional render expression. Falsy → node is hidden. */
65
+ if?: unknown;
66
+ actions?: Action[];
67
+ }
68
+
69
+ // ─── Constants ────────────────────────────────────────────────────────────────
70
+
71
+ const BRIDGE_KEYS = new Set([
72
+ '_schemaId', '_schemaType', '_schemaActions', '_schemaIf', '_schemaRenderItem',
73
+ ]);
74
+
75
+ const ALIGN_MAP: Record<string, string> = {
76
+ start: 'flex-start',
77
+ center: 'center',
78
+ end: 'flex-end',
79
+ stretch: 'stretch',
80
+ baseline: 'baseline',
81
+ };
82
+
83
+ const DISTRIBUTE_MAP: Record<string, string> = {
84
+ start: 'flex-start',
85
+ center: 'center',
86
+ end: 'flex-end',
87
+ 'space-between': 'space-between',
88
+ 'space-around': 'space-around',
89
+ 'space-evenly': 'space-evenly',
90
+ };
91
+
92
+ const FIT_TO_RESIZE_MODE: Record<string, string> = {
93
+ cover: 'cover',
94
+ contain: 'contain',
95
+ fill: 'stretch', // RN uses 'stretch', not 'fill'
96
+ center: 'center',
97
+ };
98
+
99
+ const FONT_WEIGHT_MAP: Record<string, string> = {
100
+ regular: '400',
101
+ medium: '500',
102
+ semibold: '600',
103
+ bold: '700',
104
+ };
105
+
106
+ const TEXT_STYLE_PRESETS: Record<string, {
107
+ fontSize: number;
108
+ fontWeight: string;
109
+ lineHeight?: number;
110
+ textTransform?: string;
111
+ letterSpacing?: number;
112
+ }> = {
113
+ 'heading-1': { fontSize: 28, fontWeight: '700' },
114
+ 'heading-2': { fontSize: 22, fontWeight: '700' },
115
+ 'heading-3': { fontSize: 18, fontWeight: '600' },
116
+ 'body': { fontSize: 16, fontWeight: '400' },
117
+ 'body-sm': { fontSize: 14, fontWeight: '400' },
118
+ 'caption': { fontSize: 12, fontWeight: '400' },
119
+ 'label': { fontSize: 13, fontWeight: '500' },
120
+ 'overline': { fontSize: 11, fontWeight: '500', textTransform: 'uppercase', letterSpacing: 0.8 },
121
+ };
122
+
123
+ // inputType → RN keyboard + behaviour props
124
+ const INPUT_TYPE_MAP: Record<string, {
125
+ keyboardType: string;
126
+ secureTextEntry: boolean;
127
+ autoCapitalize: string;
128
+ autoCorrect: boolean;
129
+ }> = {
130
+ text: { keyboardType: 'default', secureTextEntry: false, autoCapitalize: 'sentences', autoCorrect: true },
131
+ email: { keyboardType: 'email-address', secureTextEntry: false, autoCapitalize: 'none', autoCorrect: false },
132
+ password: { keyboardType: 'default', secureTextEntry: true, autoCapitalize: 'none', autoCorrect: false },
133
+ number: { keyboardType: 'numeric', secureTextEntry: false, autoCapitalize: 'none', autoCorrect: false },
134
+ decimal: { keyboardType: 'decimal-pad', secureTextEntry: false, autoCapitalize: 'none', autoCorrect: false },
135
+ phone: { keyboardType: 'phone-pad', secureTextEntry: false, autoCapitalize: 'none', autoCorrect: false },
136
+ url: { keyboardType: 'url', secureTextEntry: false, autoCapitalize: 'none', autoCorrect: false },
137
+ search: { keyboardType: 'default', secureTextEntry: false, autoCapitalize: 'none', autoCorrect: false },
138
+ };
139
+
140
+ // ─── Prop helpers ─────────────────────────────────────────────────────────────
141
+
142
+ function stripBridgeKeys(props: Record<string, unknown>): Record<string, unknown> {
143
+ const out: Record<string, unknown> = {};
144
+ for (const k in props) {
145
+ if (!BRIDGE_KEYS.has(k)) out[k] = props[k];
146
+ }
147
+ return out;
148
+ }
149
+
150
+ function extractMeta(raw: Record<string, unknown>): {
151
+ actions: Action[] | undefined;
152
+ condition: unknown;
153
+ } {
154
+ return {
155
+ actions: raw._schemaActions as Action[] | undefined,
156
+ condition: raw._schemaIf,
157
+ };
158
+ }
159
+
160
+ // ─── RN style sub-builders ────────────────────────────────────────────────────
161
+
162
+ function resolveSizeModeRN(
163
+ value: SizeModeValue | undefined,
164
+ axis: 'width' | 'height',
165
+ out: Record<string, unknown>,
166
+ parentDirection: 'vertical' | 'horizontal',
167
+ ): void {
168
+ if (value === undefined) return;
169
+ if (value === 'fill') {
170
+ const isMainAxis =
171
+ (parentDirection === 'vertical' && axis === 'height') ||
172
+ (parentDirection === 'horizontal' && axis === 'width');
173
+ if (isMainAxis) {
174
+ out.flex = 1;
175
+ } else {
176
+ out.alignSelf = 'stretch';
177
+ }
178
+ } else if (value === 'hug') {
179
+ out.alignSelf = 'flex-start';
180
+ } else {
181
+ out[axis] = value;
182
+ }
183
+ }
184
+
185
+ function resolvePaddingRN(padding: SpacingValue | undefined): Record<string, unknown> {
186
+ if (padding === undefined) return {};
187
+ if (typeof padding === 'number') return { padding };
188
+ const { top, right, bottom, left } = padding;
189
+ if (top === bottom && left === right && top !== undefined && left !== undefined) {
190
+ return { paddingVertical: top, paddingHorizontal: left };
191
+ }
192
+ return {
193
+ ...(top !== undefined ? { paddingTop: top } : {}),
194
+ ...(right !== undefined ? { paddingRight: right } : {}),
195
+ ...(bottom !== undefined ? { paddingBottom: bottom } : {}),
196
+ ...(left !== undefined ? { paddingLeft: left } : {}),
197
+ };
198
+ }
199
+
200
+ function resolveRadiusRN(radius: RadiusValue | undefined): Record<string, unknown> {
201
+ if (radius === undefined) return {};
202
+ if (typeof radius === 'number') return { borderRadius: radius };
203
+ return {
204
+ ...(radius.tl !== undefined ? { borderTopLeftRadius: radius.tl } : {}),
205
+ ...(radius.tr !== undefined ? { borderTopRightRadius: radius.tr } : {}),
206
+ ...(radius.br !== undefined ? { borderBottomRightRadius: radius.br } : {}),
207
+ ...(radius.bl !== undefined ? { borderBottomLeftRadius: radius.bl } : {}),
208
+ };
209
+ }
210
+
211
+ function resolveBorderRN(border: BorderValue | undefined): Record<string, unknown> {
212
+ if (!border) return {};
213
+ const out: Record<string, unknown> = {};
214
+ if (border.style) out.borderStyle = border.style;
215
+ if (border.color) out.borderColor = border.color;
216
+ if (border.widths) {
217
+ const w = border.widths;
218
+ if (w.top !== undefined) out.borderTopWidth = w.top;
219
+ if (w.right !== undefined) out.borderRightWidth = w.right;
220
+ if (w.bottom !== undefined) out.borderBottomWidth = w.bottom;
221
+ if (w.left !== undefined) out.borderLeftWidth = w.left;
222
+ } else if (border.width !== undefined) {
223
+ out.borderWidth = border.width;
224
+ }
225
+ return out;
226
+ }
227
+
228
+ function resolveShadowRN(shadow: ShadowValue | undefined): Record<string, unknown> {
229
+ if (!shadow) return {};
230
+ return {
231
+ shadowColor: shadow.color,
232
+ shadowOffset: { width: shadow.offsetX, height: shadow.offsetY },
233
+ shadowRadius: shadow.blur,
234
+ shadowOpacity: shadow.opacity,
235
+ elevation: Math.round(shadow.blur / 2),
236
+ };
237
+ }
238
+
239
+ function resolveTransformRN(props: {
240
+ rotate?: number;
241
+ scale?: number;
242
+ translateX?: number;
243
+ translateY?: number;
244
+ }): Record<string, unknown> {
245
+ const transforms: Record<string, unknown>[] = [];
246
+ if (props.rotate !== undefined && props.rotate !== 0) transforms.push({ rotate: `${props.rotate}deg` });
247
+ if (props.scale !== undefined && props.scale !== 1) transforms.push({ scale: props.scale });
248
+ if (props.translateX !== undefined && props.translateX !== 0) transforms.push({ translateX: props.translateX });
249
+ if (props.translateY !== undefined && props.translateY !== 0) transforms.push({ translateY: props.translateY });
250
+ return transforms.length ? { transform: transforms } : {};
251
+ }
252
+
253
+ function resolvePositionRN(props: Record<string, unknown>): Record<string, unknown> {
254
+ const out: Record<string, unknown> = {};
255
+ if (props.position) out.position = props.position;
256
+ if (props.position === 'absolute') {
257
+ if (props.top !== undefined) out.top = props.top;
258
+ if (props.right !== undefined) out.right = props.right;
259
+ if (props.bottom !== undefined) out.bottom = props.bottom;
260
+ if (props.left !== undefined) out.left = props.left;
261
+ }
262
+ if (props.zIndex !== undefined) out.zIndex = props.zIndex;
263
+ return out;
264
+ }
265
+
266
+ function resolveAccessibilityProps(props: Record<string, unknown>): Record<string, unknown> {
267
+ const out: Record<string, unknown> = {};
268
+ if (props.accessible !== undefined) out.accessible = props.accessible;
269
+ if (props.accessibilityLabel) out.accessibilityLabel = props.accessibilityLabel;
270
+ if (props.accessibilityRole) out.accessibilityRole = props.accessibilityRole;
271
+ if (props.testID) out.testID = props.testID;
272
+ return out;
273
+ }
274
+
275
+ // ─── Full frame-style builder ─────────────────────────────────────────────────
276
+
277
+ function buildFrameStyle(
278
+ props: Record<string, unknown>,
279
+ parentDirection: 'vertical' | 'horizontal',
280
+ ): Record<string, unknown> {
281
+ const style: Record<string, unknown> = {};
282
+ const frameType = (props.type as string | undefined) ?? 'stack';
283
+
284
+ if (frameType === 'grid') {
285
+ if (props.align) style.alignItems = ALIGN_MAP[props.align as string] ?? props.align;
286
+ } else {
287
+ style.flexDirection = props.direction === 'horizontal' ? 'row' : 'column';
288
+ if (props.align) style.alignItems = ALIGN_MAP[props.align as string] ?? props.align;
289
+ if (props.distribute) style.justifyContent = DISTRIBUTE_MAP[props.distribute as string] ?? props.distribute;
290
+ if (props.gap !== undefined && props.gap !== 0) style.gap = props.gap;
291
+ if (props.wrap === 'yes') style.flexWrap = 'wrap';
292
+ }
293
+
294
+ resolveSizeModeRN(props.width as SizeModeValue | undefined, 'width', style, parentDirection);
295
+ resolveSizeModeRN(props.height as SizeModeValue | undefined, 'height', style, parentDirection);
296
+ if (props.minWidth !== undefined) style.minWidth = props.minWidth;
297
+ if (props.maxWidth !== undefined) style.maxWidth = props.maxWidth;
298
+ if (props.minHeight !== undefined) style.minHeight = props.minHeight;
299
+ if (props.maxHeight !== undefined) style.maxHeight = props.maxHeight;
300
+ if (props.aspectRatio !== undefined) style.aspectRatio = props.aspectRatio;
301
+
302
+ Object.assign(style, resolvePaddingRN(props.padding as SpacingValue | undefined));
303
+
304
+ if (props.fill && props.fill !== 'transparent') style.backgroundColor = props.fill;
305
+ Object.assign(style, resolveRadiusRN(props.radius as RadiusValue | undefined));
306
+ Object.assign(style, resolveBorderRN(props.border as BorderValue | undefined));
307
+ Object.assign(style, resolveShadowRN(props.shadow as ShadowValue | undefined));
308
+ if (props.opacity !== undefined) style.opacity = (props.opacity as number) / 100;
309
+ if (props.overflow === true) style.overflow = 'hidden';
310
+
311
+ Object.assign(style, resolvePositionRN(props));
312
+ Object.assign(style, resolveTransformRN(props as Parameters<typeof resolveTransformRN>[0]));
313
+
314
+ return style;
315
+ }
316
+
317
+ function splitFrameStyle(style: Record<string, unknown>): {
318
+ outer: Record<string, unknown>;
319
+ inner: Record<string, unknown>;
320
+ } {
321
+ const OUTER_KEYS = new Set([
322
+ 'flex', 'alignSelf', 'width', 'height',
323
+ 'minWidth', 'maxWidth', 'minHeight', 'maxHeight', 'aspectRatio',
324
+ 'position', 'top', 'right', 'bottom', 'left', 'zIndex',
325
+ 'transform',
326
+ ]);
327
+ const outer: Record<string, unknown> = {};
328
+ const inner: Record<string, unknown> = {};
329
+ for (const [k, v] of Object.entries(style)) {
330
+ if (OUTER_KEYS.has(k)) outer[k] = v;
331
+ else inner[k] = v;
332
+ }
333
+ return { outer, inner };
334
+ }
335
+
336
+ function splitScrollStyle(style: Record<string, unknown>): {
337
+ containerStyle: Record<string, unknown>;
338
+ contentStyle: Record<string, unknown>;
339
+ } {
340
+ const CONTENT_KEYS = new Set([
341
+ 'flexDirection', 'justifyContent', 'alignItems', 'flexWrap', 'gap',
342
+ 'padding', 'paddingTop', 'paddingRight', 'paddingBottom', 'paddingLeft',
343
+ 'paddingVertical', 'paddingHorizontal',
344
+ ]);
345
+ const containerStyle: Record<string, unknown> = {};
346
+ const contentStyle: Record<string, unknown> = {};
347
+ for (const [k, v] of Object.entries(style)) {
348
+ if (CONTENT_KEYS.has(k)) contentStyle[k] = v;
349
+ else containerStyle[k] = v;
350
+ }
351
+ return { containerStyle, contentStyle };
352
+ }
353
+
354
+ // ─── Safe area helper ─────────────────────────────────────────────────────────
355
+
356
+ function safeAreaEdges(safeArea: SafeAreaValue | undefined): string[] {
357
+ if (!safeArea) return [];
358
+ return (Object.entries(safeArea) as [string, boolean][])
359
+ .filter(([, v]) => v === true)
360
+ .map(([edge]) => edge);
361
+ }
362
+
363
+ // ─── Custom component definition ──────────────────────────────────────────────
364
+
365
+ export interface CustomComponentDef {
366
+ rootNodeId: string;
367
+ nodes: CraftSchema;
368
+ }
369
+
370
+ export type CustomComponentsMap = Record<string, CustomComponentDef>;
371
+
372
+ // ─── Node transformers ────────────────────────────────────────────────────────
373
+
374
+ function transformScreen(
375
+ id: string,
376
+ raw: Record<string, unknown>,
377
+ children: SDUINode[],
378
+ ): SDUINode {
379
+ const { actions, condition } = extractMeta(raw);
380
+ const props = stripBridgeKeys(raw);
381
+
382
+ const edges = safeAreaEdges(props.safeArea as SafeAreaValue | undefined);
383
+ const hasSafeArea = edges.length > 0;
384
+ const vis = props.visible !== undefined && props.visible !== true ? { visible: props.visible } : {};
385
+
386
+ const style: Record<string, unknown> = { flex: 1 };
387
+ if (props.backgroundColor) style.backgroundColor = props.backgroundColor;
388
+
389
+ let content: SDUINode[];
390
+ if (props.scrollable) {
391
+ const paddingStyle = resolvePaddingRN(props.padding as SpacingValue | undefined);
392
+ const scrollNode: SDUINode = {
393
+ id: `${id}__scroll`,
394
+ type: 'scrollview',
395
+ props: {
396
+ style: { flex: 1 },
397
+ ...(Object.keys(paddingStyle).length ? { contentContainerStyle: paddingStyle } : {}),
398
+ showsVerticalScrollIndicator: false,
399
+ showsHorizontalScrollIndicator: false,
400
+ },
401
+ children,
402
+ };
403
+ content = [scrollNode];
404
+ } else {
405
+ Object.assign(style, resolvePaddingRN(props.padding as SpacingValue | undefined));
406
+ content = children;
407
+ }
408
+
409
+ const statusBar = props.statusBarStyle && props.statusBarStyle !== 'dark'
410
+ ? { statusBarStyle: props.statusBarStyle }
411
+ : {};
412
+
413
+ const node: SDUINode = {
414
+ id,
415
+ type: hasSafeArea ? 'safeareaview' : 'view',
416
+ props: {
417
+ style,
418
+ ...(hasSafeArea ? { edges } : {}),
419
+ ...statusBar,
420
+ ...vis,
421
+ },
422
+ children: content,
423
+ };
424
+
425
+ if (condition !== undefined) node.if = condition;
426
+ if (actions) node.actions = actions;
427
+ return node;
428
+ }
429
+
430
+ function transformFrame(
431
+ id: string,
432
+ raw: Record<string, unknown>,
433
+ children: SDUINode[],
434
+ parentDirection: 'vertical' | 'horizontal',
435
+ ): SDUINode {
436
+ const { actions, condition } = extractMeta(raw);
437
+ const p = stripBridgeKeys(raw);
438
+
439
+ const isGrid = ((p.type as string | undefined) ?? 'stack') === 'grid';
440
+ const isScrollable = p.scrollable === true;
441
+ const isPressable = p.pressable === true;
442
+ const direction: 'vertical' | 'horizontal' = p.direction === 'horizontal' ? 'horizontal' : 'vertical';
443
+ const edges = safeAreaEdges(p.safeArea as SafeAreaValue | undefined);
444
+ const hasSafeArea = edges.length > 0;
445
+ const access = resolveAccessibilityProps(p);
446
+ const vis = p.visible !== undefined && p.visible !== true ? { visible: p.visible } : {};
447
+
448
+ const fullStyle = buildFrameStyle(p, parentDirection);
449
+ const { outer: sizingStyle, inner: appearanceStyle } = splitFrameStyle(fullStyle);
450
+ const { containerStyle: scrollOuter, contentStyle: scrollContent } = splitScrollStyle(fullStyle);
451
+
452
+ function finalize(node: SDUINode): SDUINode {
453
+ if (condition !== undefined) node.if = condition;
454
+ if (actions && node.type === 'pressable') node.actions = actions;
455
+ else if (actions && !isPressable) node.actions = actions;
456
+ return node;
457
+ }
458
+
459
+ // ── Grid ──────────────────────────────────────────────────────────────────
460
+ if (isGrid) {
461
+ const gridStyle: Record<string, unknown> = { ...fullStyle, flexDirection: 'row', flexWrap: 'wrap' };
462
+ delete gridStyle.gap;
463
+ if (p.columnGap !== undefined) gridStyle.columnGap = p.columnGap;
464
+ if (p.rowGap !== undefined) gridStyle.rowGap = p.rowGap;
465
+
466
+ const gridView: SDUINode = {
467
+ id: hasSafeArea || isPressable ? `${id}__grid` : id,
468
+ type: 'view',
469
+ props: {
470
+ ...(Object.keys(gridStyle).length ? { style: gridStyle } : {}),
471
+ numColumns: (p.columns as number | undefined) ?? 2,
472
+ ...access,
473
+ ...(hasSafeArea || isPressable ? {} : vis),
474
+ },
475
+ children,
476
+ };
477
+
478
+ if (!hasSafeArea && !isPressable) return finalize(gridView);
479
+
480
+ let inner: SDUINode = gridView;
481
+
482
+ if (hasSafeArea) {
483
+ inner = {
484
+ id: isPressable ? `${id}__sav` : id,
485
+ type: 'safeareaview',
486
+ props: { edges, ...(isPressable ? {} : vis) },
487
+ children: [gridView],
488
+ };
489
+ }
490
+
491
+ if (!isPressable) return finalize(inner);
492
+
493
+ return finalize({
494
+ id,
495
+ type: 'pressable',
496
+ props: {
497
+ ...(Object.keys(sizingStyle).length ? { style: sizingStyle } : {}),
498
+ ...(actions ? { actions } : {}),
499
+ ...vis,
500
+ },
501
+ children: [inner],
502
+ });
503
+ }
504
+
505
+ // ── Scrollable ────────────────────────────────────────────────────────────
506
+ if (isScrollable) {
507
+ const scrollNode: SDUINode = {
508
+ id: hasSafeArea || isPressable ? `${id}__scroll` : id,
509
+ type: 'scrollview',
510
+ props: {
511
+ ...(Object.keys(scrollOuter).length ? { style: scrollOuter } : {}),
512
+ ...(Object.keys(scrollContent).length ? { contentContainerStyle: scrollContent } : {}),
513
+ horizontal: direction === 'horizontal',
514
+ showsHorizontalScrollIndicator: p.showScrollIndicator === true,
515
+ showsVerticalScrollIndicator: p.showScrollIndicator === true,
516
+ ...access,
517
+ ...(hasSafeArea || isPressable ? {} : vis),
518
+ },
519
+ children,
520
+ };
521
+
522
+ if (!hasSafeArea && !isPressable) return finalize(scrollNode);
523
+
524
+ let inner: SDUINode = scrollNode;
525
+
526
+ if (hasSafeArea) {
527
+ inner = {
528
+ id: isPressable ? `${id}__sav` : id,
529
+ type: 'safeareaview',
530
+ props: {
531
+ edges,
532
+ ...(Object.keys(sizingStyle).length ? { style: sizingStyle } : {}),
533
+ ...(isPressable ? {} : vis),
534
+ },
535
+ children: [scrollNode],
536
+ };
537
+ }
538
+
539
+ if (!isPressable) return finalize(inner);
540
+
541
+ return finalize({
542
+ id,
543
+ type: 'pressable',
544
+ props: {
545
+ ...(Object.keys(sizingStyle).length ? { style: sizingStyle } : {}),
546
+ ...(actions ? { actions } : {}),
547
+ ...vis,
548
+ },
549
+ children: [inner],
550
+ });
551
+ }
552
+
553
+ // ── Plain view / safeareaview ─────────────────────────────────────────────
554
+
555
+ if (!isPressable && !hasSafeArea) {
556
+ return finalize({
557
+ id,
558
+ type: 'view',
559
+ props: {
560
+ ...(Object.keys(fullStyle).length ? { style: fullStyle } : {}),
561
+ ...access,
562
+ ...vis,
563
+ },
564
+ children,
565
+ });
566
+ }
567
+
568
+ if (!isPressable && hasSafeArea) {
569
+ return finalize({
570
+ id,
571
+ type: 'safeareaview',
572
+ props: {
573
+ edges,
574
+ ...(Object.keys(fullStyle).length ? { style: fullStyle } : {}),
575
+ ...access,
576
+ ...vis,
577
+ },
578
+ children,
579
+ });
580
+ }
581
+
582
+ // isPressable === true
583
+ const innerNode: SDUINode = hasSafeArea
584
+ ? {
585
+ id: `${id}__sav`,
586
+ type: 'safeareaview',
587
+ props: {
588
+ edges,
589
+ ...(Object.keys(appearanceStyle).length ? { style: appearanceStyle } : {}),
590
+ ...access,
591
+ },
592
+ children,
593
+ }
594
+ : {
595
+ id: `${id}__content`,
596
+ type: 'view',
597
+ props: {
598
+ ...(Object.keys(appearanceStyle).length ? { style: appearanceStyle } : {}),
599
+ ...access,
600
+ },
601
+ children,
602
+ };
603
+
604
+ return finalize({
605
+ id,
606
+ type: 'pressable',
607
+ props: {
608
+ ...(Object.keys(sizingStyle).length ? { style: sizingStyle } : {}),
609
+ ...(actions ? { actions } : {}),
610
+ ...vis,
611
+ },
612
+ children: [innerNode],
613
+ });
614
+ }
615
+
616
+ function transformText(
617
+ id: string,
618
+ raw: Record<string, unknown>,
619
+ parentDirection: 'vertical' | 'horizontal',
620
+ ): SDUINode {
621
+ const { actions, condition } = extractMeta(raw);
622
+ const props = stripBridgeKeys(raw);
623
+
624
+ const style: Record<string, unknown> = {};
625
+
626
+ if (props.textStyle) {
627
+ const preset = TEXT_STYLE_PRESETS[props.textStyle as string];
628
+ if (preset) {
629
+ style.fontSize = preset.fontSize;
630
+ style.fontWeight = preset.fontWeight;
631
+ if (preset.lineHeight !== undefined) style.lineHeight = preset.lineHeight;
632
+ if (preset.textTransform) style.textTransform = preset.textTransform;
633
+ if (preset.letterSpacing !== undefined) style.letterSpacing = preset.letterSpacing;
634
+ }
635
+ }
636
+
637
+ if (props.color) style.color = props.color;
638
+ if (props.fontSize !== undefined) style.fontSize = props.fontSize;
639
+ if (props.fontWeight) style.fontWeight = FONT_WEIGHT_MAP[props.fontWeight as string] ?? props.fontWeight;
640
+ if (props.fontFamily) style.fontFamily = props.fontFamily;
641
+ if (props.fontStyle) style.fontStyle = props.fontStyle;
642
+ if (props.lineHeight !== undefined) style.lineHeight = props.lineHeight;
643
+ if (props.letterSpacing !== undefined) style.letterSpacing = props.letterSpacing;
644
+ if (props.align) style.textAlign = props.align;
645
+ if (props.transform && props.transform !== 'none') style.textTransform = props.transform;
646
+ if (props.decoration && props.decoration !== 'none') style.textDecorationLine = props.decoration;
647
+ if (props.opacity !== undefined) style.opacity = (props.opacity as number) / 100;
648
+ Object.assign(style, resolvePositionRN(props));
649
+ resolveSizeModeRN(props.width as SizeModeValue | undefined, 'width', style, parentDirection);
650
+
651
+ const textProps: Record<string, unknown> = {};
652
+ if (typeof props.truncate === 'number' && props.truncate > 0) {
653
+ textProps.numberOfLines = props.truncate;
654
+ textProps.ellipsizeMode = 'tail';
655
+ }
656
+ if (props.adjustFontSize === true) {
657
+ textProps.adjustsFontSizeToFit = true;
658
+ if (props.minFontSize !== undefined) textProps.minimumFontScale = props.minFontSize;
659
+ }
660
+ if (props.selectable !== undefined) textProps.selectable = props.selectable;
661
+
662
+ const node: SDUINode = {
663
+ id,
664
+ type: 'text',
665
+ props: {
666
+ // SDK contract: text content lives in the `value` prop.
667
+ // The Craft editor stores text in `content`; map it to `value` here.
668
+ value: (props.content as string | undefined) ?? '',
669
+ ...(Object.keys(style).length ? { style } : {}),
670
+ ...textProps,
671
+ ...resolveAccessibilityProps(props),
672
+ ...(props.visible !== undefined && props.visible !== true ? { visible: props.visible } : {}),
673
+ },
674
+ };
675
+ if (condition !== undefined) node.if = condition;
676
+ if (actions) node.actions = actions;
677
+ return node;
678
+ }
679
+
680
+ function transformImage(
681
+ id: string,
682
+ raw: Record<string, unknown>,
683
+ children: SDUINode[],
684
+ parentDirection: 'vertical' | 'horizontal',
685
+ ): SDUINode {
686
+ const { actions, condition } = extractMeta(raw);
687
+ const props = stripBridgeKeys(raw);
688
+
689
+ const style: Record<string, unknown> = {};
690
+ resolveSizeModeRN(props.width as SizeModeValue | undefined, 'width', style, parentDirection);
691
+ resolveSizeModeRN(props.height as SizeModeValue | undefined, 'height', style, parentDirection);
692
+ if (props.aspectRatio !== undefined) style.aspectRatio = props.aspectRatio;
693
+ Object.assign(style, resolveRadiusRN(props.radius as RadiusValue | undefined));
694
+ if (props.opacity !== undefined) style.opacity = (props.opacity as number) / 100;
695
+ Object.assign(style, resolvePositionRN(props));
696
+
697
+ const imageProps: Record<string, unknown> = {};
698
+ if (props.src) imageProps.source = { uri: props.src };
699
+ if (props.placeholder) imageProps.defaultSource = { uri: props.placeholder };
700
+ if (props.fallback) imageProps.fallback = { uri: props.fallback };
701
+ imageProps.resizeMode = FIT_TO_RESIZE_MODE[(props.fit as string | undefined) ?? 'cover'] ?? 'cover';
702
+
703
+ if (props.background === true) imageProps.background = true;
704
+
705
+ if (props.decorative) {
706
+ imageProps.accessible = false;
707
+ imageProps.accessibilityElementsHidden = true;
708
+ } else if (props.alt) {
709
+ imageProps.accessible = true;
710
+ imageProps.accessibilityLabel = props.alt;
711
+ }
712
+ if (props.testID) imageProps.testID = props.testID;
713
+
714
+ const node: SDUINode = {
715
+ id,
716
+ type: 'image',
717
+ props: {
718
+ ...(Object.keys(style).length ? { style } : {}),
719
+ ...imageProps,
720
+ ...(props.visible !== undefined && props.visible !== true ? { visible: props.visible } : {}),
721
+ },
722
+ ...(props.background === true && children.length ? { children } : {}),
723
+ };
724
+ if (condition !== undefined) node.if = condition;
725
+ if (actions) node.actions = actions;
726
+ return node;
727
+ }
728
+
729
+ function transformInput(
730
+ id: string,
731
+ raw: Record<string, unknown>,
732
+ parentDirection: 'vertical' | 'horizontal',
733
+ ): SDUINode {
734
+ const { actions, condition } = extractMeta(raw);
735
+ const props = stripBridgeKeys(raw);
736
+
737
+ const style: Record<string, unknown> = {};
738
+ resolveSizeModeRN((props.width as SizeModeValue | undefined) ?? 'fill', 'width', style, parentDirection);
739
+ Object.assign(style, resolvePaddingRN(props.padding as SpacingValue | undefined));
740
+ if (props.opacity !== undefined) style.opacity = (props.opacity as number) / 100;
741
+ Object.assign(style, resolvePositionRN(props));
742
+
743
+ const inputTypeDefaults = INPUT_TYPE_MAP[(props.inputType as string | undefined) ?? 'text'] ?? INPUT_TYPE_MAP.text;
744
+
745
+ const node: SDUINode = {
746
+ id,
747
+ type: 'textinput',
748
+ props: {
749
+ ...(Object.keys(style).length ? { style } : {}),
750
+ ...(props.label !== undefined ? { label: props.label } : {}),
751
+ ...(props.placeholder !== undefined ? { placeholder: props.placeholder } : {}),
752
+ ...(props.value !== undefined ? { value: props.value } : {}),
753
+ ...(props.helperText !== undefined ? { helperText: props.helperText } : {}),
754
+ ...(props.errorText !== undefined ? { errorText: props.errorText } : {}),
755
+ ...inputTypeDefaults,
756
+ ...(props.autoCapitalize ? { autoCapitalize: props.autoCapitalize } : {}),
757
+ returnKeyType: (props.returnKey as string | undefined) ?? 'done',
758
+ multiline: (props.multiline as boolean | undefined) ?? false,
759
+ ...(props.multiline && props.minLines ? { numberOfLines: props.minLines } : {}),
760
+ ...(props.multiline && props.maxLines ? { maxNumberOfLines: props.maxLines } : {}),
761
+ variant: (props.variant as string | undefined) ?? 'outline',
762
+ size: (props.size as string | undefined) ?? 'md',
763
+ ...(props.leadingIcon ? { leadingIcon: props.leadingIcon } : {}),
764
+ ...(props.trailingIcon ? { trailingIcon: props.trailingIcon } : {}),
765
+ ...resolveAccessibilityProps(props),
766
+ ...(props.visible !== undefined && props.visible !== true ? { visible: props.visible } : {}),
767
+ },
768
+ };
769
+ if (condition !== undefined) node.if = condition;
770
+ if (actions) node.actions = actions;
771
+ return node;
772
+ }
773
+
774
+ function transformList(
775
+ id: string,
776
+ raw: Record<string, unknown>,
777
+ children: SDUINode[],
778
+ parentDirection: 'vertical' | 'horizontal',
779
+ ): SDUINode {
780
+ const { actions, condition } = extractMeta(raw);
781
+ const props = stripBridgeKeys(raw);
782
+
783
+ const listType = (props.type as string | undefined) ?? 'stack';
784
+ const direction: 'vertical' | 'horizontal' = props.direction === 'horizontal' ? 'horizontal' : 'vertical';
785
+ const edges = safeAreaEdges(props.safeArea as SafeAreaValue | undefined);
786
+ const hasSafeArea = edges.length > 0;
787
+ const vis = props.visible !== undefined && props.visible !== true ? { visible: props.visible } : {};
788
+
789
+ const fullStyle = buildFrameStyle(props, parentDirection);
790
+ const { containerStyle, contentStyle } = splitScrollStyle(fullStyle);
791
+
792
+ // Children of PF.List are the per-item template.
793
+ let itemTemplate: SDUINode | undefined;
794
+ if (children.length === 1) {
795
+ itemTemplate = children[0];
796
+ } else if (children.length > 1) {
797
+ itemTemplate = { id: `${id}__template`, type: 'view', props: {}, children };
798
+ }
799
+
800
+ const flatlistNode: SDUINode = {
801
+ id: hasSafeArea ? `${id}__list` : id,
802
+ type: 'flatlist',
803
+ props: {
804
+ ...(Object.keys(containerStyle).length ? { style: containerStyle } : {}),
805
+ ...(Object.keys(contentStyle).length ? { contentContainerStyle: contentStyle } : {}),
806
+ // `data` holds the binding expression for the list items array.
807
+ // RecursiveRenderer resolves it and passes items to the FlatList primitive.
808
+ data: props.data,
809
+ keyExtractor: (props.keyExtractor as string | undefined) ?? 'id',
810
+ ...(listType === 'grid' ? { numColumns: (props.columns as number | undefined) ?? 2 } : {}),
811
+ ...(listType === 'grid' && props.columnGap ? { columnGap: props.columnGap } : {}),
812
+ ...(listType === 'grid' && props.rowGap ? { rowGap: props.rowGap } : {}),
813
+ horizontal: direction === 'horizontal',
814
+ showsHorizontalScrollIndicator: props.showScrollIndicator === true,
815
+ showsVerticalScrollIndicator: props.showScrollIndicator === true,
816
+ ...resolveAccessibilityProps(props),
817
+ ...(hasSafeArea ? {} : vis),
818
+ },
819
+ ...(itemTemplate ? { itemTemplate } : {}),
820
+ };
821
+
822
+ if (!hasSafeArea) {
823
+ if (condition !== undefined) flatlistNode.if = condition;
824
+ if (actions) flatlistNode.actions = actions;
825
+ return flatlistNode;
826
+ }
827
+
828
+ const { outer: sizingStyle } = splitFrameStyle(fullStyle);
829
+ const savNode: SDUINode = {
830
+ id,
831
+ type: 'safeareaview',
832
+ props: {
833
+ edges,
834
+ ...(Object.keys(sizingStyle).length ? { style: sizingStyle } : {}),
835
+ ...vis,
836
+ },
837
+ children: [flatlistNode],
838
+ };
839
+ if (condition !== undefined) savNode.if = condition;
840
+ if (actions) savNode.actions = actions;
841
+ return savNode;
842
+ }
843
+
844
+ function transformIcon(
845
+ id: string,
846
+ raw: Record<string, unknown>,
847
+ ): SDUINode {
848
+ const { actions, condition } = extractMeta(raw);
849
+ const props = stripBridgeKeys(raw);
850
+
851
+ const style: Record<string, unknown> = {};
852
+ if (props.opacity !== undefined) style.opacity = (props.opacity as number) / 100;
853
+ Object.assign(style, resolvePositionRN(props));
854
+
855
+ const node: SDUINode = {
856
+ id,
857
+ type: 'icon',
858
+ props: {
859
+ name: props.name,
860
+ set: (props.set as string | undefined) ?? 'default',
861
+ size: (props.size as number | undefined) ?? 24,
862
+ ...(props.color ? { color: props.color } : {}),
863
+ ...(Object.keys(style).length ? { style } : {}),
864
+ ...resolveAccessibilityProps(props),
865
+ ...(props.visible !== undefined && props.visible !== true ? { visible: props.visible } : {}),
866
+ },
867
+ };
868
+ if (condition !== undefined) node.if = condition;
869
+ if (actions) node.actions = actions;
870
+ return node;
871
+ }
872
+
873
+ // ─── Core recursive builder ───────────────────────────────────────────────────
874
+
875
+ function buildNodeFromSchema(
876
+ nodeId: string,
877
+ schema: CraftSchema,
878
+ customComponents: CustomComponentsMap,
879
+ visited: Set<string>,
880
+ parentId?: string,
881
+ ): SDUINode {
882
+ if (visited.has(nodeId)) {
883
+ throw new Error(`Circular reference detected at node ${nodeId}`);
884
+ }
885
+ visited.add(nodeId);
886
+
887
+ const node = schema[nodeId];
888
+ if (!node) throw new Error(`Node not found: ${nodeId}`);
889
+
890
+ const resolvedName = node.type?.resolvedName;
891
+ if (!resolvedName) throw new Error(`Invalid node type at ${nodeId}`);
892
+
893
+ const raw = (node.props ?? {}) as Record<string, unknown>;
894
+ const parentDirection: 'vertical' | 'horizontal' = parentId
895
+ ? (schema[parentId]?.props?.['direction'] === 'horizontal' ? 'horizontal' : 'vertical')
896
+ : 'vertical';
897
+ const outputId = nodeId === 'ROOT' ? 'root' : nodeId;
898
+
899
+ const childNodes = (node.nodes ?? []).map(childId =>
900
+ buildNodeFromSchema(childId, schema, customComponents, visited, nodeId),
901
+ );
902
+
903
+ switch (resolvedName) {
904
+ case 'PF.Screen':
905
+ return transformScreen(outputId, raw, childNodes);
906
+
907
+ case 'PF.Frame':
908
+ return transformFrame(outputId, raw, childNodes, parentDirection);
909
+
910
+ case 'PF.Text':
911
+ return transformText(outputId, raw, parentDirection);
912
+
913
+ case 'PF.Image':
914
+ return transformImage(outputId, raw, childNodes, parentDirection);
915
+
916
+ case 'PF.Input':
917
+ return transformInput(outputId, raw, parentDirection);
918
+
919
+ case 'PF.List':
920
+ return transformList(outputId, raw, childNodes, parentDirection);
921
+
922
+ case 'PF.Icon':
923
+ return transformIcon(outputId, raw);
924
+
925
+ case 'Component':
926
+ return resolveCustomComponent(outputId, raw, parentDirection, customComponents);
927
+
928
+ default:
929
+ return { id: outputId, type: 'view', props: {}, children: childNodes };
930
+ }
931
+ }
932
+
933
+ // ─── Custom component inliner ─────────────────────────────────────────────────
934
+
935
+ function resolveCustomComponent(
936
+ id: string,
937
+ raw: Record<string, unknown>,
938
+ parentDirection: 'vertical' | 'horizontal',
939
+ customComponents: CustomComponentsMap,
940
+ ): SDUINode {
941
+ const { actions, condition } = extractMeta(raw);
942
+ const props = stripBridgeKeys(raw);
943
+
944
+ const { componentId, componentKey, propValues, ...frameProps } = props as {
945
+ componentId?: string;
946
+ componentKey?: string;
947
+ propValues?: Record<string, unknown>;
948
+ [k: string]: unknown;
949
+ };
950
+ const key = componentKey ?? componentId;
951
+ const def = key ? customComponents[key] : undefined;
952
+
953
+ const wrapperStyle = buildFrameStyle(frameProps, parentDirection);
954
+ const vis = props.visible !== undefined && props.visible !== true ? { visible: props.visible } : {};
955
+
956
+ if (!def) {
957
+ return {
958
+ id,
959
+ type: 'view',
960
+ props: { ...(Object.keys(wrapperStyle).length ? { style: wrapperStyle } : {}), ...vis },
961
+ children: [],
962
+ };
963
+ }
964
+
965
+ const componentVisited = new Set<string>();
966
+ const componentRoot = buildNodeFromSchema(
967
+ def.rootNodeId,
968
+ def.nodes,
969
+ customComponents,
970
+ componentVisited,
971
+ );
972
+
973
+ if (propValues && Object.keys(propValues).length) {
974
+ componentRoot.props = { ...componentRoot.props, _propValues: propValues };
975
+ }
976
+
977
+ if (Object.keys(wrapperStyle).length > 0) {
978
+ const wrapper: SDUINode = {
979
+ id,
980
+ type: 'view',
981
+ props: { style: wrapperStyle, ...vis },
982
+ children: [componentRoot],
983
+ };
984
+ if (condition !== undefined) wrapper.if = condition;
985
+ if (actions) wrapper.actions = actions;
986
+ return wrapper;
987
+ }
988
+
989
+ componentRoot.id = id;
990
+ componentRoot.props = { ...componentRoot.props, ...vis };
991
+ if (condition !== undefined) componentRoot.if = condition;
992
+ if (actions) componentRoot.actions = actions;
993
+ return componentRoot;
994
+ }
995
+
996
+ // ─── Main entry ───────────────────────────────────────────────────────────────
997
+
998
+ /**
999
+ * Convert a Craft.js editor schema into an SDUINode tree ready for the SDK's
1000
+ * RecursiveRenderer.
1001
+ *
1002
+ * @param craftJson - The full Craft.js node map (keyed by node ID, must include "ROOT").
1003
+ * @param customComponents - Optional map of reusable component definitions.
1004
+ * @returns The root SDUINode.
1005
+ */
1006
+ export function transformCraftToSDUI(
1007
+ craftJson: CraftSchema,
1008
+ customComponents: CustomComponentsMap = {},
1009
+ ): SDUINode {
1010
+ if (!craftJson || typeof craftJson !== 'object') {
1011
+ throw new Error('Invalid craft schema');
1012
+ }
1013
+
1014
+ const visited = new Set<string>();
1015
+ return buildNodeFromSchema('ROOT', craftJson, customComponents, visited);
1016
+ }