@oxyhq/services 22.10.0 → 22.11.1

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 (274) hide show
  1. package/lib/commonjs/ui/components/AvatarCameraBadge.js +42 -0
  2. package/lib/commonjs/ui/components/AvatarCameraBadge.js.map +1 -0
  3. package/lib/commonjs/ui/components/OxyAccountDialogScreen.js +41 -19
  4. package/lib/commonjs/ui/components/OxyAccountDialogScreen.js.map +1 -1
  5. package/lib/commonjs/ui/components/OxyAuthChooser.js +640 -168
  6. package/lib/commonjs/ui/components/OxyAuthChooser.js.map +1 -1
  7. package/lib/commonjs/ui/components/ProfileButton.js +14 -0
  8. package/lib/commonjs/ui/components/ProfileButton.js.map +1 -1
  9. package/lib/commonjs/ui/components/ProfileSummaryCard.js +3 -23
  10. package/lib/commonjs/ui/components/ProfileSummaryCard.js.map +1 -1
  11. package/lib/commonjs/ui/components/SurfaceScreen.js +41 -5
  12. package/lib/commonjs/ui/components/SurfaceScreen.js.map +1 -1
  13. package/lib/commonjs/ui/components/oxyAuthChooserStyles.js +78 -26
  14. package/lib/commonjs/ui/components/oxyAuthChooserStyles.js.map +1 -1
  15. package/lib/commonjs/ui/components/photogrid/JustifiedPhotoGrid.js +83 -23
  16. package/lib/commonjs/ui/components/photogrid/JustifiedPhotoGrid.js.map +1 -1
  17. package/lib/commonjs/ui/context/OxyContext.js +73 -37
  18. package/lib/commonjs/ui/context/OxyContext.js.map +1 -1
  19. package/lib/commonjs/ui/hooks/queries/queryKeys.js +18 -1
  20. package/lib/commonjs/ui/hooks/queries/queryKeys.js.map +1 -1
  21. package/lib/commonjs/ui/hooks/queries/useFileQueries.js +129 -0
  22. package/lib/commonjs/ui/hooks/queries/useFileQueries.js.map +1 -0
  23. package/lib/commonjs/ui/hooks/useAvatarPicker.js +36 -48
  24. package/lib/commonjs/ui/hooks/useAvatarPicker.js.map +1 -1
  25. package/lib/commonjs/ui/hooks/useReduceMotion.js +57 -0
  26. package/lib/commonjs/ui/hooks/useReduceMotion.js.map +1 -0
  27. package/lib/commonjs/ui/hooks/useSurfaceHeader.js +1 -1
  28. package/lib/commonjs/ui/hooks/useSurfaceHeader.js.map +1 -1
  29. package/lib/commonjs/ui/navigation/accountDialogManager.js +22 -0
  30. package/lib/commonjs/ui/navigation/accountDialogManager.js.map +1 -1
  31. package/lib/commonjs/ui/navigation/bottomSheetManager.js +5 -6
  32. package/lib/commonjs/ui/navigation/bottomSheetManager.js.map +1 -1
  33. package/lib/commonjs/ui/navigation/routes.js +1 -0
  34. package/lib/commonjs/ui/navigation/routes.js.map +1 -1
  35. package/lib/commonjs/ui/navigation/surfaceNavStack.js +62 -15
  36. package/lib/commonjs/ui/navigation/surfaceNavStack.js.map +1 -1
  37. package/lib/commonjs/ui/navigation/surfaceRegistry.js +38 -17
  38. package/lib/commonjs/ui/navigation/surfaceRegistry.js.map +1 -1
  39. package/lib/commonjs/ui/navigation/surfaces.js +98 -21
  40. package/lib/commonjs/ui/navigation/surfaces.js.map +1 -1
  41. package/lib/commonjs/ui/screens/AvatarCropScreen.js +281 -279
  42. package/lib/commonjs/ui/screens/AvatarCropScreen.js.map +1 -1
  43. package/lib/commonjs/ui/screens/ChangeAvatarScreen.js +301 -0
  44. package/lib/commonjs/ui/screens/ChangeAvatarScreen.js.map +1 -0
  45. package/lib/commonjs/ui/screens/FileManagementScreen.js +194 -431
  46. package/lib/commonjs/ui/screens/FileManagementScreen.js.map +1 -1
  47. package/lib/commonjs/ui/screens/WelcomeNewUserScreen.js +16 -48
  48. package/lib/commonjs/ui/screens/WelcomeNewUserScreen.js.map +1 -1
  49. package/lib/commonjs/ui/screens/fileManagement/FileLibraryError.js +63 -0
  50. package/lib/commonjs/ui/screens/fileManagement/FileLibraryError.js.map +1 -0
  51. package/lib/commonjs/ui/screens/fileManagement/PhotoPickerSection.js +71 -39
  52. package/lib/commonjs/ui/screens/fileManagement/PhotoPickerSection.js.map +1 -1
  53. package/lib/commonjs/ui/screens/fileManagement/hooks/useFileUploadState.js +44 -81
  54. package/lib/commonjs/ui/screens/fileManagement/hooks/useFileUploadState.js.map +1 -1
  55. package/lib/commonjs/ui/stores/resetSessionScopedStores.js +8 -3
  56. package/lib/commonjs/ui/stores/resetSessionScopedStores.js.map +1 -1
  57. package/lib/commonjs/ui/utils/avatarUtils.js +11 -2
  58. package/lib/commonjs/ui/utils/avatarUtils.js.map +1 -1
  59. package/lib/commonjs/ui/utils/fileManagement.js +0 -79
  60. package/lib/commonjs/ui/utils/fileManagement.js.map +1 -1
  61. package/lib/module/ui/components/AvatarCameraBadge.js +40 -0
  62. package/lib/module/ui/components/AvatarCameraBadge.js.map +1 -0
  63. package/lib/module/ui/components/OxyAccountDialogScreen.js +41 -19
  64. package/lib/module/ui/components/OxyAccountDialogScreen.js.map +1 -1
  65. package/lib/module/ui/components/OxyAuthChooser.js +642 -171
  66. package/lib/module/ui/components/OxyAuthChooser.js.map +1 -1
  67. package/lib/module/ui/components/ProfileButton.js +15 -1
  68. package/lib/module/ui/components/ProfileButton.js.map +1 -1
  69. package/lib/module/ui/components/ProfileSummaryCard.js +2 -23
  70. package/lib/module/ui/components/ProfileSummaryCard.js.map +1 -1
  71. package/lib/module/ui/components/SurfaceScreen.js +43 -7
  72. package/lib/module/ui/components/SurfaceScreen.js.map +1 -1
  73. package/lib/module/ui/components/oxyAuthChooserStyles.js +78 -26
  74. package/lib/module/ui/components/oxyAuthChooserStyles.js.map +1 -1
  75. package/lib/module/ui/components/photogrid/JustifiedPhotoGrid.js +83 -25
  76. package/lib/module/ui/components/photogrid/JustifiedPhotoGrid.js.map +1 -1
  77. package/lib/module/ui/context/OxyContext.js +74 -38
  78. package/lib/module/ui/context/OxyContext.js.map +1 -1
  79. package/lib/module/ui/hooks/queries/queryKeys.js +16 -0
  80. package/lib/module/ui/hooks/queries/queryKeys.js.map +1 -1
  81. package/lib/module/ui/hooks/queries/useFileQueries.js +120 -0
  82. package/lib/module/ui/hooks/queries/useFileQueries.js.map +1 -0
  83. package/lib/module/ui/hooks/useAvatarPicker.js +37 -49
  84. package/lib/module/ui/hooks/useAvatarPicker.js.map +1 -1
  85. package/lib/module/ui/hooks/useReduceMotion.js +53 -0
  86. package/lib/module/ui/hooks/useReduceMotion.js.map +1 -0
  87. package/lib/module/ui/hooks/useSurfaceHeader.js +1 -1
  88. package/lib/module/ui/hooks/useSurfaceHeader.js.map +1 -1
  89. package/lib/module/ui/navigation/accountDialogManager.js +20 -0
  90. package/lib/module/ui/navigation/accountDialogManager.js.map +1 -1
  91. package/lib/module/ui/navigation/bottomSheetManager.js +6 -7
  92. package/lib/module/ui/navigation/bottomSheetManager.js.map +1 -1
  93. package/lib/module/ui/navigation/routes.js +1 -0
  94. package/lib/module/ui/navigation/routes.js.map +1 -1
  95. package/lib/module/ui/navigation/surfaceNavStack.js +62 -15
  96. package/lib/module/ui/navigation/surfaceNavStack.js.map +1 -1
  97. package/lib/module/ui/navigation/surfaceRegistry.js +38 -17
  98. package/lib/module/ui/navigation/surfaceRegistry.js.map +1 -1
  99. package/lib/module/ui/navigation/surfaces.js +97 -22
  100. package/lib/module/ui/navigation/surfaces.js.map +1 -1
  101. package/lib/module/ui/screens/AvatarCropScreen.js +283 -281
  102. package/lib/module/ui/screens/AvatarCropScreen.js.map +1 -1
  103. package/lib/module/ui/screens/ChangeAvatarScreen.js +298 -0
  104. package/lib/module/ui/screens/ChangeAvatarScreen.js.map +1 -0
  105. package/lib/module/ui/screens/FileManagementScreen.js +196 -433
  106. package/lib/module/ui/screens/FileManagementScreen.js.map +1 -1
  107. package/lib/module/ui/screens/WelcomeNewUserScreen.js +17 -49
  108. package/lib/module/ui/screens/WelcomeNewUserScreen.js.map +1 -1
  109. package/lib/module/ui/screens/fileManagement/FileLibraryError.js +59 -0
  110. package/lib/module/ui/screens/fileManagement/FileLibraryError.js.map +1 -0
  111. package/lib/module/ui/screens/fileManagement/PhotoPickerSection.js +71 -40
  112. package/lib/module/ui/screens/fileManagement/PhotoPickerSection.js.map +1 -1
  113. package/lib/module/ui/screens/fileManagement/hooks/useFileUploadState.js +45 -81
  114. package/lib/module/ui/screens/fileManagement/hooks/useFileUploadState.js.map +1 -1
  115. package/lib/module/ui/stores/resetSessionScopedStores.js +8 -3
  116. package/lib/module/ui/stores/resetSessionScopedStores.js.map +1 -1
  117. package/lib/module/ui/utils/avatarUtils.js +11 -2
  118. package/lib/module/ui/utils/avatarUtils.js.map +1 -1
  119. package/lib/module/ui/utils/fileManagement.js +0 -78
  120. package/lib/module/ui/utils/fileManagement.js.map +1 -1
  121. package/lib/typescript/commonjs/ui/components/AvatarCameraBadge.d.ts +4 -0
  122. package/lib/typescript/commonjs/ui/components/AvatarCameraBadge.d.ts.map +1 -0
  123. package/lib/typescript/commonjs/ui/components/OxyAccountDialogScreen.d.ts +2 -1
  124. package/lib/typescript/commonjs/ui/components/OxyAccountDialogScreen.d.ts.map +1 -1
  125. package/lib/typescript/commonjs/ui/components/OxyAuthChooser.d.ts.map +1 -1
  126. package/lib/typescript/commonjs/ui/components/ProfileButton.d.ts.map +1 -1
  127. package/lib/typescript/commonjs/ui/components/ProfileSummaryCard.d.ts.map +1 -1
  128. package/lib/typescript/commonjs/ui/components/SurfaceScreen.d.ts +1 -4
  129. package/lib/typescript/commonjs/ui/components/SurfaceScreen.d.ts.map +1 -1
  130. package/lib/typescript/commonjs/ui/components/oxyAuthChooserStyles.d.ts +71 -19
  131. package/lib/typescript/commonjs/ui/components/oxyAuthChooserStyles.d.ts.map +1 -1
  132. package/lib/typescript/commonjs/ui/components/photogrid/JustifiedPhotoGrid.d.ts +10 -8
  133. package/lib/typescript/commonjs/ui/components/photogrid/JustifiedPhotoGrid.d.ts.map +1 -1
  134. package/lib/typescript/commonjs/ui/context/OxyContext.d.ts.map +1 -1
  135. package/lib/typescript/commonjs/ui/hooks/queries/queryKeys.d.ts +9 -0
  136. package/lib/typescript/commonjs/ui/hooks/queries/queryKeys.d.ts.map +1 -1
  137. package/lib/typescript/commonjs/ui/hooks/queries/useFileQueries.d.ts +50 -0
  138. package/lib/typescript/commonjs/ui/hooks/queries/useFileQueries.d.ts.map +1 -0
  139. package/lib/typescript/commonjs/ui/hooks/useAvatarPicker.d.ts +15 -10
  140. package/lib/typescript/commonjs/ui/hooks/useAvatarPicker.d.ts.map +1 -1
  141. package/lib/typescript/commonjs/ui/hooks/useReduceMotion.d.ts +14 -0
  142. package/lib/typescript/commonjs/ui/hooks/useReduceMotion.d.ts.map +1 -0
  143. package/lib/typescript/commonjs/ui/hooks/useSurfaceHeader.d.ts +1 -1
  144. package/lib/typescript/commonjs/ui/hooks/useSurfaceHeader.d.ts.map +1 -1
  145. package/lib/typescript/commonjs/ui/navigation/accountDialogManager.d.ts +13 -0
  146. package/lib/typescript/commonjs/ui/navigation/accountDialogManager.d.ts.map +1 -1
  147. package/lib/typescript/commonjs/ui/navigation/bottomSheetManager.d.ts +4 -5
  148. package/lib/typescript/commonjs/ui/navigation/bottomSheetManager.d.ts.map +1 -1
  149. package/lib/typescript/commonjs/ui/navigation/routes.d.ts +1 -1
  150. package/lib/typescript/commonjs/ui/navigation/routes.d.ts.map +1 -1
  151. package/lib/typescript/commonjs/ui/navigation/surfaceNavStack.d.ts +21 -0
  152. package/lib/typescript/commonjs/ui/navigation/surfaceNavStack.d.ts.map +1 -1
  153. package/lib/typescript/commonjs/ui/navigation/surfaceRegistry.d.ts +73 -13
  154. package/lib/typescript/commonjs/ui/navigation/surfaceRegistry.d.ts.map +1 -1
  155. package/lib/typescript/commonjs/ui/navigation/surfaces.d.ts +42 -11
  156. package/lib/typescript/commonjs/ui/navigation/surfaces.d.ts.map +1 -1
  157. package/lib/typescript/commonjs/ui/screens/AvatarCropScreen.d.ts +18 -5
  158. package/lib/typescript/commonjs/ui/screens/AvatarCropScreen.d.ts.map +1 -1
  159. package/lib/typescript/commonjs/ui/screens/ChangeAvatarScreen.d.ts +34 -0
  160. package/lib/typescript/commonjs/ui/screens/ChangeAvatarScreen.d.ts.map +1 -0
  161. package/lib/typescript/commonjs/ui/screens/FileManagementScreen.d.ts.map +1 -1
  162. package/lib/typescript/commonjs/ui/screens/WelcomeNewUserScreen.d.ts.map +1 -1
  163. package/lib/typescript/commonjs/ui/screens/fileManagement/FileLibraryError.d.ts +23 -0
  164. package/lib/typescript/commonjs/ui/screens/fileManagement/FileLibraryError.d.ts.map +1 -0
  165. package/lib/typescript/commonjs/ui/screens/fileManagement/PhotoPickerSection.d.ts +4 -0
  166. package/lib/typescript/commonjs/ui/screens/fileManagement/PhotoPickerSection.d.ts.map +1 -1
  167. package/lib/typescript/commonjs/ui/screens/fileManagement/hooks/useFileUploadState.d.ts +10 -2
  168. package/lib/typescript/commonjs/ui/screens/fileManagement/hooks/useFileUploadState.d.ts.map +1 -1
  169. package/lib/typescript/commonjs/ui/stores/resetSessionScopedStores.d.ts +8 -1
  170. package/lib/typescript/commonjs/ui/stores/resetSessionScopedStores.d.ts.map +1 -1
  171. package/lib/typescript/commonjs/ui/types/navigation.d.ts +2 -0
  172. package/lib/typescript/commonjs/ui/types/navigation.d.ts.map +1 -1
  173. package/lib/typescript/commonjs/ui/utils/avatarUtils.d.ts.map +1 -1
  174. package/lib/typescript/commonjs/ui/utils/fileManagement.d.ts +0 -48
  175. package/lib/typescript/commonjs/ui/utils/fileManagement.d.ts.map +1 -1
  176. package/lib/typescript/module/ui/components/AvatarCameraBadge.d.ts +4 -0
  177. package/lib/typescript/module/ui/components/AvatarCameraBadge.d.ts.map +1 -0
  178. package/lib/typescript/module/ui/components/OxyAccountDialogScreen.d.ts +2 -1
  179. package/lib/typescript/module/ui/components/OxyAccountDialogScreen.d.ts.map +1 -1
  180. package/lib/typescript/module/ui/components/OxyAuthChooser.d.ts.map +1 -1
  181. package/lib/typescript/module/ui/components/ProfileButton.d.ts.map +1 -1
  182. package/lib/typescript/module/ui/components/ProfileSummaryCard.d.ts.map +1 -1
  183. package/lib/typescript/module/ui/components/SurfaceScreen.d.ts +1 -4
  184. package/lib/typescript/module/ui/components/SurfaceScreen.d.ts.map +1 -1
  185. package/lib/typescript/module/ui/components/oxyAuthChooserStyles.d.ts +71 -19
  186. package/lib/typescript/module/ui/components/oxyAuthChooserStyles.d.ts.map +1 -1
  187. package/lib/typescript/module/ui/components/photogrid/JustifiedPhotoGrid.d.ts +10 -8
  188. package/lib/typescript/module/ui/components/photogrid/JustifiedPhotoGrid.d.ts.map +1 -1
  189. package/lib/typescript/module/ui/context/OxyContext.d.ts.map +1 -1
  190. package/lib/typescript/module/ui/hooks/queries/queryKeys.d.ts +9 -0
  191. package/lib/typescript/module/ui/hooks/queries/queryKeys.d.ts.map +1 -1
  192. package/lib/typescript/module/ui/hooks/queries/useFileQueries.d.ts +50 -0
  193. package/lib/typescript/module/ui/hooks/queries/useFileQueries.d.ts.map +1 -0
  194. package/lib/typescript/module/ui/hooks/useAvatarPicker.d.ts +15 -10
  195. package/lib/typescript/module/ui/hooks/useAvatarPicker.d.ts.map +1 -1
  196. package/lib/typescript/module/ui/hooks/useReduceMotion.d.ts +14 -0
  197. package/lib/typescript/module/ui/hooks/useReduceMotion.d.ts.map +1 -0
  198. package/lib/typescript/module/ui/hooks/useSurfaceHeader.d.ts +1 -1
  199. package/lib/typescript/module/ui/hooks/useSurfaceHeader.d.ts.map +1 -1
  200. package/lib/typescript/module/ui/navigation/accountDialogManager.d.ts +13 -0
  201. package/lib/typescript/module/ui/navigation/accountDialogManager.d.ts.map +1 -1
  202. package/lib/typescript/module/ui/navigation/bottomSheetManager.d.ts +4 -5
  203. package/lib/typescript/module/ui/navigation/bottomSheetManager.d.ts.map +1 -1
  204. package/lib/typescript/module/ui/navigation/routes.d.ts +1 -1
  205. package/lib/typescript/module/ui/navigation/routes.d.ts.map +1 -1
  206. package/lib/typescript/module/ui/navigation/surfaceNavStack.d.ts +21 -0
  207. package/lib/typescript/module/ui/navigation/surfaceNavStack.d.ts.map +1 -1
  208. package/lib/typescript/module/ui/navigation/surfaceRegistry.d.ts +73 -13
  209. package/lib/typescript/module/ui/navigation/surfaceRegistry.d.ts.map +1 -1
  210. package/lib/typescript/module/ui/navigation/surfaces.d.ts +42 -11
  211. package/lib/typescript/module/ui/navigation/surfaces.d.ts.map +1 -1
  212. package/lib/typescript/module/ui/screens/AvatarCropScreen.d.ts +18 -5
  213. package/lib/typescript/module/ui/screens/AvatarCropScreen.d.ts.map +1 -1
  214. package/lib/typescript/module/ui/screens/ChangeAvatarScreen.d.ts +34 -0
  215. package/lib/typescript/module/ui/screens/ChangeAvatarScreen.d.ts.map +1 -0
  216. package/lib/typescript/module/ui/screens/FileManagementScreen.d.ts.map +1 -1
  217. package/lib/typescript/module/ui/screens/WelcomeNewUserScreen.d.ts.map +1 -1
  218. package/lib/typescript/module/ui/screens/fileManagement/FileLibraryError.d.ts +23 -0
  219. package/lib/typescript/module/ui/screens/fileManagement/FileLibraryError.d.ts.map +1 -0
  220. package/lib/typescript/module/ui/screens/fileManagement/PhotoPickerSection.d.ts +4 -0
  221. package/lib/typescript/module/ui/screens/fileManagement/PhotoPickerSection.d.ts.map +1 -1
  222. package/lib/typescript/module/ui/screens/fileManagement/hooks/useFileUploadState.d.ts +10 -2
  223. package/lib/typescript/module/ui/screens/fileManagement/hooks/useFileUploadState.d.ts.map +1 -1
  224. package/lib/typescript/module/ui/stores/resetSessionScopedStores.d.ts +8 -1
  225. package/lib/typescript/module/ui/stores/resetSessionScopedStores.d.ts.map +1 -1
  226. package/lib/typescript/module/ui/types/navigation.d.ts +2 -0
  227. package/lib/typescript/module/ui/types/navigation.d.ts.map +1 -1
  228. package/lib/typescript/module/ui/utils/avatarUtils.d.ts.map +1 -1
  229. package/lib/typescript/module/ui/utils/fileManagement.d.ts +0 -48
  230. package/lib/typescript/module/ui/utils/fileManagement.d.ts.map +1 -1
  231. package/package.json +11 -7
  232. package/src/ui/components/AvatarCameraBadge.tsx +35 -0
  233. package/src/ui/components/OxyAccountDialogScreen.tsx +38 -15
  234. package/src/ui/components/OxyAuthChooser.tsx +722 -185
  235. package/src/ui/components/ProfileButton.tsx +16 -1
  236. package/src/ui/components/ProfileSummaryCard.tsx +2 -25
  237. package/src/ui/components/SurfaceScreen.tsx +48 -14
  238. package/src/ui/components/oxyAuthChooserStyles.ts +75 -23
  239. package/src/ui/components/photogrid/JustifiedPhotoGrid.tsx +158 -84
  240. package/src/ui/components/photogrid/__tests__/chunkPhotos.test.ts +51 -0
  241. package/src/ui/context/OxyContext.tsx +74 -34
  242. package/src/ui/hooks/queries/queryKeys.ts +15 -0
  243. package/src/ui/hooks/queries/useFileQueries.ts +162 -0
  244. package/src/ui/hooks/useAvatarPicker.ts +38 -53
  245. package/src/ui/hooks/useReduceMotion.ts +58 -0
  246. package/src/ui/hooks/useSurfaceHeader.ts +2 -1
  247. package/src/ui/navigation/__tests__/getSurfaceConfig.test.ts +55 -7
  248. package/src/ui/navigation/__tests__/surfaceNavStack.test.ts +81 -0
  249. package/src/ui/navigation/accountDialogManager.ts +28 -0
  250. package/src/ui/navigation/bottomSheetManager.ts +6 -7
  251. package/src/ui/navigation/routes.ts +3 -1
  252. package/src/ui/navigation/surfaceNavStack.ts +72 -9
  253. package/src/ui/navigation/surfaceRegistry.ts +91 -17
  254. package/src/ui/navigation/surfaces.ts +108 -29
  255. package/src/ui/screens/AvatarCropScreen.tsx +336 -298
  256. package/src/ui/screens/ChangeAvatarScreen.tsx +359 -0
  257. package/src/ui/screens/FileManagementScreen.tsx +189 -432
  258. package/src/ui/screens/WelcomeNewUserScreen.tsx +18 -48
  259. package/src/ui/screens/fileManagement/FileLibraryError.tsx +56 -0
  260. package/src/ui/screens/fileManagement/PhotoPickerSection.tsx +73 -44
  261. package/src/ui/screens/fileManagement/hooks/useFileUploadState.ts +55 -71
  262. package/src/ui/stores/resetSessionScopedStores.ts +8 -3
  263. package/src/ui/types/navigation.ts +2 -0
  264. package/src/ui/utils/avatarUtils.ts +11 -2
  265. package/src/ui/utils/fileManagement.ts +1 -101
  266. package/lib/commonjs/ui/stores/fileStore.js +0 -153
  267. package/lib/commonjs/ui/stores/fileStore.js.map +0 -1
  268. package/lib/module/ui/stores/fileStore.js +0 -145
  269. package/lib/module/ui/stores/fileStore.js.map +0 -1
  270. package/lib/typescript/commonjs/ui/stores/fileStore.d.ts +0 -31
  271. package/lib/typescript/commonjs/ui/stores/fileStore.d.ts.map +0 -1
  272. package/lib/typescript/module/ui/stores/fileStore.d.ts +0 -31
  273. package/lib/typescript/module/ui/stores/fileStore.d.ts.map +0 -1
  274. package/src/ui/stores/fileStore.ts +0 -118
@@ -45,7 +45,12 @@ import { useDeviceManagement } from '../hooks/useDeviceManagement';
45
45
  import { getStorageKeys, createPlatformStorage, type StorageInterface } from '../utils/storageHelpers';
46
46
  import type { RouteName } from '../navigation/routes';
47
47
  import { showBottomSheet as globalShowBottomSheet } from '../navigation/bottomSheetManager';
48
- import { presentDetached, type SurfaceInstance } from '../navigation/surfaces';
48
+ import {
49
+ presentDetached,
50
+ topRouteSurface,
51
+ type SurfaceInstance,
52
+ type TrackedSurface,
53
+ } from '../navigation/surfaces';
49
54
  import { useQueryClient, onlineManager } from '@tanstack/react-query';
50
55
  import { clearQueryCache } from '../hooks/queryClient';
51
56
  import { useAvatarPicker } from '../hooks/useAvatarPicker';
@@ -652,6 +657,28 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
652
657
  // surface settling (`result.finally`) flips it false — no other write site.
653
658
  const accountDialogSurfaceRef = useRef<SurfaceInstance<'AccountDialog'> | null>(null);
654
659
 
660
+ // When the account dialog is shown by MORPHING an already-open route surface
661
+ // (e.g. ManageAccount → the account switcher) it is pushed as a frame ONTO that
662
+ // surface rather than presented as its own. This holds that host surface so the
663
+ // close path can pop the AccountDialog frame back to it. `null` when the dialog
664
+ // is its OWN detached surface (opened cold, with no surface underneath).
665
+ const accountDialogHostRef = useRef<TrackedSurface | null>(null);
666
+
667
+ // The single teardown for the account dialog wherever it lives: pop the morphed
668
+ // frame back to its host surface (reshaping Manage ← switcher), or dismiss the
669
+ // detached surface (whose settle then flips `accountDialogOpen`). Reads only
670
+ // stable refs + `setAccountDialogOpen`, so its identity never changes.
671
+ const dismissAccountDialogSurface = useCallback((): void => {
672
+ const host = accountDialogHostRef.current;
673
+ if (host) {
674
+ accountDialogHostRef.current = null;
675
+ setAccountDialogOpen(false);
676
+ host.goBack();
677
+ return;
678
+ }
679
+ accountDialogSurfaceRef.current?.dismiss();
680
+ }, []);
681
+
655
682
  const accountDialogControllerRef = useRef<AccountDialogController | null>(null);
656
683
  if (!accountDialogControllerRef.current) {
657
684
  accountDialogControllerRef.current = createAccountDialogController({
@@ -665,11 +692,9 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
665
692
  commitSession: (session) => handleWebSessionRef.current(session),
666
693
  commitSwitchedSession: (session) => commitSwitchedSessionRef.current(session),
667
694
  onSignedIn: () => {
668
- // Dismiss the surface; its settle (`result.finally` below) is what flips
669
- // `accountDialogOpen` false. The surface's present→settle lifecycle is the
670
- // SINGLE source of truth for "is the account dialog open" — never a manual
671
- // flip here.
672
- accountDialogSurfaceRef.current?.dismiss();
695
+ // Close the dialog: pop the morphed frame back to its host surface, or
696
+ // dismiss the detached surface (whose settle flips `accountDialogOpen`).
697
+ dismissAccountDialogSurface();
673
698
  },
674
699
  openUrl: (url) => {
675
700
  if (isWebBrowser()) {
@@ -694,40 +719,55 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
694
719
  const accountDialogController = accountDialogControllerRef.current;
695
720
 
696
721
  const openAccountDialog = useCallback((view?: AccountDialogView): void => {
697
- accountDialogControllerRef.current?.setView(view ?? 'accounts');
698
- // Present the AccountDialog surface on the shared Bloom stack the FIRST time
699
- // (subsequent opens just re-point the controller's view above). `presentDetached`
700
- // keeps it OUT of the `showBottomSheet` route-surface lineage, so closing the
701
- // bottom-sheet session never touches the account dialog and vice-versa. When a
702
- // route surface (e.g. ManageAccount) is already open, this stacks the dialog
703
- // ABOVE it; dismissing the dialog unwinds back to that surface.
704
- if (!accountDialogSurfaceRef.current) {
705
- const instance = presentDetached(
706
- 'AccountDialog',
707
- { initialView: view ?? 'accounts' },
708
- { placement: { base: 'bottom', md: 'center' }, dismissOnBackdrop: false, maxWidth: 420 },
709
- );
710
- accountDialogSurfaceRef.current = instance;
711
- // The surface settling is the ONE place `accountDialogOpen` flips false —
712
- // covering every dismiss path (close button, `onSignedIn`, programmatic
713
- // `closeAccountDialog`, host unmount). So the stack owns the open state.
714
- instance.result.finally(() => {
715
- if (accountDialogSurfaceRef.current === instance) {
716
- accountDialogSurfaceRef.current = null;
717
- setAccountDialogOpen(false);
718
- }
719
- });
722
+ const nextView = view ?? 'accounts';
723
+ accountDialogControllerRef.current?.setView(nextView);
724
+
725
+ // Its own detached surface is already open just re-point the view above.
726
+ if (accountDialogSurfaceRef.current) {
727
+ setAccountDialogOpen(true);
728
+ return;
720
729
  }
730
+
731
+ // When a route surface (e.g. ManageAccount) is already open, MORPH it in
732
+ // place: push the AccountDialog as a frame ONTO that surface, so the panel
733
+ // reshapes from that surface to the switcher and a back returns to it. With no
734
+ // surface open (opened cold from a sign-in button), present the dialog as its
735
+ // OWN detached surface — kept OUT of the `showBottomSheet` route-surface
736
+ // lineage so closing the bottom-sheet session never touches it and vice-versa.
737
+ // `navigate` is idempotent for the top frame (it replaces rather than
738
+ // duplicates), so calling this while already morphed in just re-points the
739
+ // view, and calling it after a back re-pushes the frame — no stale guard.
740
+ const host = topRouteSurface();
741
+ if (host) {
742
+ accountDialogHostRef.current = host;
743
+ host.navigate('AccountDialog', { initialView: nextView });
744
+ setAccountDialogOpen(true);
745
+ return;
746
+ }
747
+
748
+ const instance = presentDetached(
749
+ 'AccountDialog',
750
+ { initialView: nextView },
751
+ { placement: { base: 'bottom', md: 'center' }, dismissOnBackdrop: false, maxWidth: 420 },
752
+ );
753
+ accountDialogSurfaceRef.current = instance;
754
+ // The surface settling is the ONE place `accountDialogOpen` flips false for
755
+ // the detached path — covering every dismiss (close button, `onSignedIn`,
756
+ // programmatic `closeAccountDialog`, host unmount). The morphed path clears
757
+ // its own state in `dismissAccountDialogSurface`.
758
+ instance.result.finally(() => {
759
+ if (accountDialogSurfaceRef.current === instance) {
760
+ accountDialogSurfaceRef.current = null;
761
+ setAccountDialogOpen(false);
762
+ }
763
+ });
721
764
  setAccountDialogOpen(true);
722
765
  }, []);
723
766
 
724
767
  const closeAccountDialog = useCallback((): void => {
725
768
  accountDialogControllerRef.current?.cancelSignIn();
726
- // Dismiss the surface; its settle (`result.finally` above) flips
727
- // `accountDialogOpen` false. Do NOT flip it here — the surface lifecycle is
728
- // the single source of truth, so a manual write would be a second authority.
729
- accountDialogSurfaceRef.current?.dismiss();
730
- }, []);
769
+ dismissAccountDialogSurface();
770
+ }, [dismissAccountDialogSurface]);
731
771
 
732
772
  // Start driving the dialog on mount; tear it down on unmount.
733
773
  useEffect(() => {
@@ -100,6 +100,13 @@ export const queryKeys = {
100
100
  usage: (accountScope?: string) => [...queryKeys.storage.all, 'usage', accountScope] as const,
101
101
  },
102
102
 
103
+ // User file library (infinite-paginated; owner-scoped so switching the active
104
+ // account shows that account's files without a manual reset)
105
+ files: {
106
+ all: ['files'] as const,
107
+ list: (ownerId?: string) => [...queryKeys.files.all, 'list', ownerId || 'anonymous'] as const,
108
+ },
109
+
103
110
  // Connected apps (OAuth grants the user has authorized)
104
111
  connectedApps: {
105
112
  all: ['connectedApps'] as const,
@@ -204,3 +211,11 @@ export const invalidateConnectedAppsQueries = (queryClient: QueryClient): void =
204
211
  queryClient.invalidateQueries({ queryKey: queryKeys.connectedApps.all });
205
212
  };
206
213
 
214
+ /**
215
+ * Helper to invalidate the user's file library (all owners). Pass an ownerId via
216
+ * `queryKeys.files.list(ownerId)` for a scoped invalidation instead.
217
+ */
218
+ export const invalidateFileQueries = (queryClient: QueryClient): void => {
219
+ queryClient.invalidateQueries({ queryKey: queryKeys.files.all });
220
+ };
221
+
@@ -0,0 +1,162 @@
1
+ /**
2
+ * User file-library queries + cache helpers.
3
+ *
4
+ * The file list is owned by React Query (`useInfiniteQuery`), NOT a zustand
5
+ * store: server pages live in the query cache, and every optimistic mutation
6
+ * (upload / delete / visibility) edits that cache via the pure `*InCache`
7
+ * helpers below. The `queryFn` is side-effect-free — it only fetches and maps a
8
+ * page. This is what lets the file manager stay effect-free while preserving
9
+ * infinite scroll, pull-to-refresh, and optimistic upload reconciliation.
10
+ */
11
+
12
+ import {
13
+ useInfiniteQuery,
14
+ type InfiniteData,
15
+ type QueryClient,
16
+ } from '@tanstack/react-query';
17
+ import type { FileMetadata } from '@oxyhq/core';
18
+ import { useOxy } from '../../context/OxyContext';
19
+ import { queryKeys } from './queryKeys';
20
+
21
+ /** Files fetched per page. */
22
+ export const FILES_PAGE_SIZE = 40;
23
+
24
+ /** Raw file record as returned by `oxyServices.listUserFiles` / `assetUpload`. */
25
+ export interface RawUserFile {
26
+ id: string;
27
+ originalName?: string;
28
+ sha256?: string;
29
+ mime?: string;
30
+ size?: number;
31
+ createdAt?: string;
32
+ metadata?: Record<string, unknown>;
33
+ variants?: unknown[];
34
+ }
35
+
36
+ /** One page of the infinite file list. */
37
+ export interface UserFilesPage {
38
+ files: FileMetadata[];
39
+ total: number;
40
+ hasMore: boolean;
41
+ nextOffset: number;
42
+ }
43
+
44
+ export type UserFilesInfinite = InfiniteData<UserFilesPage>;
45
+
46
+ /** Normalize a raw API file record into the client `FileMetadata` shape. */
47
+ export function mapRawFileToMetadata(raw: RawUserFile): FileMetadata {
48
+ return {
49
+ id: raw.id,
50
+ filename: raw.originalName ?? raw.sha256 ?? '',
51
+ contentType: raw.mime ?? '',
52
+ length: raw.size ?? 0,
53
+ chunkSize: 0,
54
+ uploadDate: raw.createdAt ?? '',
55
+ metadata: raw.metadata ?? {},
56
+ variants: (raw.variants ?? []) as FileMetadata['variants'],
57
+ };
58
+ }
59
+
60
+ /**
61
+ * Infinite-paginated list of the owner's files. Fires on mount and whenever
62
+ * `ownerId` changes (a new key); `staleTime: 0` reloads on every open. The
63
+ * rendered list is `data.pages.flatMap(p => p.files)`.
64
+ */
65
+ export const useUserFilesInfinite = (ownerId?: string) => {
66
+ const { oxyServices } = useOxy();
67
+ return useInfiniteQuery<UserFilesPage, Error>({
68
+ queryKey: queryKeys.files.list(ownerId),
69
+ queryFn: async ({ pageParam }) => {
70
+ const offset = typeof pageParam === 'number' ? pageParam : 0;
71
+ const response = await oxyServices.listUserFiles(FILES_PAGE_SIZE, offset);
72
+ const raw = (response.files ?? []) as RawUserFile[];
73
+ const files = raw.map(mapRawFileToMetadata);
74
+ return {
75
+ files,
76
+ total: response.total ?? offset + files.length,
77
+ hasMore: Boolean(response.hasMore),
78
+ nextOffset: offset + FILES_PAGE_SIZE,
79
+ };
80
+ },
81
+ initialPageParam: 0,
82
+ getNextPageParam: (lastPage) => (lastPage.hasMore ? lastPage.nextOffset : undefined),
83
+ enabled: Boolean(ownerId),
84
+ staleTime: 0,
85
+ refetchOnWindowFocus: false,
86
+ });
87
+ };
88
+
89
+ // --- Pure cache transforms (optimistic updates) -----------------------------
90
+ // Each edits the cached `InfiniteData` for one owner. All no-op when nothing is
91
+ // cached yet (e.g. an upload before the first page loads) — the subsequent
92
+ // `invalidateQueries` reconcile then brings the authoritative record.
93
+
94
+ const fileListKey = (ownerId?: string) => queryKeys.files.list(ownerId);
95
+
96
+ const mapPages = (
97
+ data: UserFilesInfinite,
98
+ fn: (files: FileMetadata[]) => FileMetadata[],
99
+ ): UserFilesInfinite => ({
100
+ ...data,
101
+ pages: data.pages.map((page) => ({ ...page, files: fn(page.files) })),
102
+ });
103
+
104
+ /** Prepend an (optimistic) file to the first page. */
105
+ export function prependFileToCache(
106
+ queryClient: QueryClient,
107
+ ownerId: string | undefined,
108
+ file: FileMetadata,
109
+ ): void {
110
+ queryClient.setQueryData<UserFilesInfinite>(fileListKey(ownerId), (data) => {
111
+ if (!data || data.pages.length === 0) return data;
112
+ return {
113
+ ...data,
114
+ pages: data.pages.map((page, index) =>
115
+ index === 0 ? { ...page, files: [file, ...page.files] } : page,
116
+ ),
117
+ };
118
+ });
119
+ }
120
+
121
+ /** Remove a file (by id) from every page. */
122
+ export function removeFileFromCache(
123
+ queryClient: QueryClient,
124
+ ownerId: string | undefined,
125
+ fileId: string,
126
+ ): void {
127
+ queryClient.setQueryData<UserFilesInfinite>(fileListKey(ownerId), (data) =>
128
+ data ? mapPages(data, (files) => files.filter((f) => f.id !== fileId)) : data,
129
+ );
130
+ }
131
+
132
+ /** Swap an optimistic file (by id) for its persisted counterpart. */
133
+ export function replaceFileInCache(
134
+ queryClient: QueryClient,
135
+ ownerId: string | undefined,
136
+ oldId: string,
137
+ newFile: FileMetadata,
138
+ ): void {
139
+ queryClient.setQueryData<UserFilesInfinite>(fileListKey(ownerId), (data) =>
140
+ data ? mapPages(data, (files) => files.map((f) => (f.id === oldId ? newFile : f))) : data,
141
+ );
142
+ }
143
+
144
+ /** Shallow-merge a metadata patch into a cached file (e.g. visibility change). */
145
+ export function patchFileMetadataInCache(
146
+ queryClient: QueryClient,
147
+ ownerId: string | undefined,
148
+ fileId: string,
149
+ metadataPatch: Record<string, unknown>,
150
+ ): void {
151
+ queryClient.setQueryData<UserFilesInfinite>(fileListKey(ownerId), (data) =>
152
+ data
153
+ ? mapPages(data, (files) =>
154
+ files.map((f) =>
155
+ f.id === fileId
156
+ ? { ...f, metadata: { ...f.metadata, ...metadataPatch } as FileMetadata['metadata'] }
157
+ : f,
158
+ ),
159
+ )
160
+ : data,
161
+ );
162
+ }
@@ -1,18 +1,23 @@
1
1
  /**
2
2
  * Avatar Picker Hook
3
3
  *
4
- * Extracts avatar selection logic from OxyContext for better modularity.
4
+ * The ONE write path for a user's profile picture. It owns no UI: the whole
5
+ * choose-and-crop experience is a single surface.
5
6
  *
6
- * Flow (typed promises over the shared surface stack — no callback props):
7
- * 1. `surfaces.present('FileManagement', …)` opens the image-only picker and
8
- * resolves with the picked file (or `undefined` if the user cancels).
9
- * 2. Resolve a private-safe source URL, then `surfaces.present('AvatarCrop', …)`
10
- * opens the square-crop editor and resolves with the cropped JPEG.
11
- * 3. The cropped JPEG is uploaded as a NEW file via `oxyServices.assetUpload`
12
- * and that file becomes the user's avatar.
7
+ * Flow (typed promise over the shared surface stack — no callback props):
8
+ * 1. `openWithinOrPresent('ChangeAvatar')` opens the source list (device gallery,
9
+ * camera, existing Oxy files, remove) MORPHING into the caller's surface
10
+ * when one is open, else presenting cold. Whichever source the user picks, it
11
+ * navigates WITHIN to the crop editor so there is exactly one cropper and
12
+ * one entry into it.
13
+ * 2. The surface resolves with the cropped JPEG, with `{ removed: true }` when
14
+ * the user removed their photo, or `undefined` when they cancelled.
15
+ * 3. A crop is uploaded as a NEW file via `oxyServices.assetUpload` and set as
16
+ * the avatar; a removal clears the avatar field.
13
17
  *
14
- * `expo-image-manipulator` is required for the crop step (declared as an
15
- * optional peer in the consuming app) see AvatarCropScreen.
18
+ * `expo-image-manipulator` is required for the crop step and `expo-image-picker`
19
+ * for the device/camera sources (both declared as optional peers in the
20
+ * consuming app) — see AvatarCropScreen / ChangeAvatarScreen.
16
21
  */
17
22
 
18
23
  import { useCallback } from 'react';
@@ -21,7 +26,7 @@ import { translate, updateAvatarVisibility } from '@oxyhq/core';
21
26
  import type { QueryClient } from '@tanstack/react-query';
22
27
  import { toast } from '@oxyhq/bloom';
23
28
  import { updateProfileWithAvatar } from '../utils/avatarUtils';
24
- import { surfaces } from '../navigation/surfaces';
29
+ import { openWithinOrPresent } from '../navigation/surfaces';
25
30
  import type { AvatarCropResult } from '../screens/AvatarCropScreen';
26
31
 
27
32
  interface UseAvatarPickerOptions {
@@ -98,56 +103,36 @@ export function useAvatarPicker({
98
103
  [activeSessionId, currentLanguage, oxyServices, queryClient],
99
104
  );
100
105
 
101
- const openAvatarPicker = useCallback(async () => {
102
- // 1. Present the image-only picker; it dismisses with the picked file when
103
- // the user taps one, or `undefined` if they cancel.
104
- const file = await surfaces.present('FileManagement', {
105
- selectMode: true,
106
- multiSelect: false,
107
- disabledMimeTypes: ['video/', 'audio/', 'application/pdf'],
108
- });
109
- if (!file) return;
110
-
111
- if (!file.contentType?.startsWith('image/')) {
112
- toast.error(
113
- translate(currentLanguage ?? undefined, 'editProfile.toasts.selectImage') ||
114
- 'Please select an image file',
115
- );
116
- return;
117
- }
118
-
119
- // 2. Resolve a working source URL. The picked file is usually PRIVATE, so
120
- // the synchronous `getFileDownloadUrl` (public CDN origin) would 404 and
121
- // the crop screen's `Image.getSize` would fail silently. `assetGetUrl`
122
- // throws on failure (unlike `getFileDownloadUrlAsync`, which swallows
123
- // errors and falls back to the broken public URL), so a failure surfaces
124
- // a real user-facing error instead of a blank crop canvas. No variant is
125
- // requested — cropping needs the original.
126
- let sourceUri: string;
106
+ /** Clear the avatar so the profile falls back to the user's initials. */
107
+ const removeAvatar = useCallback(async () => {
127
108
  try {
128
- const resolved = await oxyServices.assetGetUrl(file.id);
129
- if (!resolved?.url) {
130
- throw new Error('No download URL returned for the selected image');
131
- }
132
- sourceUri = resolved.url;
109
+ await updateProfileWithAvatar({ avatar: '' }, oxyServices, activeSessionId, queryClient);
110
+ toast.success(
111
+ translate(currentLanguage ?? undefined, 'editProfile.toasts.avatarRemoved'),
112
+ );
133
113
  } catch (e: unknown) {
134
114
  const message = e instanceof Error ? e.message : undefined;
135
115
  toast.error(
136
116
  message ||
137
- translate(currentLanguage ?? undefined, 'editProfile.toasts.cropMeasureFailed') ||
138
- 'Could not load the selected image',
117
+ translate(currentLanguage ?? undefined, 'editProfile.toasts.updateAvatarFailed'),
139
118
  );
140
- return;
141
119
  }
120
+ }, [activeSessionId, currentLanguage, oxyServices, queryClient]);
142
121
 
143
- // 3. Present the square-crop editor in place of the picker; it dismisses
144
- // with the cropped JPEG on confirm, or `undefined` if cancelled.
145
- const cropped = await surfaces.present('AvatarCrop', { imageUri: sourceUri });
146
- if (!cropped) return;
147
-
148
- // 4. Upload + set as the user's avatar.
149
- await finalizeCroppedAvatar(cropped);
150
- }, [currentLanguage, finalizeCroppedAvatar, oxyServices]);
122
+ const openAvatarPicker = useCallback(async () => {
123
+ // The source list + crop editor are ONE flow. When triggered from a screen
124
+ // already inside a surface (EditProfile, ManageAccount, WelcomeNewUser every
125
+ // current entry point), it MORPHS into that surface and pops back on finish;
126
+ // triggered with no surface open it presents cold. Either way it resolves with
127
+ // the cropped JPEG, a removal, or `undefined` when the user backs out.
128
+ const result = await openWithinOrPresent('ChangeAvatar');
129
+ if (!result) return;
130
+ if ('removed' in result) {
131
+ await removeAvatar();
132
+ return;
133
+ }
134
+ await finalizeCroppedAvatar(result);
135
+ }, [finalizeCroppedAvatar, removeAvatar]);
151
136
 
152
137
  return { openAvatarPicker };
153
138
  }
@@ -0,0 +1,58 @@
1
+ /**
2
+ * useReduceMotion
3
+ *
4
+ * Reactive boolean reflecting the OS "reduce motion" accessibility preference.
5
+ * Wraps `AccessibilityInfo` as an external store via `useSyncExternalStore` — the
6
+ * effect-free, React-Compiler-safe way to read an external mutable source. The
7
+ * initial value is read asynchronously once (the platform API is a Promise) and
8
+ * cached module-side; subsequent changes arrive via the `reduceMotionChanged`
9
+ * event. Mirrors the `useOnlineStatus` pattern.
10
+ *
11
+ * Consumers use it to pin entrance/stagger animations off under reduce-motion.
12
+ */
13
+
14
+ import { useSyncExternalStore } from 'react';
15
+ import { AccessibilityInfo } from 'react-native';
16
+
17
+ let cachedReduceMotion = false;
18
+ let initialized = false;
19
+ const listeners = new Set<() => void>();
20
+
21
+ const notifyAll = (): void => {
22
+ for (const listener of listeners) listener();
23
+ };
24
+
25
+ const setCached = (value: boolean): void => {
26
+ if (cachedReduceMotion === value) return;
27
+ cachedReduceMotion = value;
28
+ notifyAll();
29
+ };
30
+
31
+ const subscribeReduceMotion = (notify: () => void): (() => void) => {
32
+ // Populate the cache the first time anyone subscribes (the platform read is
33
+ // async, so it cannot be a synchronous `getSnapshot`).
34
+ if (!initialized) {
35
+ initialized = true;
36
+ AccessibilityInfo.isReduceMotionEnabled()
37
+ .then(setCached)
38
+ .catch(() => {
39
+ // No preference available — the `false` default stands.
40
+ });
41
+ }
42
+ listeners.add(notify);
43
+ const sub = AccessibilityInfo.addEventListener('reduceMotionChanged', setCached);
44
+ return () => {
45
+ listeners.delete(notify);
46
+ sub.remove();
47
+ };
48
+ };
49
+
50
+ const getSnapshot = (): boolean => cachedReduceMotion;
51
+
52
+ // SSR/web-first paint: assume motion is allowed (no reduce) until the async read
53
+ // resolves — matches the prior default.
54
+ const getServerSnapshot = (): boolean => false;
55
+
56
+ export function useReduceMotion(): boolean {
57
+ return useSyncExternalStore(subscribeReduceMotion, getSnapshot, getServerSnapshot);
58
+ }
@@ -13,7 +13,7 @@ import type { DialogHeaderConfig } from '@oxyhq/bloom/dialog';
13
13
  */
14
14
  export type SurfaceHeaderContent = Pick<
15
15
  DialogHeaderConfig,
16
- 'title' | 'subtitle' | 'largeTitle' | 'left' | 'right' | 'onBack'
16
+ 'title' | 'titleContent' | 'subtitle' | 'largeTitle' | 'left' | 'right' | 'onBack'
17
17
  >;
18
18
 
19
19
  interface SurfaceHeaderContextValue {
@@ -47,6 +47,7 @@ export function useSurfaceHeader(content: SurfaceHeaderContent | null | undefine
47
47
  }, [
48
48
  set,
49
49
  content?.title,
50
+ content?.titleContent,
50
51
  content?.subtitle,
51
52
  content?.largeTitle,
52
53
  content?.left,
@@ -1,10 +1,10 @@
1
1
  import { getSurfaceConfig, getSurfacePresentation } from '../surfaceRegistry';
2
2
 
3
3
  /**
4
- * `getSurfaceConfig` resolves which Bloom surface a route is presented in. The
5
- * flagship image picker (FileManagement in image-only select mode) upgrades to a
6
- * full-bleed `'fullScreen'` surface that owns its own scrolling; every other
7
- * route stays on the responsive `'sheet'`. These guard the routing contract.
4
+ * `getSurfaceConfig` resolves how a route renders. EVERY route is a responsive
5
+ * `'sheet'` that MORPHS in place; NO route stacks (genuine overlays are Bloom-raw
6
+ * calls outside this registry). The flagship image picker is a sheet that owns its
7
+ * own scrolling + chrome. These guard the routing contract.
8
8
  */
9
9
  describe('getSurfaceConfig presentation', () => {
10
10
  const imageOnlyPickerProps = {
@@ -18,11 +18,59 @@ describe('getSurfaceConfig presentation', () => {
18
18
  expect(getSurfacePresentation('ManageAccount', {})).toBe('sheet');
19
19
  });
20
20
 
21
- it('routes the FileManagement image-only picker to a full-screen surface', () => {
21
+ it('renders the FileManagement image-only picker as a morphing own-scroller sheet', () => {
22
22
  const config = getSurfaceConfig('FileManagement', imageOnlyPickerProps);
23
- expect(config.presentation).toBe('fullScreen');
24
- // The full-bleed picker owns its own FlatList → the host must not wrap it.
23
+ // It morphs in place like every screen — a sheet, not a stacked full-bleed.
24
+ expect(config.presentation).toBe('sheet');
25
+ expect(config.stacks).toBe(false);
26
+ // PhotoPickerView owns its own FlatList → the host must not wrap it in a
27
+ // ScrollView; and its own translucent bar → no Dialog nav header.
25
28
  expect(config.scrollable).toBe(false);
29
+ expect(config.header).toBe(false);
30
+ });
31
+
32
+ it('morphs the surface for every route, the picker included', () => {
33
+ // NAV-WITHIN: drilling into these swaps the content inside ONE surface,
34
+ // which reshapes the panel instead of hard-cutting.
35
+ expect(getSurfaceConfig('ManageAccount', {}).morph).toBe(true);
36
+ expect(getSurfaceConfig('FAQ', {}).morph).toBe(true);
37
+ expect(getSurfaceConfig('FollowersList', {}).morph).toBe(true);
38
+ expect(getSurfaceConfig('FileManagement', imageOnlyPickerProps).morph).toBe(true);
39
+ });
40
+
41
+ it('never stacks any route — every screen morphs in place', () => {
42
+ // `stacks` is the DECLARATIVE morph-vs-stack signal, and it is FALSE for
43
+ // every route: overlays (action sheets / confirm / prompt) are Bloom-raw
44
+ // `surfaces.present`/`confirm` calls outside this registry.
45
+ expect(getSurfaceConfig('ManageAccount', {}).stacks).toBe(false);
46
+ expect(getSurfaceConfig('AvatarCrop', {}).stacks).toBe(false);
47
+ expect(getSurfaceConfig('ChangeAvatar', {}).stacks).toBe(false);
48
+ expect(getSurfaceConfig('AccountDialog', {}).stacks).toBe(false);
49
+ expect(getSurfaceConfig('FileManagement', imageOnlyPickerProps).stacks).toBe(false);
50
+ expect(getSurfaceConfig('FileManagement', {}).stacks).toBe(false);
51
+ });
52
+
53
+ it('lets the avatar flow morph list → crop inside ONE surface', () => {
54
+ // ChangeAvatar navigates WITHIN its surface to AvatarCrop, so both frames
55
+ // must share a presentation (else the crop would open on top instead of
56
+ // morphing) and both must opt IN to the morph.
57
+ const list = getSurfaceConfig('ChangeAvatar', {});
58
+ const crop = getSurfaceConfig('AvatarCrop', {});
59
+ expect(crop.presentation).toBe(list.presentation);
60
+ expect(list.morph).toBe(true);
61
+ expect(crop.morph).toBe(true);
62
+ // The morph can only RESIZE the panel when the Dialog owns the scroll
63
+ // container — an own-scroller surface cross-fades but never reshapes.
64
+ expect(list.scrollable).toBe(true);
65
+ expect(crop.scrollable).toBe(true);
66
+ });
67
+
68
+ it('gives the avatar flow the Dialog nav header on both frames', () => {
69
+ // The Dialog's `header` is fixed for a surface's whole life, so a
70
+ // headerless crop frame inside a header-mode surface would stack two
71
+ // bars. Both frames declare their chrome through `useSurfaceHeader`.
72
+ expect(getSurfaceConfig('ChangeAvatar', {}).header).toBe(true);
73
+ expect(getSurfaceConfig('AvatarCrop', {}).header).toBe(true);
26
74
  });
27
75
 
28
76
  it('keeps FileManagement on the sheet when it is not an image-only picker', () => {