@oxyhq/services 22.8.6 → 22.9.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 (152) hide show
  1. package/lib/commonjs/ui/components/OxyAccountDialog.js +30 -39
  2. package/lib/commonjs/ui/components/OxyAccountDialog.js.map +1 -1
  3. package/lib/commonjs/ui/components/OxyProvider.js +9 -29
  4. package/lib/commonjs/ui/components/OxyProvider.js.map +1 -1
  5. package/lib/commonjs/ui/components/SurfaceScreen.js +146 -0
  6. package/lib/commonjs/ui/components/SurfaceScreen.js.map +1 -0
  7. package/lib/commonjs/ui/components/fileManagement/UploadPreview.js +3 -3
  8. package/lib/commonjs/ui/components/fileManagement/UploadPreview.js.map +1 -1
  9. package/lib/commonjs/ui/context/OxyContext.js +32 -1
  10. package/lib/commonjs/ui/context/OxyContext.js.map +1 -1
  11. package/lib/commonjs/ui/navigation/bottomSheetManager.js +38 -56
  12. package/lib/commonjs/ui/navigation/bottomSheetManager.js.map +1 -1
  13. package/lib/commonjs/ui/navigation/routes.js +7 -57
  14. package/lib/commonjs/ui/navigation/routes.js.map +1 -1
  15. package/lib/commonjs/ui/navigation/surfaceNavStack.js +98 -0
  16. package/lib/commonjs/ui/navigation/surfaceNavStack.js.map +1 -0
  17. package/lib/commonjs/ui/navigation/surfaceRegistry.js +108 -0
  18. package/lib/commonjs/ui/navigation/surfaceRegistry.js.map +1 -0
  19. package/lib/commonjs/ui/navigation/surfaces.js +219 -0
  20. package/lib/commonjs/ui/navigation/surfaces.js.map +1 -0
  21. package/lib/commonjs/ui/screens/FileManagementScreen.js +8 -1
  22. package/lib/commonjs/ui/screens/FileManagementScreen.js.map +1 -1
  23. package/lib/commonjs/ui/screens/fileManagement/PhotoPickerSection.js +60 -3
  24. package/lib/commonjs/ui/screens/fileManagement/PhotoPickerSection.js.map +1 -1
  25. package/lib/commonjs/ui/screens/fileManagement/hooks/useFileUploadState.js +14 -17
  26. package/lib/commonjs/ui/screens/fileManagement/hooks/useFileUploadState.js.map +1 -1
  27. package/lib/commonjs/ui/screens/fileManagement/shared.js +13 -1
  28. package/lib/commonjs/ui/screens/fileManagement/shared.js.map +1 -1
  29. package/lib/commonjs/ui/types/surfaceScreen.js +6 -0
  30. package/lib/commonjs/ui/types/surfaceScreen.js.map +1 -0
  31. package/lib/module/ui/components/OxyAccountDialog.js +31 -40
  32. package/lib/module/ui/components/OxyAccountDialog.js.map +1 -1
  33. package/lib/module/ui/components/OxyProvider.js +10 -30
  34. package/lib/module/ui/components/OxyProvider.js.map +1 -1
  35. package/lib/module/ui/components/SurfaceScreen.js +141 -0
  36. package/lib/module/ui/components/SurfaceScreen.js.map +1 -0
  37. package/lib/module/ui/components/fileManagement/UploadPreview.js +3 -3
  38. package/lib/module/ui/components/fileManagement/UploadPreview.js.map +1 -1
  39. package/lib/module/ui/context/OxyContext.js +32 -1
  40. package/lib/module/ui/context/OxyContext.js.map +1 -1
  41. package/lib/module/ui/navigation/bottomSheetManager.js +37 -53
  42. package/lib/module/ui/navigation/bottomSheetManager.js.map +1 -1
  43. package/lib/module/ui/navigation/routes.js +6 -56
  44. package/lib/module/ui/navigation/routes.js.map +1 -1
  45. package/lib/module/ui/navigation/surfaceNavStack.js +95 -0
  46. package/lib/module/ui/navigation/surfaceNavStack.js.map +1 -0
  47. package/lib/module/ui/navigation/surfaceRegistry.js +102 -0
  48. package/lib/module/ui/navigation/surfaceRegistry.js.map +1 -0
  49. package/lib/module/ui/navigation/surfaces.js +207 -0
  50. package/lib/module/ui/navigation/surfaces.js.map +1 -0
  51. package/lib/module/ui/screens/FileManagementScreen.js +8 -1
  52. package/lib/module/ui/screens/FileManagementScreen.js.map +1 -1
  53. package/lib/module/ui/screens/fileManagement/PhotoPickerSection.js +60 -3
  54. package/lib/module/ui/screens/fileManagement/PhotoPickerSection.js.map +1 -1
  55. package/lib/module/ui/screens/fileManagement/hooks/useFileUploadState.js +14 -18
  56. package/lib/module/ui/screens/fileManagement/hooks/useFileUploadState.js.map +1 -1
  57. package/lib/module/ui/screens/fileManagement/shared.js +12 -0
  58. package/lib/module/ui/screens/fileManagement/shared.js.map +1 -1
  59. package/lib/module/ui/types/surfaceScreen.js +4 -0
  60. package/lib/module/ui/types/surfaceScreen.js.map +1 -0
  61. package/lib/typescript/commonjs/ui/components/OxyAccountDialog.d.ts +29 -27
  62. package/lib/typescript/commonjs/ui/components/OxyAccountDialog.d.ts.map +1 -1
  63. package/lib/typescript/commonjs/ui/components/OxyProvider.d.ts +3 -2
  64. package/lib/typescript/commonjs/ui/components/OxyProvider.d.ts.map +1 -1
  65. package/lib/typescript/commonjs/ui/components/SurfaceScreen.d.ts +24 -0
  66. package/lib/typescript/commonjs/ui/components/SurfaceScreen.d.ts.map +1 -0
  67. package/lib/typescript/commonjs/ui/components/fileManagement/UploadPreview.d.ts +3 -10
  68. package/lib/typescript/commonjs/ui/components/fileManagement/UploadPreview.d.ts.map +1 -1
  69. package/lib/typescript/commonjs/ui/context/OxyContext.d.ts.map +1 -1
  70. package/lib/typescript/commonjs/ui/navigation/bottomSheetManager.d.ts +24 -16
  71. package/lib/typescript/commonjs/ui/navigation/bottomSheetManager.d.ts.map +1 -1
  72. package/lib/typescript/commonjs/ui/navigation/routes.d.ts +2 -60
  73. package/lib/typescript/commonjs/ui/navigation/routes.d.ts.map +1 -1
  74. package/lib/typescript/commonjs/ui/navigation/surfaceNavStack.d.ts +51 -0
  75. package/lib/typescript/commonjs/ui/navigation/surfaceNavStack.d.ts.map +1 -0
  76. package/lib/typescript/commonjs/ui/navigation/surfaceRegistry.d.ts +117 -0
  77. package/lib/typescript/commonjs/ui/navigation/surfaceRegistry.d.ts.map +1 -0
  78. package/lib/typescript/commonjs/ui/navigation/surfaces.d.ts +79 -0
  79. package/lib/typescript/commonjs/ui/navigation/surfaces.d.ts.map +1 -0
  80. package/lib/typescript/commonjs/ui/screens/FileManagementScreen.d.ts.map +1 -1
  81. package/lib/typescript/commonjs/ui/screens/fileManagement/PhotoPickerSection.d.ts +7 -0
  82. package/lib/typescript/commonjs/ui/screens/fileManagement/PhotoPickerSection.d.ts.map +1 -1
  83. package/lib/typescript/commonjs/ui/screens/fileManagement/hooks/useFileUploadState.d.ts +1 -1
  84. package/lib/typescript/commonjs/ui/screens/fileManagement/hooks/useFileUploadState.d.ts.map +1 -1
  85. package/lib/typescript/commonjs/ui/screens/fileManagement/shared.d.ts +4 -0
  86. package/lib/typescript/commonjs/ui/screens/fileManagement/shared.d.ts.map +1 -1
  87. package/lib/typescript/commonjs/ui/types/surfaceScreen.d.ts +38 -0
  88. package/lib/typescript/commonjs/ui/types/surfaceScreen.d.ts.map +1 -0
  89. package/lib/typescript/module/ui/components/OxyAccountDialog.d.ts +29 -27
  90. package/lib/typescript/module/ui/components/OxyAccountDialog.d.ts.map +1 -1
  91. package/lib/typescript/module/ui/components/OxyProvider.d.ts +3 -2
  92. package/lib/typescript/module/ui/components/OxyProvider.d.ts.map +1 -1
  93. package/lib/typescript/module/ui/components/SurfaceScreen.d.ts +24 -0
  94. package/lib/typescript/module/ui/components/SurfaceScreen.d.ts.map +1 -0
  95. package/lib/typescript/module/ui/components/fileManagement/UploadPreview.d.ts +3 -10
  96. package/lib/typescript/module/ui/components/fileManagement/UploadPreview.d.ts.map +1 -1
  97. package/lib/typescript/module/ui/context/OxyContext.d.ts.map +1 -1
  98. package/lib/typescript/module/ui/navigation/bottomSheetManager.d.ts +24 -16
  99. package/lib/typescript/module/ui/navigation/bottomSheetManager.d.ts.map +1 -1
  100. package/lib/typescript/module/ui/navigation/routes.d.ts +2 -60
  101. package/lib/typescript/module/ui/navigation/routes.d.ts.map +1 -1
  102. package/lib/typescript/module/ui/navigation/surfaceNavStack.d.ts +51 -0
  103. package/lib/typescript/module/ui/navigation/surfaceNavStack.d.ts.map +1 -0
  104. package/lib/typescript/module/ui/navigation/surfaceRegistry.d.ts +117 -0
  105. package/lib/typescript/module/ui/navigation/surfaceRegistry.d.ts.map +1 -0
  106. package/lib/typescript/module/ui/navigation/surfaces.d.ts +79 -0
  107. package/lib/typescript/module/ui/navigation/surfaces.d.ts.map +1 -0
  108. package/lib/typescript/module/ui/screens/FileManagementScreen.d.ts.map +1 -1
  109. package/lib/typescript/module/ui/screens/fileManagement/PhotoPickerSection.d.ts +7 -0
  110. package/lib/typescript/module/ui/screens/fileManagement/PhotoPickerSection.d.ts.map +1 -1
  111. package/lib/typescript/module/ui/screens/fileManagement/hooks/useFileUploadState.d.ts +1 -1
  112. package/lib/typescript/module/ui/screens/fileManagement/hooks/useFileUploadState.d.ts.map +1 -1
  113. package/lib/typescript/module/ui/screens/fileManagement/shared.d.ts +4 -0
  114. package/lib/typescript/module/ui/screens/fileManagement/shared.d.ts.map +1 -1
  115. package/lib/typescript/module/ui/types/surfaceScreen.d.ts +38 -0
  116. package/lib/typescript/module/ui/types/surfaceScreen.d.ts.map +1 -0
  117. package/package.json +2 -2
  118. package/src/ui/components/OxyAccountDialog.tsx +32 -38
  119. package/src/ui/components/OxyProvider.tsx +12 -34
  120. package/src/ui/components/SurfaceScreen.tsx +166 -0
  121. package/src/ui/components/fileManagement/UploadPreview.tsx +8 -16
  122. package/src/ui/context/OxyContext.tsx +27 -1
  123. package/src/ui/navigation/__tests__/getSurfaceConfig.test.ts +39 -0
  124. package/src/ui/navigation/bottomSheetManager.ts +46 -73
  125. package/src/ui/navigation/routes.ts +7 -106
  126. package/src/ui/navigation/surfaceNavStack.ts +110 -0
  127. package/src/ui/navigation/surfaceRegistry.ts +174 -0
  128. package/src/ui/navigation/surfaces.ts +244 -0
  129. package/src/ui/screens/FileManagementScreen.tsx +8 -1
  130. package/src/ui/screens/fileManagement/PhotoPickerSection.tsx +69 -7
  131. package/src/ui/screens/fileManagement/hooks/useFileUploadState.ts +16 -18
  132. package/src/ui/screens/fileManagement/shared.ts +23 -0
  133. package/src/ui/types/surfaceScreen.ts +38 -0
  134. package/lib/commonjs/ui/components/BottomSheet.js +0 -49
  135. package/lib/commonjs/ui/components/BottomSheet.js.map +0 -1
  136. package/lib/commonjs/ui/components/BottomSheetRouter.js +0 -298
  137. package/lib/commonjs/ui/components/BottomSheetRouter.js.map +0 -1
  138. package/lib/module/ui/components/BottomSheet.js +0 -45
  139. package/lib/module/ui/components/BottomSheet.js.map +0 -1
  140. package/lib/module/ui/components/BottomSheetRouter.js +0 -292
  141. package/lib/module/ui/components/BottomSheetRouter.js.map +0 -1
  142. package/lib/typescript/commonjs/ui/components/BottomSheet.d.ts +0 -29
  143. package/lib/typescript/commonjs/ui/components/BottomSheet.d.ts.map +0 -1
  144. package/lib/typescript/commonjs/ui/components/BottomSheetRouter.d.ts +0 -9
  145. package/lib/typescript/commonjs/ui/components/BottomSheetRouter.d.ts.map +0 -1
  146. package/lib/typescript/module/ui/components/BottomSheet.d.ts +0 -29
  147. package/lib/typescript/module/ui/components/BottomSheet.d.ts.map +0 -1
  148. package/lib/typescript/module/ui/components/BottomSheetRouter.d.ts +0 -9
  149. package/lib/typescript/module/ui/components/BottomSheetRouter.d.ts.map +0 -1
  150. package/src/ui/components/BottomSheet.tsx +0 -48
  151. package/src/ui/components/BottomSheetRouter.tsx +0 -310
  152. package/src/ui/navigation/__tests__/getSheetConfig.test.ts +0 -36
@@ -0,0 +1,110 @@
1
+ import { createStore, type StoreApi } from 'zustand/vanilla';
2
+ import type { RouteName } from './routes';
3
+
4
+ /**
5
+ * Per-surface route history — the NAV-WITHIN axis.
6
+ *
7
+ * Bloom's surface stack owns DEPTH (a surface presented on top of another). This
8
+ * store owns the ORTHOGONAL axis: the route history INSIDE one SDK surface. Each
9
+ * SDK surface holds its own {@link SurfaceNavStack}; `navigate`/`replace`/`goBack`
10
+ * mutate it and the SAME Bloom surface re-renders its content from the new top
11
+ * frame. It is the per-surface reincarnation of the old global
12
+ * `bottomSheetStore`'s `history` + `currentStep`.
13
+ *
14
+ * `closing`/`closeResult` model a pending dismissal: the surface's `SurfaceScreen`
15
+ * watches them and calls the Bloom surface's `dismiss(result)` from an effect —
16
+ * so a dismiss requested BEFORE the surface has mounted still resolves once it
17
+ * does, with no controls-capture race and no render-phase side effect.
18
+ */
19
+
20
+ /** One frame in a surface's route history. */
21
+ export interface NavFrame {
22
+ route: RouteName;
23
+ props: Record<string, unknown>;
24
+ /** Current wizard step for step-based screens (seeded from `props.initialStep`). */
25
+ step: number;
26
+ }
27
+
28
+ export interface NavStackState {
29
+ /** Bottom → top; the last frame is the visible screen. */
30
+ frames: NavFrame[];
31
+ /** Flipped once a dismiss is requested; the host resolves the Bloom surface. */
32
+ closing: boolean;
33
+ /** The value the surface's `present()` promise resolves with on dismissal. */
34
+ closeResult: unknown;
35
+ }
36
+
37
+ export interface SurfaceNavStack {
38
+ store: StoreApi<NavStackState>;
39
+ /** Drill in: push a new frame. */
40
+ navigate(route: RouteName, props?: Record<string, unknown>): void;
41
+ /** Swap the top frame in place (no history entry). */
42
+ replace(route: RouteName, props?: Record<string, unknown>): void;
43
+ /** Pop one frame. Returns `false` when already at the root frame. */
44
+ goBack(): boolean;
45
+ /** Whether a back is possible (a deeper frame OR a prior wizard step). */
46
+ canGoBack(): boolean;
47
+ /** Set the current frame's wizard step. */
48
+ setStep(step: number): void;
49
+ /** Request dismissal of the whole surface, resolving its promise with `result`. */
50
+ requestDismiss(result?: unknown): void;
51
+ /** The current (top) frame. */
52
+ getTop(): NavFrame;
53
+ }
54
+
55
+ function makeFrame(route: RouteName, props?: Record<string, unknown>): NavFrame {
56
+ const resolved = props ?? {};
57
+ const step = typeof resolved.initialStep === 'number' ? resolved.initialStep : 0;
58
+ return { route, props: resolved, step };
59
+ }
60
+
61
+ export function createSurfaceNavStack(
62
+ route: RouteName,
63
+ props?: Record<string, unknown>,
64
+ ): SurfaceNavStack {
65
+ const store = createStore<NavStackState>(() => ({
66
+ frames: [makeFrame(route, props)],
67
+ closing: false,
68
+ closeResult: undefined,
69
+ }));
70
+
71
+ const getTop = (): NavFrame => {
72
+ const { frames } = store.getState();
73
+ return frames[frames.length - 1];
74
+ };
75
+
76
+ return {
77
+ store,
78
+ getTop,
79
+ navigate(nextRoute, nextProps) {
80
+ store.setState((state) => ({ frames: [...state.frames, makeFrame(nextRoute, nextProps)] }));
81
+ },
82
+ replace(nextRoute, nextProps) {
83
+ store.setState((state) => ({
84
+ frames: [...state.frames.slice(0, -1), makeFrame(nextRoute, nextProps)],
85
+ }));
86
+ },
87
+ goBack() {
88
+ const { frames } = store.getState();
89
+ if (frames.length <= 1) return false;
90
+ store.setState({ frames: frames.slice(0, -1) });
91
+ return true;
92
+ },
93
+ canGoBack() {
94
+ const { frames } = store.getState();
95
+ return frames.length > 1 || (frames[frames.length - 1]?.step ?? 0) > 0;
96
+ },
97
+ setStep(step) {
98
+ store.setState((state) => {
99
+ const frames = state.frames.slice();
100
+ const index = frames.length - 1;
101
+ frames[index] = { ...frames[index], step };
102
+ return { frames };
103
+ });
104
+ },
105
+ requestDismiss(result) {
106
+ if (store.getState().closing) return;
107
+ store.setState({ closing: true, closeResult: result });
108
+ },
109
+ };
110
+ }
@@ -0,0 +1,174 @@
1
+ import type { ReactNode } from 'react';
2
+ import type { AccountDialogView } from '@oxyhq/core';
3
+ import type { RouteName } from './routes';
4
+
5
+ /**
6
+ * The SDK's typed surface registry — the contract layer the SDK stacks on top of
7
+ * Bloom's content-agnostic surface stack (`@oxyhq/bloom/surfaces`). One entry per
8
+ * route describing its `props` (what the presenter must pass) and its `result`
9
+ * (what awaiting `present()` resolves with). Hand-maintained, contract-first:
10
+ * adding a route here is the single edit that types it end to end.
11
+ *
12
+ * For P1 most routes keep permissive `Record<string, unknown>` props + a `void`
13
+ * result — the historical grab-bag `showBottomSheet`/`navigate` props are
14
+ * unchanged. Later phases tighten individual routes without changing this shape.
15
+ */
16
+
17
+ /** One registry entry: the props a route is presented with + its dismissal result. */
18
+ type SurfaceRoute<Props = Record<string, unknown>, Result = void> = {
19
+ props: Props;
20
+ result: Result;
21
+ };
22
+
23
+ export interface SurfaceRegistry {
24
+ ManageAccount: SurfaceRoute;
25
+ AccountVerification: SurfaceRoute;
26
+ PaymentGateway: SurfaceRoute;
27
+ Profile: SurfaceRoute;
28
+ LanguageSelector: SurfaceRoute;
29
+ PrivacySettings: SurfaceRoute;
30
+ SearchSettings: SurfaceRoute;
31
+ FileManagement: SurfaceRoute;
32
+ HelpSupport: SurfaceRoute;
33
+ FAQ: SurfaceRoute;
34
+ Feedback: SurfaceRoute;
35
+ LegalDocuments: SurfaceRoute;
36
+ AppInfo: SurfaceRoute;
37
+ PremiumSubscription: SurfaceRoute;
38
+ WelcomeNewUser: SurfaceRoute;
39
+ UserLinks: SurfaceRoute;
40
+ HistoryView: SurfaceRoute;
41
+ SavesCollections: SurfaceRoute;
42
+ EditProfile: SurfaceRoute;
43
+ EditProfileField: SurfaceRoute;
44
+ LearnMoreUsernames: SurfaceRoute;
45
+ TrustCenter: SurfaceRoute;
46
+ TrustLeaderboard: SurfaceRoute;
47
+ TrustRewards: SurfaceRoute;
48
+ TrustRules: SurfaceRoute;
49
+ AboutTrust: SurfaceRoute;
50
+ TrustFAQ: SurfaceRoute;
51
+ FollowersList: SurfaceRoute;
52
+ FollowingList: SurfaceRoute;
53
+ CreateAccount: SurfaceRoute;
54
+ AccountMembers: SurfaceRoute;
55
+ AccountSettings: SurfaceRoute;
56
+ AvatarCrop: SurfaceRoute;
57
+ Notifications: SurfaceRoute;
58
+ ConnectedApps: SurfaceRoute;
59
+ Preferences: SurfaceRoute;
60
+ /** Unified account switcher + sign-in surface (folded `OxyAccountDialog` body). */
61
+ AccountDialog: SurfaceRoute<{ initialView?: AccountDialogView }>;
62
+ }
63
+
64
+ /**
65
+ * Compile-time guard: the registry keys must be EXACTLY the route names. If a new
66
+ * `RouteName` is added without a registry entry (or vice-versa) this alias
67
+ * resolves to `never` and the `satisfies` assertion below fails to type-check.
68
+ */
69
+ type RegistryCoversRoutes = [RouteName] extends [keyof SurfaceRegistry]
70
+ ? [keyof SurfaceRegistry] extends [RouteName]
71
+ ? true
72
+ : never
73
+ : never;
74
+ const _registryCoversRoutes: RegistryCoversRoutes = true;
75
+ void _registryCoversRoutes;
76
+
77
+ /** Typed props a route is presented with. */
78
+ export type SurfaceProps<K extends RouteName> = SurfaceRegistry[K]['props'];
79
+ /** What awaiting a route's `present()` resolves with when it is dismissed. */
80
+ export type SurfaceResult<K extends RouteName> = SurfaceRegistry[K]['result'];
81
+
82
+ /**
83
+ * How a surface renders — the SDK's presentation taxonomy. Each value maps to a
84
+ * concrete Bloom `Dialog` placement/chrome in `surfaces.ts`:
85
+ *
86
+ * - `'sheet'` → responsive `{ base: 'bottom', md: 'center' }` (the default:
87
+ * a bottom sheet on narrow viewports, a centered card on wide).
88
+ * - `'center'` → a plain centered modal at every width.
89
+ * - `'drawer'` → responsive `{ base: 'bottom', md: 'left' }` side drawer.
90
+ * - `'fullScreen'` → approximated with the shared `Dialog`: a full-height sheet /
91
+ * large centered card, flush (`contentPadding: 0`) content, a
92
+ * black canvas and programmatic-only dismiss (the flagship
93
+ * image picker). A real Bloom `'fullScreen'` placement is a
94
+ * follow-up if the approximation reads wrong on device.
95
+ */
96
+ export type SurfacePresentation = 'sheet' | 'center' | 'drawer' | 'fullScreen';
97
+
98
+ /**
99
+ * Per-route surface configuration. Merges the historical `SheetRouteConfig`
100
+ * knobs with the new {@link SurfacePresentation}.
101
+ *
102
+ * NOTE: `scrollable` / `manualActivation` / `dynamicBackdrop` / `handleComponent`
103
+ * describe the in-tree bottom-sheet's pan + scroll behaviour. On Bloom's `Dialog`
104
+ * surface those concerns are now owned by the shared `DialogBottomSheet` (it opts
105
+ * pure custom children OUT of its internal ScrollView and manages the pan itself),
106
+ * so for P1 these fields are informational — reserved for a future Bloom
107
+ * bottom-placement passthrough. `presentation` (+ `scrollable` for the picker's
108
+ * flush layout) is what actually drives the surface today.
109
+ */
110
+ export interface SurfaceRouteConfig {
111
+ /** Which surface hosts the route. */
112
+ presentation: SurfacePresentation;
113
+ /**
114
+ * When `false`, the route owns its own scroll container (a FlatList /
115
+ * SectionList / VirtualizedList) and the host must not wrap it in a ScrollView.
116
+ */
117
+ scrollable: boolean;
118
+ /** Body-pan activation strategy — reserved (owned by Bloom's Dialog surface). */
119
+ manualActivation: boolean;
120
+ /** Drag-proportional backdrop dim — reserved (owned by Bloom's Dialog surface). */
121
+ dynamicBackdrop: boolean;
122
+ /** Optional custom drag-handle slot — reserved (owned by Bloom's Dialog surface). */
123
+ handleComponent?: () => ReactNode;
124
+ }
125
+
126
+ /** Defaults shared across all routes — the canonical responsive "sheet". */
127
+ const DEFAULT_SURFACE_CONFIG: SurfaceRouteConfig = {
128
+ presentation: 'sheet',
129
+ scrollable: true,
130
+ manualActivation: true,
131
+ dynamicBackdrop: true,
132
+ };
133
+
134
+ /**
135
+ * Predicate matching `FileManagementScreen`'s internal `isImageOnlyPicker`
136
+ * derivation. When the consumer restricts to image MIME types (no videos, no
137
+ * audio, no documents), FileManagement renders the flagship `PhotoPickerView`,
138
+ * which owns its own FlatList and reads best as a full-bleed picker. Kept in sync
139
+ * with `FileManagementScreen` — both check the same disabled MIME type families.
140
+ */
141
+ const isFileManagementImageOnlyPicker = (props: Record<string, unknown>): boolean => {
142
+ if (!props.selectMode) return false;
143
+ const disabled = props.disabledMimeTypes;
144
+ if (!Array.isArray(disabled) || disabled.length === 0) return false;
145
+ const has = (predicate: (mt: string) => boolean): boolean =>
146
+ disabled.some((mt) => typeof mt === 'string' && predicate(mt));
147
+ const blocksVideos = has((mt) => mt === 'video/' || mt.startsWith('video/'));
148
+ const blocksAudio = has((mt) => mt === 'audio/' || mt.startsWith('audio/'));
149
+ const blocksDocs = has(
150
+ (mt) => mt === 'application/pdf' || mt === 'application/' || mt.startsWith('application/'),
151
+ );
152
+ return blocksVideos && blocksAudio && blocksDocs;
153
+ };
154
+
155
+ /**
156
+ * Resolve the surface configuration for a route + props. Defaults to the
157
+ * responsive sheet; the image-only FileManagement picker upgrades to a full-bleed
158
+ * `'fullScreen'` surface that owns its own scrolling.
159
+ */
160
+ export const getSurfaceConfig = (
161
+ route: RouteName,
162
+ props: Record<string, unknown>,
163
+ ): SurfaceRouteConfig => {
164
+ if (route === 'FileManagement' && isFileManagementImageOnlyPicker(props)) {
165
+ return { ...DEFAULT_SURFACE_CONFIG, presentation: 'fullScreen', scrollable: false };
166
+ }
167
+ return DEFAULT_SURFACE_CONFIG;
168
+ };
169
+
170
+ /** Convenience: just the presentation taxonomy for a route + props. */
171
+ export const getSurfacePresentation = (
172
+ route: RouteName,
173
+ props: Record<string, unknown>,
174
+ ): SurfacePresentation => getSurfaceConfig(route, props).presentation;
@@ -0,0 +1,244 @@
1
+ import { createElement } from 'react';
2
+ import type { ViewStyle } from 'react-native';
3
+ import {
4
+ surfaces as bloomSurfaces,
5
+ type PresentOptions,
6
+ type SurfaceControls,
7
+ } from '@oxyhq/bloom/surfaces';
8
+ import SurfaceScreen from '../components/SurfaceScreen';
9
+ import type { RouteName } from './routes';
10
+ import {
11
+ getSurfaceConfig,
12
+ getSurfacePresentation,
13
+ type SurfacePresentation,
14
+ type SurfaceProps,
15
+ type SurfaceResult,
16
+ type SurfaceRouteConfig,
17
+ } from './surfaceRegistry';
18
+ import { createSurfaceNavStack, type SurfaceNavStack } from './surfaceNavStack';
19
+
20
+ /**
21
+ * The SDK's typed surface API — a route registry layered on top of Bloom's
22
+ * content-agnostic surface stack (`@oxyhq/bloom/surfaces`).
23
+ *
24
+ * `present(route)` opens a NEW Bloom surface (the DEPTH axis) whose content is a
25
+ * {@link SurfaceScreen} driven by the route's own {@link SurfaceNavStack} (the
26
+ * NAV-WITHIN axis). Navigating within that surface mutates its nav stack and
27
+ * re-renders the SAME Bloom surface; presenting stacks another surface on top.
28
+ *
29
+ * Bloom coordinates z-order / backdrop / dismiss across every surface (SDK's and
30
+ * apps'), so nothing clashes.
31
+ */
32
+
33
+ /** Responsive "sheet": bottom sheet on narrow, centered card on wide. */
34
+ const SHEET_PLACEMENT = { base: 'bottom', md: 'center' } as const;
35
+
36
+ /** Full-bleed black canvas for the `'fullScreen'` approximation (image picker). */
37
+ const FULLSCREEN_SURFACE_STYLE: ViewStyle = { backgroundColor: '#000000', overflow: 'hidden' };
38
+
39
+ /** Map a route's presentation taxonomy onto concrete Bloom `Dialog` options. */
40
+ function bloomOptionsFor(config: SurfaceRouteConfig): PresentOptions {
41
+ switch (config.presentation) {
42
+ case 'center':
43
+ return { placement: 'center' };
44
+ case 'drawer':
45
+ return { placement: { base: 'bottom', md: 'left' } };
46
+ case 'fullScreen':
47
+ // Approximate a full-screen surface with the shared `Dialog`: a tall
48
+ // sheet / large centered card, flush content, a black canvas and
49
+ // programmatic-only dismiss — matching the shipped image-picker Dialog.
50
+ return {
51
+ placement: SHEET_PLACEMENT,
52
+ contentPadding: 0,
53
+ maxWidth: 640,
54
+ maxHeightRatio: 0.9,
55
+ dismissOnBackdrop: false,
56
+ style: FULLSCREEN_SURFACE_STYLE,
57
+ panelStyle: FULLSCREEN_SURFACE_STYLE,
58
+ };
59
+ default:
60
+ return { placement: SHEET_PLACEMENT };
61
+ }
62
+ }
63
+
64
+ /**
65
+ * A live handle to a presented SDK surface. Exposes DEPTH controls (`dismiss`,
66
+ * `result`) and NAV-WITHIN controls (`navigate`/`replace`/`goBack`/`setStep`) that
67
+ * drive this surface's own nav stack. Held by the `showBottomSheet` adapter (base
68
+ * surface) and by `OxyContext` (the AccountDialog surface).
69
+ */
70
+ export interface SurfaceInstance<K extends RouteName = RouteName> {
71
+ readonly route: RouteName;
72
+ readonly presentation: SurfacePresentation;
73
+ navigate: (route: RouteName, props?: Record<string, unknown>) => void;
74
+ replace: (route: RouteName, props?: Record<string, unknown>) => void;
75
+ goBack: () => boolean;
76
+ canGoBack: () => boolean;
77
+ setStep: (step: number) => void;
78
+ dismiss: (result?: SurfaceResult<K>) => void;
79
+ readonly result: Promise<SurfaceResult<K> | undefined>;
80
+ }
81
+
82
+ /**
83
+ * The type-erased handle the `showBottomSheet` adapter needs from a tracked
84
+ * surface: what it hosts, and how to drive/dismiss it. Kept result-type-free so
85
+ * the (generic) `SurfaceInstance<K>` never has to be widened into a shared array.
86
+ */
87
+ interface TrackedSurface {
88
+ readonly route: RouteName;
89
+ readonly presentation: SurfacePresentation;
90
+ navigate: (route: RouteName, props?: Record<string, unknown>) => void;
91
+ goBack: () => boolean;
92
+ dismiss: () => void;
93
+ }
94
+
95
+ /**
96
+ * Active SDK "route" surfaces (the `showBottomSheet` lineage), bottom → top.
97
+ * `present`/`presentRoute` register here; `presentDetached` (AccountDialog) does
98
+ * NOT, so the base-sheet navigation logic never treats the account modal as a
99
+ * navigable base. Entries self-remove when their surface is dismissed.
100
+ */
101
+ const activeRouteSurfaces: TrackedSurface[] = [];
102
+
103
+ function presentInternal<K extends RouteName>(
104
+ route: K,
105
+ props: Record<string, unknown>,
106
+ opts: PresentOptions | undefined,
107
+ track: boolean,
108
+ ): SurfaceInstance<K> {
109
+ const navStack = createSurfaceNavStack(route, props);
110
+ const config = getSurfaceConfig(route, props);
111
+ const bloomOpts: PresentOptions = { label: route, ...bloomOptionsFor(config), ...opts };
112
+
113
+ const result = bloomSurfaces.present<SurfaceResult<K>>((surface: SurfaceControls) =>
114
+ createElement(SurfaceScreen, { navStack, surface, presentation: config.presentation }),
115
+ bloomOpts,
116
+ );
117
+
118
+ const instance: SurfaceInstance<K> = {
119
+ route,
120
+ presentation: config.presentation,
121
+ navigate: (nextRoute, nextProps) => navStack.navigate(nextRoute, nextProps),
122
+ replace: (nextRoute, nextProps) => navStack.replace(nextRoute, nextProps),
123
+ goBack: () => navStack.goBack(),
124
+ canGoBack: () => navStack.canGoBack(),
125
+ setStep: (step) => navStack.setStep(step),
126
+ dismiss: (dismissResult) => navStack.requestDismiss(dismissResult),
127
+ result,
128
+ };
129
+
130
+ if (track) {
131
+ const tracked: TrackedSurface = {
132
+ route: instance.route,
133
+ presentation: instance.presentation,
134
+ navigate: instance.navigate,
135
+ goBack: instance.goBack,
136
+ dismiss: () => instance.dismiss(),
137
+ };
138
+ activeRouteSurfaces.push(tracked);
139
+ result.finally(() => {
140
+ const index = activeRouteSurfaces.indexOf(tracked);
141
+ if (index >= 0) activeRouteSurfaces.splice(index, 1);
142
+ });
143
+ }
144
+
145
+ return instance;
146
+ }
147
+
148
+ /**
149
+ * Present a NEW route surface on top of the stack (the DEPTH axis). Resolves with
150
+ * the value the surface is dismissed with. Registered in the route-surface stack.
151
+ */
152
+ export function present<K extends RouteName>(
153
+ route: K,
154
+ props?: SurfaceProps<K>,
155
+ opts?: PresentOptions,
156
+ ): SurfaceInstance<K> {
157
+ return presentInternal(route, (props ?? {}) as Record<string, unknown>, opts, true);
158
+ }
159
+
160
+ /**
161
+ * Present a surface that is NOT part of the `showBottomSheet` base-navigation
162
+ * lineage (the AccountDialog modal). Otherwise identical to {@link present}.
163
+ */
164
+ export function presentDetached<K extends RouteName>(
165
+ route: K,
166
+ props?: SurfaceProps<K>,
167
+ opts?: PresentOptions,
168
+ ): SurfaceInstance<K> {
169
+ return presentInternal(route, (props ?? {}) as Record<string, unknown>, opts, false);
170
+ }
171
+
172
+ /**
173
+ * Untyped present for the adapter, the nav helpers, and the `SurfaceScreen`
174
+ * binding (where the route is a runtime value and its props are already erased).
175
+ */
176
+ export function presentRoute(route: RouteName, props?: Record<string, unknown>): SurfaceInstance {
177
+ return presentInternal(route, props ?? {}, undefined, true);
178
+ }
179
+
180
+ /** The top-most active route surface, or `undefined` when none are open. */
181
+ export function topRouteSurface(): TrackedSurface | undefined {
182
+ return activeRouteSurfaces[activeRouteSurfaces.length - 1];
183
+ }
184
+
185
+ /**
186
+ * Dismiss every active route surface (the whole `showBottomSheet` session). The
187
+ * AccountDialog is untracked, so it is untouched. Iterates a snapshot because
188
+ * `dismiss` mutates `activeRouteSurfaces` asynchronously as surfaces settle.
189
+ */
190
+ export function closeAllRouteSurfaces(): void {
191
+ for (const instance of [...activeRouteSurfaces].reverse()) instance.dismiss();
192
+ }
193
+
194
+ /**
195
+ * Drill in from a screen: navigate WITHIN the current surface when the target's
196
+ * presentation matches, else present the target as a NEW surface on top (so a
197
+ * sheet screen opening the full-bleed picker gets the right chrome).
198
+ */
199
+ export function navigateWithinOrPresent(
200
+ currentPresentation: SurfacePresentation,
201
+ navStack: SurfaceNavStack,
202
+ route: RouteName,
203
+ props?: Record<string, unknown>,
204
+ ): void {
205
+ const nextProps = props ?? {};
206
+ if (getSurfacePresentation(route, nextProps) === currentPresentation) {
207
+ navStack.navigate(route, nextProps);
208
+ } else {
209
+ presentRoute(route, nextProps);
210
+ }
211
+ }
212
+
213
+ /** Replace variant of {@link navigateWithinOrPresent}. */
214
+ export function replaceWithinOrPresent(
215
+ currentPresentation: SurfacePresentation,
216
+ navStack: SurfaceNavStack,
217
+ route: RouteName,
218
+ props?: Record<string, unknown>,
219
+ ): void {
220
+ const nextProps = props ?? {};
221
+ if (getSurfacePresentation(route, nextProps) === currentPresentation) {
222
+ navStack.replace(route, nextProps);
223
+ } else {
224
+ presentRoute(route, nextProps);
225
+ }
226
+ }
227
+
228
+ /**
229
+ * The SDK's imperative surface API (a module singleton). `present` is the typed
230
+ * route presenter; `dismissAll` unwinds the whole session. Not yet exported from
231
+ * the package root — the public `surfaces`/`useSurfaces` API lands in a later
232
+ * phase; for P1 the internal adapters (`showBottomSheet`, `openAccountDialog`)
233
+ * are the only public entry points.
234
+ */
235
+ export const surfaces = {
236
+ present: <K extends RouteName>(route: K, props?: SurfaceProps<K>, opts?: PresentOptions) =>
237
+ present(route, props, opts).result,
238
+ dismissAll: closeAllRouteSurfaces,
239
+ } as const;
240
+
241
+ /** Hook form of {@link surfaces} (stable singleton). */
242
+ export function useSurfaces(): typeof surfaces {
243
+ return surfaces;
244
+ }
@@ -1123,7 +1123,13 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
1123
1123
  // this is pure layout: the top chrome (header / controls / search) plus a
1124
1124
  // photo grid of shimmering tiles sized with the same 3-per-row geometry the
1125
1125
  // real photo grid uses. No hand-rolled `Animated` shimmer.
1126
- if (loading) {
1126
+ //
1127
+ // The image-only picker owns its OWN loading skeleton (rendered by
1128
+ // `PhotoPickerView` when its photo list is still loading), so it inherits the
1129
+ // picker's placement-aware container width / columns / tile size and the
1130
+ // Dialog-vs-bottom-sheet sizing. Skipping the browse chrome here is what keeps
1131
+ // the picker from flashing the wrong (file-manager) skeleton inside the Dialog.
1132
+ if (loading && !isImageOnlyPicker) {
1127
1133
  const GRID_PADDING = 10;
1128
1134
  const TILE_GAP = 4;
1129
1135
  const GRID_COLUMNS = 3;
@@ -1197,6 +1203,7 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
1197
1203
  uploadProgress={uploadProgress}
1198
1204
  hasMore={paging.hasMore}
1199
1205
  loadingMore={paging.loadingMore}
1206
+ loading={loading}
1200
1207
  reduceMotion={reduceMotion}
1201
1208
  getThumbUrl={thumbSourceFor}
1202
1209
  primaryColor={colors.primary}
@@ -14,6 +14,7 @@ import {
14
14
  import { Image as ExpoImage } from 'expo-image';
15
15
  import { Ionicons, MaterialCommunityIcons } from '@expo/vector-icons';
16
16
  import { BREAKPOINTS } from '@oxyhq/bloom/styles';
17
+ import * as Skeleton from '@oxyhq/bloom/skeleton';
17
18
  import type { FileMetadata } from '@oxyhq/core';
18
19
  import { computePhotoGridLayout } from './photoGridLayout';
19
20
 
@@ -122,6 +123,13 @@ export interface PhotoPickerViewProps {
122
123
  uploadProgress: { current: number; total: number } | null;
123
124
  hasMore: boolean;
124
125
  loadingMore: boolean;
126
+ /**
127
+ * Initial-load flag. While `true` AND no photos have arrived yet, the picker
128
+ * renders its OWN loading skeleton (placement-aware grid of shimmer tiles)
129
+ * instead of the empty state — so the loading UI matches the picker's shape
130
+ * and container width, not the browse file-manager chrome.
131
+ */
132
+ loading: boolean;
125
133
  reduceMotion: boolean;
126
134
  getThumbUrl: (file: FileMetadata) => string | undefined;
127
135
  primaryColor: string;
@@ -161,6 +169,18 @@ const RING_SPRING_TENSION = 120;
161
169
  const CELL_CORNER_RADIUS = 8; // `rounded-radius-8`
162
170
  const SELECTION_RING_WIDTH = 3; // `border-[3px]`
163
171
 
172
+ /**
173
+ * Loading-skeleton tile fill. The picker's backdrop is ALWAYS black regardless
174
+ * of theme, so we cannot use Bloom `Skeleton.Box`'s theme default (`contrast50`,
175
+ * which is a near-black gray on a dark theme → invisible on this backdrop). A
176
+ * neutral dark gray reads as a photo placeholder on black; Bloom's shimmer still
177
+ * pulses the tile opacity on top of it. `Skeleton.Box` applies `style` after its
178
+ * own base, so this override wins while the animated opacity is preserved.
179
+ */
180
+ const SKELETON_TILE_COLOR = '#26262A';
181
+ /** Minimum skeleton rows so the grid never looks empty on a very short panel. */
182
+ const SKELETON_MIN_ROWS = 4;
183
+
164
184
  type PhotoPickerCellProps = {
165
185
  photo: FileMetadata;
166
186
  size: number;
@@ -325,6 +345,7 @@ const PhotoPickerView: React.FC<PhotoPickerViewProps> = ({
325
345
  uploadProgress,
326
346
  hasMore,
327
347
  loadingMore,
348
+ loading,
328
349
  reduceMotion,
329
350
  getThumbUrl,
330
351
  primaryColor,
@@ -366,14 +387,13 @@ const PhotoPickerView: React.FC<PhotoPickerViewProps> = ({
366
387
  // better below full height); the bottom sheet may run taller. Keep this
367
388
  // breakpoint in sync with `DIALOG_PLACEMENT` in BottomSheetRouter.
368
389
  const isCenteredPanel = windowWidth >= BREAKPOINTS.md;
369
- const rootStyle = useMemo(
370
- () => ({
371
- maxHeight: isCenteredPanel
372
- ? Math.min(Math.round(windowHeight * 0.85), 720)
373
- : Math.round(windowHeight * 0.9),
374
- }),
390
+ const maxPanelHeight = useMemo(
391
+ () => (isCenteredPanel
392
+ ? Math.min(Math.round(windowHeight * 0.85), 720)
393
+ : Math.round(windowHeight * 0.9)),
375
394
  [isCenteredPanel, windowHeight],
376
395
  );
396
+ const rootStyle = useMemo(() => ({ maxHeight: maxPanelHeight }), [maxPanelHeight]);
377
397
 
378
398
  const effectiveWidth = gridWidth > 0 ? gridWidth : windowWidth;
379
399
  const { columns, cellSize, gutter } = useMemo(
@@ -381,6 +401,18 @@ const PhotoPickerView: React.FC<PhotoPickerViewProps> = ({
381
401
  [effectiveWidth],
382
402
  );
383
403
 
404
+ // How many shimmer tiles the loading skeleton renders: enough FULL rows to
405
+ // fill the panel down to (but never past) its `maxHeight` cap, so the panel
406
+ // hugs the skeleton content instead of overflowing or scrolling. `floor`
407
+ // keeps the last row whole; `SKELETON_MIN_ROWS` guards a very short panel.
408
+ const skeletonTileCount = useMemo(() => {
409
+ const rowHeight = cellSize + gutter;
410
+ if (rowHeight <= 0) return columns * SKELETON_MIN_ROWS;
411
+ const available = Math.max(0, maxPanelHeight - GRID_CONTENT_PADDING_TOP);
412
+ const rows = Math.max(SKELETON_MIN_ROWS, Math.floor(available / rowHeight));
413
+ return rows * columns;
414
+ }, [cellSize, gutter, columns, maxPanelHeight]);
415
+
384
416
  // Map selectedIds → 1-based selection order for the badge. We freeze a
385
417
  // stable order at the time of selection: the order is the insertion
386
418
  // order of the Set (which JS preserves natively).
@@ -515,7 +547,37 @@ const PhotoPickerView: React.FC<PhotoPickerViewProps> = ({
515
547
  where Yoga already defaults min-height to 0). */}
516
548
  <View className="flex-1 min-h-0 bg-black">
517
549
  {/* Photo grid (renders behind the translucent header). */}
518
- {isEmpty ? (
550
+ {isEmpty && loading ? (
551
+ /* Loading skeleton — the picker's OWN shape: shimmer tiles laid
552
+ out with the SAME placement-aware geometry the real grid uses
553
+ (`computePhotoGridLayout(effectiveWidth)`), pushed below the
554
+ translucent header by the grid content padding. `flexWrap`
555
+ breaks rows exactly at `columns` because the last tile in each
556
+ row carries no right margin (a full row + gutters fits the
557
+ measured width by construction). The real translucent header
558
+ still floats on top, so Cancel stays reachable while loading. */
559
+ <View
560
+ className="pt-[60px]"
561
+ style={{ flexDirection: 'row', flexWrap: 'wrap' }}
562
+ pointerEvents="none"
563
+ accessibilityElementsHidden
564
+ importantForAccessibility="no-hide-descendants"
565
+ >
566
+ {Array.from({ length: skeletonTileCount }, (_, index) => (
567
+ <Skeleton.Box
568
+ key={index}
569
+ width={cellSize}
570
+ height={cellSize}
571
+ borderRadius={CELL_CORNER_RADIUS}
572
+ style={{
573
+ marginRight: (index + 1) % columns === 0 ? 0 : gutter,
574
+ marginBottom: gutter,
575
+ backgroundColor: SKELETON_TILE_COLOR,
576
+ }}
577
+ />
578
+ ))}
579
+ </View>
580
+ ) : isEmpty ? (
519
581
  <View className="flex-1 items-center justify-center px-space-32 pt-[60px]">
520
582
  <View className="opacity-30 mb-space-16">
521
583
  <MaterialCommunityIcons name="image-outline" size={64} color="#FFFFFF" />