react-native-layout-dnd 0.1.0-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (340) hide show
  1. package/LICENSE +20 -0
  2. package/README.md +262 -0
  3. package/lib/module/adapters/coordinates.js +135 -0
  4. package/lib/module/adapters/coordinates.js.map +1 -0
  5. package/lib/module/adapters/scroll.js +81 -0
  6. package/lib/module/adapters/scroll.js.map +1 -0
  7. package/lib/module/adapters/virtualizedList.js +69 -0
  8. package/lib/module/adapters/virtualizedList.js.map +1 -0
  9. package/lib/module/components/DndProvider.js +1144 -0
  10. package/lib/module/components/DndProvider.js.map +1 -0
  11. package/lib/module/components/DragHandle.js +65 -0
  12. package/lib/module/components/DragHandle.js.map +1 -0
  13. package/lib/module/components/GridController.js +151 -0
  14. package/lib/module/components/GridController.js.map +1 -0
  15. package/lib/module/components/GridDragHandle.js +15 -0
  16. package/lib/module/components/GridDragHandle.js.map +1 -0
  17. package/lib/module/components/GridZoneView.js +59 -0
  18. package/lib/module/components/GridZoneView.js.map +1 -0
  19. package/lib/module/components/SortableGrid.js +305 -0
  20. package/lib/module/components/SortableGrid.js.map +1 -0
  21. package/lib/module/components/SortableList.js +714 -0
  22. package/lib/module/components/SortableList.js.map +1 -0
  23. package/lib/module/components/dndTypes.js +4 -0
  24. package/lib/module/components/dndTypes.js.map +1 -0
  25. package/lib/module/components/gridTypes.js +4 -0
  26. package/lib/module/components/gridTypes.js.map +1 -0
  27. package/lib/module/components/handleViewProps.js +13 -0
  28. package/lib/module/components/handleViewProps.js.map +1 -0
  29. package/lib/module/contracts.js +4 -0
  30. package/lib/module/contracts.js.map +1 -0
  31. package/lib/module/controller/approval.js +98 -0
  32. package/lib/module/controller/approval.js.map +1 -0
  33. package/lib/module/controller/coordinatorBase.js +93 -0
  34. package/lib/module/controller/coordinatorBase.js.map +1 -0
  35. package/lib/module/controller/dndSession.js +621 -0
  36. package/lib/module/controller/dndSession.js.map +1 -0
  37. package/lib/module/controller/dndState.js +103 -0
  38. package/lib/module/controller/dndState.js.map +1 -0
  39. package/lib/module/controller/gridAdapter.js +191 -0
  40. package/lib/module/controller/gridAdapter.js.map +1 -0
  41. package/lib/module/controller/gridState.js +101 -0
  42. package/lib/module/controller/gridState.js.map +1 -0
  43. package/lib/module/controller/locations.js +48 -0
  44. package/lib/module/controller/locations.js.map +1 -0
  45. package/lib/module/controller/previewPath.js +73 -0
  46. package/lib/module/controller/previewPath.js.map +1 -0
  47. package/lib/module/controller/sessionCore.js +222 -0
  48. package/lib/module/controller/sessionCore.js.map +1 -0
  49. package/lib/module/controller/useDndState.js +9 -0
  50. package/lib/module/controller/useDndState.js.map +1 -0
  51. package/lib/module/controller/useGridState.js +9 -0
  52. package/lib/module/controller/useGridState.js.map +1 -0
  53. package/lib/module/controller/useStoreState.js +15 -0
  54. package/lib/module/controller/useStoreState.js.map +1 -0
  55. package/lib/module/defaults.js +7 -0
  56. package/lib/module/defaults.js.map +1 -0
  57. package/lib/module/diagnostics.js +16 -0
  58. package/lib/module/diagnostics.js.map +1 -0
  59. package/lib/module/engine/gridPlacements.js +54 -0
  60. package/lib/module/engine/gridPlacements.js.map +1 -0
  61. package/lib/module/engine/index.js +19 -0
  62. package/lib/module/engine/index.js.map +1 -0
  63. package/lib/module/engine/layout.js +36 -0
  64. package/lib/module/engine/layout.js.map +1 -0
  65. package/lib/module/engine/layoutMove.js +326 -0
  66. package/lib/module/engine/layoutMove.js.map +1 -0
  67. package/lib/module/engine/layoutState.js +344 -0
  68. package/lib/module/engine/layoutState.js.map +1 -0
  69. package/lib/module/engine/list.js +172 -0
  70. package/lib/module/engine/list.js.map +1 -0
  71. package/lib/module/engine/listIndex.js +125 -0
  72. package/lib/module/engine/listIndex.js.map +1 -0
  73. package/lib/module/engine/move.js +216 -0
  74. package/lib/module/engine/move.js.map +1 -0
  75. package/lib/module/engine/movementPolicy.js +77 -0
  76. package/lib/module/engine/movementPolicy.js.map +1 -0
  77. package/lib/module/engine/orderedTarget.js +71 -0
  78. package/lib/module/engine/orderedTarget.js.map +1 -0
  79. package/lib/module/engine/pack.js +30 -0
  80. package/lib/module/engine/pack.js.map +1 -0
  81. package/lib/module/engine/rectangles.js +12 -0
  82. package/lib/module/engine/rectangles.js.map +1 -0
  83. package/lib/module/engine/sameLayout.js +29 -0
  84. package/lib/module/engine/sameLayout.js.map +1 -0
  85. package/lib/module/engine/search.js +159 -0
  86. package/lib/module/engine/search.js.map +1 -0
  87. package/lib/module/engine/spatial.js +27 -0
  88. package/lib/module/engine/spatial.js.map +1 -0
  89. package/lib/module/engine/spatialAppRotation.js +39 -0
  90. package/lib/module/engine/spatialAppRotation.js.map +1 -0
  91. package/lib/module/engine/spatialExchange.js +164 -0
  92. package/lib/module/engine/spatialExchange.js.map +1 -0
  93. package/lib/module/engine/spatialInsertion.js +135 -0
  94. package/lib/module/engine/spatialInsertion.js.map +1 -0
  95. package/lib/module/engine/spatialPolicy.js +41 -0
  96. package/lib/module/engine/spatialPolicy.js.map +1 -0
  97. package/lib/module/engine/spatialRotation.js +48 -0
  98. package/lib/module/engine/spatialRotation.js.map +1 -0
  99. package/lib/module/engine/validation.js +205 -0
  100. package/lib/module/engine/validation.js.map +1 -0
  101. package/lib/module/gesture/DndGestureGroup.js +268 -0
  102. package/lib/module/gesture/DndGestureGroup.js.map +1 -0
  103. package/lib/module/gesture/DndGestureHost.js +32 -0
  104. package/lib/module/gesture/DndGestureHost.js.map +1 -0
  105. package/lib/module/gesture/gestureConfig.js +81 -0
  106. package/lib/module/gesture/gestureConfig.js.map +1 -0
  107. package/lib/module/gesture/gestureGrouping.js +122 -0
  108. package/lib/module/gesture/gestureGrouping.js.map +1 -0
  109. package/lib/module/gesture/gestureHost.js +106 -0
  110. package/lib/module/gesture/gestureHost.js.map +1 -0
  111. package/lib/module/index.js +23 -0
  112. package/lib/module/index.js.map +1 -0
  113. package/lib/module/package.json +1 -0
  114. package/lib/module/pointer/dndTarget.js +101 -0
  115. package/lib/module/pointer/dndTarget.js.map +1 -0
  116. package/lib/module/pointer/geometry.js +363 -0
  117. package/lib/module/pointer/geometry.js.map +1 -0
  118. package/lib/module/pointer/layoutTarget.js +307 -0
  119. package/lib/module/pointer/layoutTarget.js.map +1 -0
  120. package/lib/module/pointer/listCellMotion.js +99 -0
  121. package/lib/module/pointer/listCellMotion.js.map +1 -0
  122. package/lib/module/pointer/listTarget.js +42 -0
  123. package/lib/module/pointer/listTarget.js.map +1 -0
  124. package/lib/module/pointer/motion.js +37 -0
  125. package/lib/module/pointer/motion.js.map +1 -0
  126. package/lib/module/runtime/dndPresentation.js +81 -0
  127. package/lib/module/runtime/dndPresentation.js.map +1 -0
  128. package/lib/module/runtime/dndRenderStore.js +129 -0
  129. package/lib/module/runtime/dndRenderStore.js.map +1 -0
  130. package/lib/module/runtime/dndRuntime.js +135 -0
  131. package/lib/module/runtime/dndRuntime.js.map +1 -0
  132. package/lib/module/runtime/dndSettlement.js +77 -0
  133. package/lib/module/runtime/dndSettlement.js.map +1 -0
  134. package/lib/module/runtime/gridRuntime.js +37 -0
  135. package/lib/module/runtime/gridRuntime.js.map +1 -0
  136. package/lib/module/types.js +4 -0
  137. package/lib/module/types.js.map +1 -0
  138. package/lib/typescript/package.json +1 -0
  139. package/lib/typescript/src/adapters/coordinates.d.ts +9 -0
  140. package/lib/typescript/src/adapters/coordinates.d.ts.map +1 -0
  141. package/lib/typescript/src/adapters/scroll.d.ts +49 -0
  142. package/lib/typescript/src/adapters/scroll.d.ts.map +1 -0
  143. package/lib/typescript/src/adapters/virtualizedList.d.ts +22 -0
  144. package/lib/typescript/src/adapters/virtualizedList.d.ts.map +1 -0
  145. package/lib/typescript/src/components/DndProvider.d.ts +4 -0
  146. package/lib/typescript/src/components/DndProvider.d.ts.map +1 -0
  147. package/lib/typescript/src/components/DragHandle.d.ts +4 -0
  148. package/lib/typescript/src/components/DragHandle.d.ts.map +1 -0
  149. package/lib/typescript/src/components/GridController.d.ts +21 -0
  150. package/lib/typescript/src/components/GridController.d.ts.map +1 -0
  151. package/lib/typescript/src/components/GridDragHandle.d.ts +8 -0
  152. package/lib/typescript/src/components/GridDragHandle.d.ts.map +1 -0
  153. package/lib/typescript/src/components/GridZoneView.d.ts +8 -0
  154. package/lib/typescript/src/components/GridZoneView.d.ts.map +1 -0
  155. package/lib/typescript/src/components/SortableGrid.d.ts +4 -0
  156. package/lib/typescript/src/components/SortableGrid.d.ts.map +1 -0
  157. package/lib/typescript/src/components/SortableList.d.ts +4 -0
  158. package/lib/typescript/src/components/SortableList.d.ts.map +1 -0
  159. package/lib/typescript/src/components/dndTypes.d.ts +94 -0
  160. package/lib/typescript/src/components/dndTypes.d.ts.map +1 -0
  161. package/lib/typescript/src/components/gridTypes.d.ts +84 -0
  162. package/lib/typescript/src/components/gridTypes.d.ts.map +1 -0
  163. package/lib/typescript/src/components/handleViewProps.d.ts +6 -0
  164. package/lib/typescript/src/components/handleViewProps.d.ts.map +1 -0
  165. package/lib/typescript/src/contracts.d.ts +145 -0
  166. package/lib/typescript/src/contracts.d.ts.map +1 -0
  167. package/lib/typescript/src/controller/approval.d.ts +66 -0
  168. package/lib/typescript/src/controller/approval.d.ts.map +1 -0
  169. package/lib/typescript/src/controller/coordinatorBase.d.ts +37 -0
  170. package/lib/typescript/src/controller/coordinatorBase.d.ts.map +1 -0
  171. package/lib/typescript/src/controller/dndSession.d.ts +152 -0
  172. package/lib/typescript/src/controller/dndSession.d.ts.map +1 -0
  173. package/lib/typescript/src/controller/dndState.d.ts +27 -0
  174. package/lib/typescript/src/controller/dndState.d.ts.map +1 -0
  175. package/lib/typescript/src/controller/gridAdapter.d.ts +44 -0
  176. package/lib/typescript/src/controller/gridAdapter.d.ts.map +1 -0
  177. package/lib/typescript/src/controller/gridState.d.ts +18 -0
  178. package/lib/typescript/src/controller/gridState.d.ts.map +1 -0
  179. package/lib/typescript/src/controller/locations.d.ts +9 -0
  180. package/lib/typescript/src/controller/locations.d.ts.map +1 -0
  181. package/lib/typescript/src/controller/previewPath.d.ts +49 -0
  182. package/lib/typescript/src/controller/previewPath.d.ts.map +1 -0
  183. package/lib/typescript/src/controller/sessionCore.d.ts +106 -0
  184. package/lib/typescript/src/controller/sessionCore.d.ts.map +1 -0
  185. package/lib/typescript/src/controller/useDndState.d.ts +8 -0
  186. package/lib/typescript/src/controller/useDndState.d.ts.map +1 -0
  187. package/lib/typescript/src/controller/useGridState.d.ts +8 -0
  188. package/lib/typescript/src/controller/useGridState.d.ts.map +1 -0
  189. package/lib/typescript/src/controller/useStoreState.d.ts +16 -0
  190. package/lib/typescript/src/controller/useStoreState.d.ts.map +1 -0
  191. package/lib/typescript/src/defaults.d.ts +5 -0
  192. package/lib/typescript/src/defaults.d.ts.map +1 -0
  193. package/lib/typescript/src/diagnostics.d.ts +9 -0
  194. package/lib/typescript/src/diagnostics.d.ts.map +1 -0
  195. package/lib/typescript/src/engine/gridPlacements.d.ts +22 -0
  196. package/lib/typescript/src/engine/gridPlacements.d.ts.map +1 -0
  197. package/lib/typescript/src/engine/index.d.ts +28 -0
  198. package/lib/typescript/src/engine/index.d.ts.map +1 -0
  199. package/lib/typescript/src/engine/layout.d.ts +4 -0
  200. package/lib/typescript/src/engine/layout.d.ts.map +1 -0
  201. package/lib/typescript/src/engine/layoutMove.d.ts +46 -0
  202. package/lib/typescript/src/engine/layoutMove.d.ts.map +1 -0
  203. package/lib/typescript/src/engine/layoutState.d.ts +22 -0
  204. package/lib/typescript/src/engine/layoutState.d.ts.map +1 -0
  205. package/lib/typescript/src/engine/list.d.ts +68 -0
  206. package/lib/typescript/src/engine/list.d.ts.map +1 -0
  207. package/lib/typescript/src/engine/listIndex.d.ts +32 -0
  208. package/lib/typescript/src/engine/listIndex.d.ts.map +1 -0
  209. package/lib/typescript/src/engine/move.d.ts +10 -0
  210. package/lib/typescript/src/engine/move.d.ts.map +1 -0
  211. package/lib/typescript/src/engine/movementPolicy.d.ts +34 -0
  212. package/lib/typescript/src/engine/movementPolicy.d.ts.map +1 -0
  213. package/lib/typescript/src/engine/orderedTarget.d.ts +32 -0
  214. package/lib/typescript/src/engine/orderedTarget.d.ts.map +1 -0
  215. package/lib/typescript/src/engine/pack.d.ts +3 -0
  216. package/lib/typescript/src/engine/pack.d.ts.map +1 -0
  217. package/lib/typescript/src/engine/rectangles.d.ts +9 -0
  218. package/lib/typescript/src/engine/rectangles.d.ts.map +1 -0
  219. package/lib/typescript/src/engine/sameLayout.d.ts +4 -0
  220. package/lib/typescript/src/engine/sameLayout.d.ts.map +1 -0
  221. package/lib/typescript/src/engine/search.d.ts +28 -0
  222. package/lib/typescript/src/engine/search.d.ts.map +1 -0
  223. package/lib/typescript/src/engine/spatial.d.ts +16 -0
  224. package/lib/typescript/src/engine/spatial.d.ts.map +1 -0
  225. package/lib/typescript/src/engine/spatialAppRotation.d.ts +4 -0
  226. package/lib/typescript/src/engine/spatialAppRotation.d.ts.map +1 -0
  227. package/lib/typescript/src/engine/spatialExchange.d.ts +5 -0
  228. package/lib/typescript/src/engine/spatialExchange.d.ts.map +1 -0
  229. package/lib/typescript/src/engine/spatialInsertion.d.ts +5 -0
  230. package/lib/typescript/src/engine/spatialInsertion.d.ts.map +1 -0
  231. package/lib/typescript/src/engine/spatialPolicy.d.ts +9 -0
  232. package/lib/typescript/src/engine/spatialPolicy.d.ts.map +1 -0
  233. package/lib/typescript/src/engine/spatialRotation.d.ts +8 -0
  234. package/lib/typescript/src/engine/spatialRotation.d.ts.map +1 -0
  235. package/lib/typescript/src/engine/validation.d.ts +9 -0
  236. package/lib/typescript/src/engine/validation.d.ts.map +1 -0
  237. package/lib/typescript/src/gesture/DndGestureGroup.d.ts +33 -0
  238. package/lib/typescript/src/gesture/DndGestureGroup.d.ts.map +1 -0
  239. package/lib/typescript/src/gesture/DndGestureHost.d.ts +12 -0
  240. package/lib/typescript/src/gesture/DndGestureHost.d.ts.map +1 -0
  241. package/lib/typescript/src/gesture/gestureConfig.d.ts +15 -0
  242. package/lib/typescript/src/gesture/gestureConfig.d.ts.map +1 -0
  243. package/lib/typescript/src/gesture/gestureGrouping.d.ts +66 -0
  244. package/lib/typescript/src/gesture/gestureGrouping.d.ts.map +1 -0
  245. package/lib/typescript/src/gesture/gestureHost.d.ts +23 -0
  246. package/lib/typescript/src/gesture/gestureHost.d.ts.map +1 -0
  247. package/lib/typescript/src/index.d.ts +38 -0
  248. package/lib/typescript/src/index.d.ts.map +1 -0
  249. package/lib/typescript/src/pointer/dndTarget.d.ts +14 -0
  250. package/lib/typescript/src/pointer/dndTarget.d.ts.map +1 -0
  251. package/lib/typescript/src/pointer/geometry.d.ts +28 -0
  252. package/lib/typescript/src/pointer/geometry.d.ts.map +1 -0
  253. package/lib/typescript/src/pointer/layoutTarget.d.ts +43 -0
  254. package/lib/typescript/src/pointer/layoutTarget.d.ts.map +1 -0
  255. package/lib/typescript/src/pointer/listCellMotion.d.ts +43 -0
  256. package/lib/typescript/src/pointer/listCellMotion.d.ts.map +1 -0
  257. package/lib/typescript/src/pointer/listTarget.d.ts +7 -0
  258. package/lib/typescript/src/pointer/listTarget.d.ts.map +1 -0
  259. package/lib/typescript/src/pointer/motion.d.ts +20 -0
  260. package/lib/typescript/src/pointer/motion.d.ts.map +1 -0
  261. package/lib/typescript/src/runtime/dndPresentation.d.ts +37 -0
  262. package/lib/typescript/src/runtime/dndPresentation.d.ts.map +1 -0
  263. package/lib/typescript/src/runtime/dndRenderStore.d.ts +46 -0
  264. package/lib/typescript/src/runtime/dndRenderStore.d.ts.map +1 -0
  265. package/lib/typescript/src/runtime/dndRuntime.d.ts +138 -0
  266. package/lib/typescript/src/runtime/dndRuntime.d.ts.map +1 -0
  267. package/lib/typescript/src/runtime/dndSettlement.d.ts +18 -0
  268. package/lib/typescript/src/runtime/dndSettlement.d.ts.map +1 -0
  269. package/lib/typescript/src/runtime/gridRuntime.d.ts +54 -0
  270. package/lib/typescript/src/runtime/gridRuntime.d.ts.map +1 -0
  271. package/lib/typescript/src/types.d.ts +180 -0
  272. package/lib/typescript/src/types.d.ts.map +1 -0
  273. package/package.json +136 -0
  274. package/src/adapters/coordinates.ts +181 -0
  275. package/src/adapters/scroll.ts +147 -0
  276. package/src/adapters/virtualizedList.ts +112 -0
  277. package/src/components/DndProvider.tsx +1836 -0
  278. package/src/components/DragHandle.tsx +89 -0
  279. package/src/components/GridController.tsx +213 -0
  280. package/src/components/GridDragHandle.tsx +11 -0
  281. package/src/components/GridZoneView.tsx +57 -0
  282. package/src/components/SortableGrid.tsx +449 -0
  283. package/src/components/SortableList.tsx +1028 -0
  284. package/src/components/dndTypes.ts +102 -0
  285. package/src/components/gridTypes.ts +90 -0
  286. package/src/components/handleViewProps.ts +35 -0
  287. package/src/contracts.ts +149 -0
  288. package/src/controller/approval.ts +179 -0
  289. package/src/controller/coordinatorBase.ts +118 -0
  290. package/src/controller/dndSession.ts +1058 -0
  291. package/src/controller/dndState.ts +198 -0
  292. package/src/controller/gridAdapter.ts +268 -0
  293. package/src/controller/gridState.ts +187 -0
  294. package/src/controller/locations.ts +67 -0
  295. package/src/controller/previewPath.ts +90 -0
  296. package/src/controller/sessionCore.ts +390 -0
  297. package/src/controller/useDndState.ts +26 -0
  298. package/src/controller/useGridState.ts +22 -0
  299. package/src/controller/useStoreState.ts +27 -0
  300. package/src/defaults.ts +4 -0
  301. package/src/diagnostics.ts +23 -0
  302. package/src/engine/gridPlacements.ts +85 -0
  303. package/src/engine/index.ts +99 -0
  304. package/src/engine/layout.ts +29 -0
  305. package/src/engine/layoutMove.ts +401 -0
  306. package/src/engine/layoutState.ts +490 -0
  307. package/src/engine/list.ts +307 -0
  308. package/src/engine/listIndex.ts +174 -0
  309. package/src/engine/move.ts +274 -0
  310. package/src/engine/movementPolicy.ts +151 -0
  311. package/src/engine/orderedTarget.ts +131 -0
  312. package/src/engine/pack.ts +24 -0
  313. package/src/engine/rectangles.ts +18 -0
  314. package/src/engine/sameLayout.ts +50 -0
  315. package/src/engine/search.ts +174 -0
  316. package/src/engine/spatial.ts +36 -0
  317. package/src/engine/spatialAppRotation.ts +53 -0
  318. package/src/engine/spatialExchange.ts +212 -0
  319. package/src/engine/spatialInsertion.ts +157 -0
  320. package/src/engine/spatialPolicy.ts +48 -0
  321. package/src/engine/spatialRotation.ts +56 -0
  322. package/src/engine/validation.ts +358 -0
  323. package/src/gesture/DndGestureGroup.tsx +357 -0
  324. package/src/gesture/DndGestureHost.tsx +50 -0
  325. package/src/gesture/gestureConfig.ts +123 -0
  326. package/src/gesture/gestureGrouping.ts +210 -0
  327. package/src/gesture/gestureHost.tsx +163 -0
  328. package/src/index.ts +162 -0
  329. package/src/pointer/dndTarget.ts +213 -0
  330. package/src/pointer/geometry.ts +584 -0
  331. package/src/pointer/layoutTarget.ts +515 -0
  332. package/src/pointer/listCellMotion.ts +174 -0
  333. package/src/pointer/listTarget.ts +56 -0
  334. package/src/pointer/motion.ts +66 -0
  335. package/src/runtime/dndPresentation.ts +113 -0
  336. package/src/runtime/dndRenderStore.ts +246 -0
  337. package/src/runtime/dndRuntime.ts +311 -0
  338. package/src/runtime/dndSettlement.ts +117 -0
  339. package/src/runtime/gridRuntime.ts +73 -0
  340. package/src/types.ts +189 -0
@@ -0,0 +1,274 @@
1
+ import {
2
+ diagnosticNow,
3
+ type DiagnosticObserver,
4
+ emitDiagnostic,
5
+ type TimingDiagnostic,
6
+ } from '../diagnostics';
7
+ import { DEFAULT_SEARCH_BUDGET } from '../defaults';
8
+ import type {
9
+ GridItem,
10
+ GridValue,
11
+ GridZone,
12
+ ItemLocation,
13
+ MoveInput,
14
+ MoveResult,
15
+ PositionedItem,
16
+ ValidationIssue,
17
+ } from '../types';
18
+
19
+ import { hasSameLayout } from './sameLayout';
20
+ import { validateGridMovementPolicy } from './movementPolicy';
21
+ import { resolveSpatialMove } from './spatial';
22
+ import {
23
+ isCellPosition,
24
+ isPositiveInteger,
25
+ isRecord,
26
+ validateValue,
27
+ } from './validation';
28
+
29
+ function invalid<T>(
30
+ code: ValidationIssue['code'],
31
+ message: string,
32
+ ids: Pick<ValidationIssue, 'itemId' | 'zoneId'> = {},
33
+ ): MoveResult<T> {
34
+ return { status: 'invalid', issues: [{ code, message, ...ids }] };
35
+ }
36
+
37
+ /**
38
+ * Compute one atomic candidate from the input snapshot. The engine remembers no
39
+ * earlier preview; the session decides whether that snapshot is the committed
40
+ * value or the latest same-zone preview. Drop permission, pointer geometry,
41
+ * sessions and animation belong to the controller.
42
+ */
43
+ export function computeMove<T>(
44
+ input: MoveInput<T>,
45
+ onDiagnostic?: DiagnosticObserver,
46
+ ): MoveResult<T> {
47
+ if (typeof onDiagnostic !== 'function') return calculateMove(input);
48
+ const timings: TimingDiagnostic[] = [];
49
+ const startedAt = diagnosticNow();
50
+ let result: MoveResult<T> | undefined;
51
+ try {
52
+ result = calculateMove(input, timings);
53
+ return result;
54
+ } finally {
55
+ timings.push({
56
+ type: 'timing',
57
+ name: 'compute-move',
58
+ durationMs: diagnosticNow() - startedAt,
59
+ status: result?.status ?? 'error',
60
+ ...(result && 'attempts' in result ? { attempts: result.attempts } : {}),
61
+ });
62
+ // Deliver after the calculation so observer cost is not part of its timing.
63
+ for (const event of timings) emitDiagnostic(onDiagnostic, event);
64
+ }
65
+ }
66
+
67
+ function timedValidation<T>(
68
+ value: GridValue<T>,
69
+ name: 'input-validation' | 'candidate-validation',
70
+ timings: TimingDiagnostic[],
71
+ ) {
72
+ const startedAt = diagnosticNow();
73
+ const result = validateValue(value);
74
+ timings.push({
75
+ type: 'timing',
76
+ name,
77
+ durationMs: diagnosticNow() - startedAt,
78
+ status: result.valid ? 'valid' : 'invalid',
79
+ });
80
+ return result;
81
+ }
82
+
83
+ function calculateMove<T>(
84
+ input: MoveInput<T>,
85
+ timings?: TimingDiagnostic[],
86
+ ): MoveResult<T> {
87
+ if (!isRecord(input))
88
+ return invalid('invalid-structure', 'Expected a move input object.');
89
+ const validation = timings
90
+ ? timedValidation(input.value, 'input-validation', timings)
91
+ : validateValue(input.value);
92
+ if (!validation.valid)
93
+ return { status: 'invalid', issues: validation.issues };
94
+ const policyValidation = validateGridMovementPolicy(input.movementPolicy);
95
+ if (!policyValidation.valid)
96
+ return { status: 'invalid', issues: policyValidation.issues };
97
+ const searchBudget =
98
+ input.searchBudget === undefined
99
+ ? DEFAULT_SEARCH_BUDGET
100
+ : input.searchBudget;
101
+ if (!isPositiveInteger(searchBudget))
102
+ return invalid(
103
+ 'invalid-budget',
104
+ 'searchBudget must be a positive safe integer.',
105
+ );
106
+ if (typeof input.itemId !== 'string')
107
+ return invalid('invalid-structure', 'itemId must be a string.');
108
+ const { value, itemId, to } = input;
109
+ if (!isRecord(to) || typeof to.zoneId !== 'string')
110
+ return invalid('invalid-structure', 'A target zone is required.', {
111
+ itemId,
112
+ });
113
+
114
+ const source = value.zones.find(zone =>
115
+ zone.items.some(item => item.id === itemId),
116
+ );
117
+ const target = value.zones.find(zone => zone.id === to.zoneId);
118
+ if (!source || !target)
119
+ return invalid(
120
+ 'invalid-structure',
121
+ 'The item and target zone must exist.',
122
+ {
123
+ itemId,
124
+ zoneId: to.zoneId,
125
+ },
126
+ );
127
+ if (to.strategy !== target.strategy)
128
+ return invalid(
129
+ 'invalid-structure',
130
+ 'Target strategy must match its zone.',
131
+ {
132
+ itemId,
133
+ zoneId: target.id,
134
+ },
135
+ );
136
+
137
+ const sourceIndex = source.items.findIndex(item => item.id === itemId);
138
+ const active = source.items[sourceIndex];
139
+ const from: ItemLocation =
140
+ source.strategy === 'ordered'
141
+ ? { zoneId: source.id, strategy: 'ordered', index: sourceIndex }
142
+ : {
143
+ zoneId: source.id,
144
+ strategy: 'spatial',
145
+ position: source.items[sourceIndex].position,
146
+ };
147
+ const sameZone = source.id === target.id;
148
+ let nextTarget: GridZone<T>;
149
+ let resolvedTo = to as ItemLocation;
150
+ let attempts = 0;
151
+
152
+ if (target.strategy === 'ordered' && to.strategy === 'ordered') {
153
+ const remaining = target.items.filter(item => item.id !== itemId);
154
+ if (
155
+ !Number.isSafeInteger(to.index) ||
156
+ to.index < 0 ||
157
+ to.index > remaining.length
158
+ )
159
+ return invalid(
160
+ 'invalid-position',
161
+ 'Insertion index must address the array after removal.',
162
+ {
163
+ itemId,
164
+ zoneId: target.id,
165
+ },
166
+ );
167
+ const slotsPerRow = Math.floor(target.columns / target.itemSpan.cols);
168
+ const slotRows = Math.floor(target.rows / target.itemSpan.rows);
169
+ if (
170
+ active.span.rows !== target.itemSpan.rows ||
171
+ active.span.cols !== target.itemSpan.cols ||
172
+ slotsPerRow === 0 ||
173
+ slotRows === 0 ||
174
+ Math.ceil((remaining.length + 1) / slotsPerRow) > slotRows
175
+ )
176
+ return { status: 'impossible', attempts };
177
+ // Spatial position has no meaning in an ordered zone. Preserve opaque data by reference.
178
+ const inserted: GridItem<T> = {
179
+ id: active.id,
180
+ span: active.span,
181
+ ...(active.placement === undefined
182
+ ? {}
183
+ : { placement: active.placement }),
184
+ data: active.data,
185
+ };
186
+ remaining.splice(to.index, 0, inserted);
187
+ nextTarget = { ...target, items: remaining };
188
+ } else if (target.strategy === 'spatial' && to.strategy === 'spatial') {
189
+ if (!isCellPosition(to.position))
190
+ return invalid(
191
+ 'invalid-position',
192
+ 'A spatial target requires safe integer coordinates.',
193
+ {
194
+ itemId,
195
+ zoneId: target.id,
196
+ },
197
+ );
198
+ if (
199
+ to.position.row < 0 ||
200
+ to.position.col < 0 ||
201
+ to.position.row > target.rows - active.span.rows ||
202
+ to.position.col > target.columns - active.span.cols
203
+ )
204
+ return { status: 'impossible', attempts };
205
+ const moved: PositionedItem<T> = {
206
+ id: active.id,
207
+ span: active.span,
208
+ ...(active.placement === undefined
209
+ ? {}
210
+ : { placement: active.placement }),
211
+ data: active.data,
212
+ position: { ...to.position },
213
+ };
214
+ const result = resolveSpatialMove({
215
+ items: target.items.filter(item => item.id !== itemId),
216
+ active: moved,
217
+ origin:
218
+ sameZone && source.strategy === 'spatial'
219
+ ? source.items[sourceIndex]
220
+ : undefined,
221
+ rows: target.rows,
222
+ columns: target.columns,
223
+ searchBudget,
224
+ movementPolicy: input.movementPolicy,
225
+ });
226
+ if (result.status !== 'ok') return result;
227
+ attempts = result.attempts;
228
+ const placements = new Map(result.placements.map(item => [item.id, item]));
229
+ resolvedTo = {
230
+ zoneId: target.id,
231
+ strategy: 'spatial',
232
+ position: placements.get(itemId)!.position,
233
+ };
234
+ const items = target.items.map(item => placements.get(item.id)!);
235
+ if (!sameZone) items.push(placements.get(itemId)!);
236
+ nextTarget = { ...target, items };
237
+ } else {
238
+ return invalid('invalid-structure', 'Unsupported target strategy.', {
239
+ itemId,
240
+ });
241
+ }
242
+
243
+ const zones = value.zones.map(zone => {
244
+ if (zone.id === target.id) return nextTarget;
245
+ if (zone.id !== source.id) return zone;
246
+ const remaining = { ...zone };
247
+ remaining.items = zone.items.filter(item => item.id !== itemId);
248
+ return remaining;
249
+ });
250
+ // Deliberately omit the previous proposalResponse from every candidate.
251
+ const nextValue: GridValue<T> = { zones };
252
+ const candidateValidation = timings
253
+ ? timedValidation(nextValue, 'candidate-validation', timings)
254
+ : validateValue(nextValue);
255
+ if (!candidateValidation.valid)
256
+ return { status: 'invalid', issues: candidateValidation.issues };
257
+ const changed = !hasSameLayout(value, nextValue);
258
+ if (value.revision !== undefined) {
259
+ if (changed && value.revision === Number.MAX_SAFE_INTEGER)
260
+ return invalid(
261
+ 'invalid-configuration',
262
+ 'The layout revision cannot exceed a safe integer.',
263
+ );
264
+ nextValue.revision = value.revision + Number(changed);
265
+ }
266
+ return {
267
+ status: 'ok',
268
+ value: changed ? nextValue : { ...nextValue, zones: value.zones },
269
+ from,
270
+ to: resolvedTo,
271
+ changed,
272
+ attempts,
273
+ };
274
+ }
@@ -0,0 +1,151 @@
1
+ import type { ValidationResult } from '../types';
2
+
3
+ export type GridMoveCandidate = 'row-rotation' | 'insert' | 'exchange';
4
+
5
+ /** Declarative configuration shared by the JS engine and UI worklets. */
6
+ export type GridMovementPolicy = {
7
+ /** Full-width exchange items may rotate every item in their vertical corridor. */
8
+ rowRotation?: 'disabled' | 'full-width';
9
+ /** Home compatibility: a one-cell insert item can pull an adjacent exchange item. */
10
+ adjacentInsertExchange?: 'disabled' | 'single-cell';
11
+ /** First applicable rule wins. Rejection never falls through to another rule. */
12
+ candidateOrder?: readonly GridMoveCandidate[];
13
+ pointer?: {
14
+ /** Extra distance beyond half a cell pitch, in [0, 0.5]. Default 0.12. */
15
+ cellHysteresis?: number;
16
+ /** Cell-pitch fraction required when displacing exchange items, in [0.5, 1]. */
17
+ exchangeThreshold?: number;
18
+ /** Zone entry and ordered-slot margin in React Native logical pixels. */
19
+ boundaryHysteresis?: number;
20
+ };
21
+ };
22
+
23
+ export type ResolvedGridMovementPolicy = {
24
+ rowRotation: NonNullable<GridMovementPolicy['rowRotation']>;
25
+ adjacentInsertExchange: NonNullable<
26
+ GridMovementPolicy['adjacentInsertExchange']
27
+ >;
28
+ candidateOrder: readonly GridMoveCandidate[];
29
+ pointer: Required<NonNullable<GridMovementPolicy['pointer']>>;
30
+ };
31
+
32
+ const DEFAULT_CANDIDATES: readonly GridMoveCandidate[] = Object.freeze([
33
+ 'row-rotation',
34
+ 'insert',
35
+ 'exchange',
36
+ ]);
37
+
38
+ /** Neutral placement rules; caller data and item size never imply home behavior. */
39
+ export const DEFAULT_GRID_MOVEMENT_POLICY: ResolvedGridMovementPolicy =
40
+ Object.freeze({
41
+ rowRotation: 'disabled',
42
+ adjacentInsertExchange: 'disabled',
43
+ candidateOrder: DEFAULT_CANDIDATES,
44
+ pointer: Object.freeze({
45
+ cellHysteresis: 0.12,
46
+ exchangeThreshold: 0.8,
47
+ boundaryHysteresis: 4,
48
+ }),
49
+ });
50
+
51
+ /** Explicit compatibility with the extracted home grid's reversible movements. */
52
+ export const HOME_GRID_MOVEMENT_POLICY: ResolvedGridMovementPolicy =
53
+ Object.freeze({
54
+ ...DEFAULT_GRID_MOVEMENT_POLICY,
55
+ rowRotation: 'full-width',
56
+ adjacentInsertExchange: 'single-cell',
57
+ });
58
+
59
+ /** Copy configuration when starting a session so later caller edits cannot change it. */
60
+ export function resolveGridMovementPolicy(
61
+ policy?: GridMovementPolicy,
62
+ ): ResolvedGridMovementPolicy {
63
+ 'worklet';
64
+ return {
65
+ rowRotation:
66
+ policy?.rowRotation ?? DEFAULT_GRID_MOVEMENT_POLICY.rowRotation,
67
+ adjacentInsertExchange:
68
+ policy?.adjacentInsertExchange ??
69
+ DEFAULT_GRID_MOVEMENT_POLICY.adjacentInsertExchange,
70
+ candidateOrder: [
71
+ ...(policy?.candidateOrder ??
72
+ DEFAULT_GRID_MOVEMENT_POLICY.candidateOrder),
73
+ ],
74
+ pointer: {
75
+ cellHysteresis:
76
+ policy?.pointer?.cellHysteresis ??
77
+ DEFAULT_GRID_MOVEMENT_POLICY.pointer.cellHysteresis,
78
+ exchangeThreshold:
79
+ policy?.pointer?.exchangeThreshold ??
80
+ DEFAULT_GRID_MOVEMENT_POLICY.pointer.exchangeThreshold,
81
+ boundaryHysteresis:
82
+ policy?.pointer?.boundaryHysteresis ??
83
+ DEFAULT_GRID_MOVEMENT_POLICY.pointer.boundaryHysteresis,
84
+ },
85
+ };
86
+ }
87
+
88
+ function record(value: unknown): value is Record<string, unknown> {
89
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
90
+ }
91
+
92
+ /** Invalid policies reject the complete move before any candidate is computed. */
93
+ export function validateGridMovementPolicy(policy: unknown): ValidationResult {
94
+ if (policy === undefined) return { valid: true };
95
+ const invalid = (message: string): ValidationResult => ({
96
+ valid: false,
97
+ issues: [{ code: 'invalid-configuration', message }],
98
+ });
99
+ if (!record(policy)) return invalid('movementPolicy must be an object.');
100
+ if (
101
+ policy.rowRotation !== undefined &&
102
+ policy.rowRotation !== 'disabled' &&
103
+ policy.rowRotation !== 'full-width'
104
+ )
105
+ return invalid(
106
+ 'movementPolicy.rowRotation must be disabled or full-width.',
107
+ );
108
+ if (
109
+ policy.adjacentInsertExchange !== undefined &&
110
+ policy.adjacentInsertExchange !== 'disabled' &&
111
+ policy.adjacentInsertExchange !== 'single-cell'
112
+ )
113
+ return invalid(
114
+ 'movementPolicy.adjacentInsertExchange must be disabled or single-cell.',
115
+ );
116
+ if (
117
+ policy.candidateOrder !== undefined &&
118
+ (!Array.isArray(policy.candidateOrder) ||
119
+ policy.candidateOrder.length === 0 ||
120
+ Array.from(policy.candidateOrder).some(
121
+ candidate => !DEFAULT_CANDIDATES.includes(candidate),
122
+ ) ||
123
+ new Set(policy.candidateOrder).size !== policy.candidateOrder.length)
124
+ )
125
+ return invalid(
126
+ 'movementPolicy.candidateOrder must contain distinct supported candidates.',
127
+ );
128
+ if (policy.pointer !== undefined) {
129
+ if (!record(policy.pointer))
130
+ return invalid('movementPolicy.pointer must be an object.');
131
+ const limits = {
132
+ cellHysteresis: [0, 0.5],
133
+ exchangeThreshold: [0.5, 1],
134
+ boundaryHysteresis: [0, Infinity],
135
+ };
136
+ for (const [key, [min, max]] of Object.entries(limits)) {
137
+ const value = policy.pointer[key];
138
+ if (
139
+ value !== undefined &&
140
+ (typeof value !== 'number' ||
141
+ !Number.isFinite(value) ||
142
+ value < min ||
143
+ value > max)
144
+ )
145
+ return invalid(
146
+ `movementPolicy.pointer.${key} must be finite and between ${min} and ${max}.`,
147
+ );
148
+ }
149
+ }
150
+ return { valid: true };
151
+ }
@@ -0,0 +1,131 @@
1
+ import type { CellSpan, ValidationIssue } from '../types';
2
+
3
+ import { isCellSpan, isPositiveInteger, isRecord } from './validation';
4
+
5
+ export type OrderedIndexInput = {
6
+ rows: number;
7
+ columns: number;
8
+ itemSpan: CellSpan;
9
+ /** Target item count after removing the active item for a same-zone move. */
10
+ itemCount: number;
11
+ /** Moving item's center, relative to the target content origin (without padding). */
12
+ center: { x: number; y: number };
13
+ cellWidth: number;
14
+ cellHeight: number;
15
+ rowGap: number;
16
+ columnGap: number;
17
+ };
18
+ export type OrderedIndexResult =
19
+ | { status: 'ok'; index: number }
20
+ | { status: 'impossible' }
21
+ | { status: 'invalid'; issues: ValidationIssue[] };
22
+
23
+ /**
24
+ * Raw ordered boundary. The caller first hit-tests the pointer against actual zones,
25
+ * removes the active item, and then applies the same hysteresis for preview and drop.
26
+ */
27
+ export function computeOrderedIndex(
28
+ input: OrderedIndexInput,
29
+ ): OrderedIndexResult {
30
+ const fail = (
31
+ code: ValidationIssue['code'],
32
+ message: string,
33
+ ): OrderedIndexResult => ({
34
+ status: 'invalid',
35
+ issues: [{ code, message }],
36
+ });
37
+ if (!isRecord(input))
38
+ return fail('invalid-structure', 'Expected ordered target geometry.');
39
+ if (!isPositiveInteger(input.rows) || !isPositiveInteger(input.columns))
40
+ return fail(
41
+ 'invalid-dimension',
42
+ 'Grid dimensions must be positive safe integers.',
43
+ );
44
+ if (input.itemSpan === undefined)
45
+ return fail('missing-item-span', 'Ordered targeting requires itemSpan.');
46
+ if (!isCellSpan(input.itemSpan))
47
+ return fail(
48
+ 'invalid-span',
49
+ 'itemSpan must contain positive safe integers.',
50
+ );
51
+ if (!Number.isSafeInteger(input.itemCount) || input.itemCount < 0)
52
+ return fail(
53
+ 'invalid-structure',
54
+ 'itemCount must be a non-negative safe integer.',
55
+ );
56
+ if (
57
+ !isRecord(input.center) ||
58
+ !Number.isFinite(input.center.x) ||
59
+ !Number.isFinite(input.center.y) ||
60
+ !Number.isFinite(input.cellWidth) ||
61
+ input.cellWidth <= 0 ||
62
+ !Number.isFinite(input.cellHeight) ||
63
+ input.cellHeight <= 0 ||
64
+ !Number.isFinite(input.rowGap) ||
65
+ input.rowGap < 0 ||
66
+ !Number.isFinite(input.columnGap) ||
67
+ input.columnGap < 0
68
+ )
69
+ return fail(
70
+ 'invalid-geometry',
71
+ 'Center, cell sizes and gaps must be finite valid geometry.',
72
+ );
73
+
74
+ const {
75
+ rows,
76
+ columns,
77
+ itemSpan,
78
+ itemCount,
79
+ cellWidth,
80
+ cellHeight,
81
+ rowGap,
82
+ columnGap,
83
+ } = input;
84
+ const slotsPerRow = Math.floor(columns / itemSpan.cols);
85
+ const slotRows = Math.floor(rows / itemSpan.rows);
86
+ const width = columns * cellWidth + (columns - 1) * columnGap;
87
+ const height = rows * cellHeight + (rows - 1) * rowGap;
88
+ const slotWidth = itemSpan.cols * cellWidth + (itemSpan.cols - 1) * columnGap;
89
+ const slotHeight = itemSpan.rows * cellHeight + (itemSpan.rows - 1) * rowGap;
90
+ const strideX = slotWidth + columnGap;
91
+ const strideY = slotHeight + rowGap;
92
+ if (
93
+ ![width, height, slotWidth, slotHeight, strideX, strideY].every(
94
+ n => Number.isFinite(n) && n > 0,
95
+ )
96
+ )
97
+ return fail(
98
+ 'invalid-geometry',
99
+ 'Computed content and slot dimensions must be finite and positive.',
100
+ );
101
+ if (!slotsPerRow || !slotRows) return { status: 'impossible' };
102
+ if (Math.ceil(itemCount / slotsPerRow) > slotRows)
103
+ return fail('capacity-exceeded', 'itemCount exceeds the ordered capacity.');
104
+ const x = Math.max(0, Math.min(width, input.center.x));
105
+ const y = Math.max(0, Math.min(height, input.center.y));
106
+
107
+ // Unfillable bottom rows are after the whole array, including a partly filled last row.
108
+ if (rows % itemSpan.rows !== 0 && y >= slotRows * strideY - rowGap)
109
+ return { status: 'ok', index: itemCount };
110
+ const row = Math.max(
111
+ 0,
112
+ Math.min(slotRows - 1, Math.floor((y - slotHeight / 2) / strideY + 0.5)),
113
+ );
114
+ // Right-hand remainder belongs to this row's end, not the whole array's end.
115
+ if (columns % itemSpan.cols !== 0 && x >= slotsPerRow * strideX - columnGap)
116
+ return {
117
+ status: 'ok',
118
+ index: Math.min(itemCount, (row + 1) * slotsPerRow),
119
+ };
120
+ const col = Math.max(
121
+ 0,
122
+ Math.min(slotsPerRow - 1, Math.floor((x - slotWidth / 2) / strideX + 0.5)),
123
+ );
124
+ const slot = row * slotsPerRow + col;
125
+ if (slot >= itemCount) return { status: 'ok', index: itemCount };
126
+ const after =
127
+ slotsPerRow === 1
128
+ ? y >= row * strideY + slotHeight / 2
129
+ : x >= col * strideX + slotWidth / 2;
130
+ return { status: 'ok', index: Math.min(itemCount, slot + Number(after)) };
131
+ }
@@ -0,0 +1,24 @@
1
+ import type { PackInput, PackResult } from '../types';
2
+
3
+ import { createPlacementSearch } from './search';
4
+ import { validatePackInput } from './validation';
5
+
6
+ export function packItems<T>(input: PackInput<T>): PackResult<T> {
7
+ const validation = validatePackInput(input);
8
+ if (!validation.valid) {
9
+ return { status: 'invalid', issues: validation.issues };
10
+ }
11
+ const result = createPlacementSearch({
12
+ items: input.items,
13
+ rows: input.rows,
14
+ columns: input.columns,
15
+ searchBudget: input.searchBudget,
16
+ }).advance(input.searchBudget);
17
+ if (result.status === 'ok') {
18
+ return { status: 'ok', placements: result.placements };
19
+ }
20
+ // A chunk containing the entire global budget always resolves its status.
21
+ return {
22
+ status: result.status === 'impossible' ? 'impossible' : 'unresolved',
23
+ };
24
+ }
@@ -0,0 +1,18 @@
1
+ import type { CellPosition, CellSpan } from '../types';
2
+
3
+ type CellRectangle = { position: CellPosition; span: CellSpan };
4
+
5
+ /** Internal: rectangles have validated nonnegative anchors and positive spans. */
6
+ export function rectanglesOverlap(a: CellRectangle, b: CellRectangle): boolean {
7
+ 'worklet';
8
+ // Anchor differences stay exact without adding a span to a large coordinate.
9
+ const rowsOverlap =
10
+ a.position.row <= b.position.row
11
+ ? b.position.row - a.position.row < a.span.rows
12
+ : a.position.row - b.position.row < b.span.rows;
13
+ const colsOverlap =
14
+ a.position.col <= b.position.col
15
+ ? b.position.col - a.position.col < a.span.cols
16
+ : a.position.col - b.position.col < b.span.cols;
17
+ return rowsOverlap && colsOverlap;
18
+ }
@@ -0,0 +1,50 @@
1
+ import type { GridValue, GridZone } from '../types';
2
+
3
+ function sameZone<T>(a: GridZone<T>, b: GridZone<T>): boolean {
4
+ if (
5
+ a.strategy !== b.strategy ||
6
+ a.rows !== b.rows ||
7
+ a.columns !== b.columns ||
8
+ a.items.length !== b.items.length
9
+ )
10
+ return false;
11
+ if (a.strategy === 'ordered' && b.strategy === 'ordered') {
12
+ return (
13
+ a.itemSpan.rows === b.itemSpan.rows &&
14
+ a.itemSpan.cols === b.itemSpan.cols &&
15
+ a.items.every((item, index) => {
16
+ const other = b.items[index];
17
+ return (
18
+ item.id === other.id &&
19
+ (item.placement ?? 'exchange') === (other.placement ?? 'exchange') &&
20
+ item.span.rows === other.span.rows &&
21
+ item.span.cols === other.span.cols
22
+ );
23
+ })
24
+ );
25
+ }
26
+ if (a.strategy !== 'spatial' || b.strategy !== 'spatial') return false;
27
+ const byId = new Map(b.items.map(item => [item.id, item]));
28
+ return a.items.every(item => {
29
+ const other = byId.get(item.id);
30
+ return (
31
+ other !== undefined &&
32
+ (item.placement ?? 'exchange') === (other.placement ?? 'exchange') &&
33
+ item.span.rows === other.span.rows &&
34
+ item.span.cols === other.span.cols &&
35
+ item.position.row === other.position.row &&
36
+ item.position.col === other.position.col
37
+ );
38
+ });
39
+ }
40
+
41
+ /** Compare validated layouts. Data, responses and spatial array order are not layout. */
42
+ export function hasSameLayout<T>(a: GridValue<T>, b: GridValue<T>): boolean {
43
+ if (a === b) return true;
44
+ if (a.zones.length !== b.zones.length) return false;
45
+ const byId = new Map(b.zones.map(zone => [zone.id, zone]));
46
+ return a.zones.every(zone => {
47
+ const other = byId.get(zone.id);
48
+ return other !== undefined && sameZone(zone, other);
49
+ });
50
+ }