@oxyhq/services 22.8.4 → 22.8.6
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.
- package/lib/commonjs/ui/components/BottomSheetRouter.js +68 -23
- package/lib/commonjs/ui/components/BottomSheetRouter.js.map +1 -1
- package/lib/commonjs/ui/components/ProfileButton.js +4 -10
- package/lib/commonjs/ui/components/ProfileButton.js.map +1 -1
- package/lib/commonjs/ui/components/fileManagement/UploadPreview.js +1 -1
- package/lib/commonjs/ui/components/fileManagement/UploadPreview.js.map +1 -1
- package/lib/commonjs/ui/navigation/routes.js +7 -4
- package/lib/commonjs/ui/navigation/routes.js.map +1 -1
- package/lib/commonjs/ui/screens/FileManagementScreen.js +49 -173
- package/lib/commonjs/ui/screens/FileManagementScreen.js.map +1 -1
- package/lib/commonjs/ui/screens/fileManagement/FileListSection.js +3 -0
- package/lib/commonjs/ui/screens/fileManagement/FileListSection.js.map +1 -1
- package/lib/commonjs/ui/screens/fileManagement/PhotoPickerSection.js +111 -217
- package/lib/commonjs/ui/screens/fileManagement/PhotoPickerSection.js.map +1 -1
- package/lib/module/ui/components/BottomSheetRouter.js +69 -24
- package/lib/module/ui/components/BottomSheetRouter.js.map +1 -1
- package/lib/module/ui/components/ProfileButton.js +3 -10
- package/lib/module/ui/components/ProfileButton.js.map +1 -1
- package/lib/module/ui/components/fileManagement/UploadPreview.js +1 -1
- package/lib/module/ui/components/fileManagement/UploadPreview.js.map +1 -1
- package/lib/module/ui/navigation/routes.js +7 -4
- package/lib/module/ui/navigation/routes.js.map +1 -1
- package/lib/module/ui/screens/FileManagementScreen.js +50 -174
- package/lib/module/ui/screens/FileManagementScreen.js.map +1 -1
- package/lib/module/ui/screens/fileManagement/FileListSection.js +3 -0
- package/lib/module/ui/screens/fileManagement/FileListSection.js.map +1 -1
- package/lib/module/ui/screens/fileManagement/PhotoPickerSection.js +112 -218
- package/lib/module/ui/screens/fileManagement/PhotoPickerSection.js.map +1 -1
- package/lib/typescript/commonjs/ui/components/BottomSheetRouter.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/ProfileButton.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/navigation/routes.d.ts +12 -0
- package/lib/typescript/commonjs/ui/navigation/routes.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/screens/FileManagementScreen.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/screens/fileManagement/FileListSection.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/screens/fileManagement/PhotoPickerSection.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/BottomSheetRouter.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/ProfileButton.d.ts.map +1 -1
- package/lib/typescript/module/ui/navigation/routes.d.ts +12 -0
- package/lib/typescript/module/ui/navigation/routes.d.ts.map +1 -1
- package/lib/typescript/module/ui/screens/FileManagementScreen.d.ts.map +1 -1
- package/lib/typescript/module/ui/screens/fileManagement/FileListSection.d.ts.map +1 -1
- package/lib/typescript/module/ui/screens/fileManagement/PhotoPickerSection.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/ui/components/BottomSheetRouter.tsx +81 -28
- package/src/ui/components/ProfileButton.tsx +2 -14
- package/src/ui/components/fileManagement/UploadPreview.tsx +1 -1
- package/src/ui/navigation/__tests__/getSheetConfig.test.ts +36 -0
- package/src/ui/navigation/routes.ts +19 -4
- package/src/ui/screens/FileManagementScreen.tsx +36 -174
- package/src/ui/screens/fileManagement/FileListSection.tsx +1 -0
- package/src/ui/screens/fileManagement/PhotoPickerSection.tsx +127 -223
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ProfileButton.d.ts","sourceRoot":"","sources":["../../../../../src/ui/components/ProfileButton.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAE/B,OAAO,EAKH,KAAK,SAAS,EACd,KAAK,SAAS,EACjB,MAAM,cAAc,CAAC;
|
|
1
|
+
{"version":3,"file":"ProfileButton.d.ts","sourceRoot":"","sources":["../../../../../src/ui/components/ProfileButton.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAE/B,OAAO,EAKH,KAAK,SAAS,EACd,KAAK,SAAS,EACjB,MAAM,cAAc,CAAC;AAwCtB,MAAM,WAAW,kBAAkB;IAC/B;;;OAGG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB;;OAEG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,2DAA2D;IAC3D,gBAAgB,CAAC,EAAE,MAAM,IAAI,CAAC;IAC9B,sEAAsE;IACtE,YAAY,CAAC,EAAE,MAAM,IAAI,CAAC;IAC1B,8DAA8D;IAC9D,iBAAiB,CAAC,EAAE,MAAM,IAAI,CAAC;IAC/B;;;;OAIG;IACH,SAAS,CAAC,EAAE,IAAI,GAAG,MAAM,GAAG,MAAM,CAAC;IACnC;;;;OAIG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,gDAAgD;IAChD,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;CAChC;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,QAAA,MAAM,aAAa,EAAE,KAAK,CAAC,EAAE,CAAC,kBAAkB,CA+O/C,CAAC;AAiDF,eAAe,aAAa,CAAC"}
|
|
@@ -9,11 +9,23 @@ export declare const isValidRoute: (routeName: string) => routeName is RouteName
|
|
|
9
9
|
* remove fields without bumping the consumer surface as a breaking change.
|
|
10
10
|
*/
|
|
11
11
|
export interface SheetRouteConfig {
|
|
12
|
+
/**
|
|
13
|
+
* Which surface hosts the route. `'sheet'` (default) renders the screen in
|
|
14
|
+
* the in-tree `BottomSheet`. `'dialog'` renders it in a responsive Bloom
|
|
15
|
+
* `<Dialog>` (bottom-sheet on narrow, centered card on `md+`) — used for the
|
|
16
|
+
* flagship image picker, which reads better as a centered panel on wide
|
|
17
|
+
* viewports than a full-width sheet. `BottomSheetRouter` keeps BOTH surfaces
|
|
18
|
+
* mounted and routes the active screen into exactly one based on this value.
|
|
19
|
+
*/
|
|
20
|
+
presentation: 'sheet' | 'dialog';
|
|
12
21
|
/**
|
|
13
22
|
* When `false`, BottomSheet skips its internal ScrollView and lets the
|
|
14
23
|
* screen own scrolling. Required for screens that render a FlatList,
|
|
15
24
|
* SectionList, or any other VirtualizedList — nesting one inside a
|
|
16
25
|
* plain ScrollView breaks windowing and triggers a RN warning.
|
|
26
|
+
*
|
|
27
|
+
* Ignored for `presentation: 'dialog'` routes — those don't render in the
|
|
28
|
+
* in-tree `BottomSheet` at all.
|
|
17
29
|
*/
|
|
18
30
|
scrollable: boolean;
|
|
19
31
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"routes.d.ts","sourceRoot":"","sources":["../../../../../src/ui/navigation/routes.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AACtD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,wBAAqB,CAAC;AAO3D,MAAM,MAAM,SAAS,GACf,eAAe,GACf,qBAAqB,GACrB,gBAAgB,GAChB,SAAS,GACT,kBAAkB,GAClB,iBAAiB,GACjB,gBAAgB,GAChB,gBAAgB,GAChB,aAAa,GACb,KAAK,GACL,UAAU,GACV,gBAAgB,GAChB,SAAS,GACT,qBAAqB,GACrB,gBAAgB,GAChB,WAAW,GACX,aAAa,GACb,kBAAkB,GAClB,aAAa,GACb,kBAAkB,GAClB,oBAAoB,GACpB,aAAa,GACb,kBAAkB,GAClB,cAAc,GACd,YAAY,GACZ,YAAY,GACZ,UAAU,GACV,eAAe,GACf,eAAe,GACf,eAAe,GACf,gBAAgB,GAChB,iBAAiB,GACjB,YAAY,GACZ,eAAe,GACf,eAAe,GACf,aAAa,CAAC;AAkDpB,eAAO,MAAM,kBAAkB,GAAI,WAAW,SAAS,KAAG,aAAa,CAAC,eAAe,CAAC,GAAG,SAsB1F,CAAC;AAIF,eAAO,MAAM,YAAY,GAAI,WAAW,MAAM,KAAG,SAAS,IAAI,SAE7D,CAAC;AAEF;;;;GAIG;AACH,MAAM,WAAW,gBAAgB;IAC7B
|
|
1
|
+
{"version":3,"file":"routes.d.ts","sourceRoot":"","sources":["../../../../../src/ui/navigation/routes.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AACtD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,wBAAqB,CAAC;AAO3D,MAAM,MAAM,SAAS,GACf,eAAe,GACf,qBAAqB,GACrB,gBAAgB,GAChB,SAAS,GACT,kBAAkB,GAClB,iBAAiB,GACjB,gBAAgB,GAChB,gBAAgB,GAChB,aAAa,GACb,KAAK,GACL,UAAU,GACV,gBAAgB,GAChB,SAAS,GACT,qBAAqB,GACrB,gBAAgB,GAChB,WAAW,GACX,aAAa,GACb,kBAAkB,GAClB,aAAa,GACb,kBAAkB,GAClB,oBAAoB,GACpB,aAAa,GACb,kBAAkB,GAClB,cAAc,GACd,YAAY,GACZ,YAAY,GACZ,UAAU,GACV,eAAe,GACf,eAAe,GACf,eAAe,GACf,gBAAgB,GAChB,iBAAiB,GACjB,YAAY,GACZ,eAAe,GACf,eAAe,GACf,aAAa,CAAC;AAkDpB,eAAO,MAAM,kBAAkB,GAAI,WAAW,SAAS,KAAG,aAAa,CAAC,eAAe,CAAC,GAAG,SAsB1F,CAAC;AAIF,eAAO,MAAM,YAAY,GAAI,WAAW,MAAM,KAAG,SAAS,IAAI,SAE7D,CAAC;AAEF;;;;GAIG;AACH,MAAM,WAAW,gBAAgB;IAC7B;;;;;;;OAOG;IACH,YAAY,EAAE,OAAO,GAAG,QAAQ,CAAC;IACjC;;;;;;;;OAQG;IACH,UAAU,EAAE,OAAO,CAAC;IACpB;;;;;;;;;;OAUG;IACH,gBAAgB,EAAE,OAAO,CAAC;IAC1B;;;;OAIG;IACH,eAAe,EAAE,OAAO,CAAC;IACzB;;;;;;OAMG;IACH,eAAe,CAAC,EAAE,MAAM,SAAS,CAAC;CACrC;AAgCD;;;;GAIG;AACH,eAAO,MAAM,cAAc,GACvB,WAAW,SAAS,GAAG,IAAI,EAC3B,aAAa,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KACrC,gBAWF,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"FileManagementScreen.d.ts","sourceRoot":"","sources":["../../../../../src/ui/screens/FileManagementScreen.tsx"],"names":[],"mappings":"AAAA,OAAO,KAA4D,MAAM,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"FileManagementScreen.d.ts","sourceRoot":"","sources":["../../../../../src/ui/screens/FileManagementScreen.tsx"],"names":[],"mappings":"AAAA,OAAO,KAA4D,MAAM,OAAO,CAAC;AAejF,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,4BAAyB,CAAC;AA4CzE,QAAA,MAAM,oBAAoB,EAAE,KAAK,CAAC,EAAE,CAAC,yBAAyB,CA2/C7D,CAAC;AAEF,eAAe,oBAAoB,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"FileListSection.d.ts","sourceRoot":"","sources":["../../../../../../src/ui/screens/fileManagement/FileListSection.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAC/B,OAAO,EAIH,UAAU,EAGb,MAAM,cAAc,CAAC;AAEtB,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAEhD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAEnD,KAAK,WAAW,GAAG,UAAU,CAAC,OAAO,QAAQ,CAAC,CAAC,QAAQ,CAAC,CAAC;AAEzD,6EAA6E;AAC7E,MAAM,WAAW,YAAY;IACzB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IACvB,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,YAAY,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;CAClC;AAED,MAAM,WAAW,oBAAoB;IACjC,aAAa,EAAE,KAAK,CAAC,SAAS,CAAC,UAAU,GAAG,IAAI,CAAC,CAAC;IAClD,aAAa,EAAE,YAAY,EAAE,CAAC;IAC9B,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,YAAY,EAAE,CAAC;IACtB,MAAM,EAAE;QAAE,WAAW,EAAE,OAAO,CAAC;QAAC,OAAO,EAAE,OAAO,CAAA;KAAE,CAAC;IACnD,UAAU,EAAE,OAAO,CAAC;IACpB,MAAM,EAAE,WAAW,CAAC;IACpB,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC,KAAK,MAAM,CAAC;IACnE,SAAS,EAAE,MAAM,IAAI,CAAC;IACtB,UAAU,EAAE,MAAM,IAAI,CAAC;IACvB,aAAa,EAAE,MAAM,IAAI,CAAC;IAC1B,mEAAmE;IACnE,gBAAgB,EAAE,MAAM,KAAK,CAAC,SAAS,CAAC;CAC3C;AAED;;;;GAIG;AACH,QAAA,MAAM,eAAe,EAAE,KAAK,CAAC,EAAE,CAAC,oBAAoB,
|
|
1
|
+
{"version":3,"file":"FileListSection.d.ts","sourceRoot":"","sources":["../../../../../../src/ui/screens/fileManagement/FileListSection.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAC/B,OAAO,EAIH,UAAU,EAGb,MAAM,cAAc,CAAC;AAEtB,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAEhD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAEnD,KAAK,WAAW,GAAG,UAAU,CAAC,OAAO,QAAQ,CAAC,CAAC,QAAQ,CAAC,CAAC;AAEzD,6EAA6E;AAC7E,MAAM,WAAW,YAAY;IACzB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IACvB,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,YAAY,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;CAClC;AAED,MAAM,WAAW,oBAAoB;IACjC,aAAa,EAAE,KAAK,CAAC,SAAS,CAAC,UAAU,GAAG,IAAI,CAAC,CAAC;IAClD,aAAa,EAAE,YAAY,EAAE,CAAC;IAC9B,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,YAAY,EAAE,CAAC;IACtB,MAAM,EAAE;QAAE,WAAW,EAAE,OAAO,CAAC;QAAC,OAAO,EAAE,OAAO,CAAA;KAAE,CAAC;IACnD,UAAU,EAAE,OAAO,CAAC;IACpB,MAAM,EAAE,WAAW,CAAC;IACpB,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC,KAAK,MAAM,CAAC;IACnE,SAAS,EAAE,MAAM,IAAI,CAAC;IACtB,UAAU,EAAE,MAAM,IAAI,CAAC;IACvB,aAAa,EAAE,MAAM,IAAI,CAAC;IAC1B,mEAAmE;IACnE,gBAAgB,EAAE,MAAM,KAAK,CAAC,SAAS,CAAC;CAC3C;AAED;;;;GAIG;AACH,QAAA,MAAM,eAAe,EAAE,KAAK,CAAC,EAAE,CAAC,oBAAoB,CA6EnD,CAAC;AAEF,eAAe,eAAe,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PhotoPickerSection.d.ts","sourceRoot":"","sources":["../../../../../../src/ui/screens/fileManagement/PhotoPickerSection.tsx"],"names":[],"mappings":"AAAA,OAAO,KAA4D,MAAM,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"PhotoPickerSection.d.ts","sourceRoot":"","sources":["../../../../../../src/ui/screens/fileManagement/PhotoPickerSection.tsx"],"names":[],"mappings":"AAAA,OAAO,KAA4D,MAAM,OAAO,CAAC;AAgBjF,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AA2FhD;;;;GAIG;AACH,MAAM,WAAW,oBAAoB;IACjC,MAAM,EAAE,YAAY,EAAE,CAAC;IACvB,WAAW,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IACzB,WAAW,EAAE,OAAO,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,WAAW,EAAE,OAAO,CAAC;IACrB,UAAU,EAAE,OAAO,CAAC;IACpB,SAAS,EAAE,OAAO,CAAC;IACnB,iBAAiB,EAAE,OAAO,CAAC;IAC3B,cAAc,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IAC1D,OAAO,EAAE,OAAO,CAAC;IACjB,WAAW,EAAE,OAAO,CAAC;IACrB,YAAY,EAAE,OAAO,CAAC;IACtB,WAAW,EAAE,CAAC,IAAI,EAAE,YAAY,KAAK,MAAM,GAAG,SAAS,CAAC;IACxD,YAAY,EAAE,MAAM,CAAC;IACrB,OAAO,EAAE,OAAO,CAAC;IACjB,aAAa,EAAE,CAAC,KAAK,EAAE,YAAY,KAAK,IAAI,CAAC;IAC7C,cAAc,EAAE,CAAC,KAAK,EAAE,YAAY,KAAK,IAAI,CAAC;IAC9C,QAAQ,EAAE,MAAM,IAAI,CAAC;IACrB,SAAS,EAAE,MAAM,IAAI,CAAC;IACtB,UAAU,EAAE,MAAM,IAAI,CAAC;IACvB,QAAQ,EAAE,MAAM,IAAI,CAAC;IACrB,SAAS,EAAE,MAAM,IAAI,CAAC;IACtB,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC,KAAK,MAAM,CAAC;CACtE;AAmLD,QAAA,MAAM,eAAe,EAAE,KAAK,CAAC,EAAE,CAAC,oBAAoB,CAqWnD,CAAC;AAEF,eAAe,eAAe,CAAC"}
|
package/package.json
CHANGED
|
@@ -5,6 +5,7 @@ import type { RouteName } from '../navigation/routes';
|
|
|
5
5
|
import { getScreenComponent, getSheetConfig, isValidRoute } from '../navigation/routes';
|
|
6
6
|
import type { BaseScreenProps } from '../types/navigation';
|
|
7
7
|
import { useTheme } from '@oxyhq/bloom/theme';
|
|
8
|
+
import { Dialog } from '@oxyhq/bloom/dialog';
|
|
8
9
|
import BottomSheet, { type BottomSheetRef } from './BottomSheet';
|
|
9
10
|
import {
|
|
10
11
|
bottomSheetStore,
|
|
@@ -67,6 +68,15 @@ const errorStyles = StyleSheet.create({
|
|
|
67
68
|
message: { fontSize: 13, textAlign: 'center' },
|
|
68
69
|
});
|
|
69
70
|
|
|
71
|
+
/**
|
|
72
|
+
* Responsive placement for `presentation: 'dialog'` routes: a bottom-sheet on
|
|
73
|
+
* narrow viewports, a centered card at `md+` (≥768dp). Module-scoped so the
|
|
74
|
+
* object identity is stable across renders (Bloom's placement resolver memoizes
|
|
75
|
+
* on it). PhotoPickerSection keys its center-vs-bottom layout off the SAME `md`
|
|
76
|
+
* breakpoint — keep the two in sync if this map changes.
|
|
77
|
+
*/
|
|
78
|
+
const DIALOG_PLACEMENT = { base: 'bottom', md: 'center' } as const;
|
|
79
|
+
|
|
70
80
|
export interface BottomSheetRouterProps {
|
|
71
81
|
onScreenChange?: (screen: RouteName | null) => void;
|
|
72
82
|
onDismiss?: () => void;
|
|
@@ -93,6 +103,15 @@ const BottomSheetRouter: React.FC<BottomSheetRouterProps> = ({ onScreenChange, o
|
|
|
93
103
|
return component ?? null;
|
|
94
104
|
}, [currentScreen]);
|
|
95
105
|
|
|
106
|
+
// Route-level sheet config (which surface hosts the route, whether the sheet
|
|
107
|
+
// provides its own ScrollView, etc.). Computed early so the visibility
|
|
108
|
+
// effect can gate the in-tree sheet on the resolved presentation.
|
|
109
|
+
const sheetConfig = useMemo(
|
|
110
|
+
() => getSheetConfig(currentScreen, screenProps),
|
|
111
|
+
[currentScreen, screenProps],
|
|
112
|
+
);
|
|
113
|
+
const isDialogPresentation = sheetConfig.presentation === 'dialog';
|
|
114
|
+
|
|
96
115
|
// Notify screen changes
|
|
97
116
|
useEffect(() => {
|
|
98
117
|
if (prevScreenRef.current !== currentScreen) {
|
|
@@ -101,16 +120,19 @@ const BottomSheetRouter: React.FC<BottomSheetRouterProps> = ({ onScreenChange, o
|
|
|
101
120
|
}
|
|
102
121
|
}, [currentScreen, onScreenChange]);
|
|
103
122
|
|
|
104
|
-
// Control visibility
|
|
123
|
+
// Control visibility. The in-tree BottomSheet only ever presents for
|
|
124
|
+
// 'sheet'-presentation routes — 'dialog' routes render in the Bloom
|
|
125
|
+
// `<Dialog>` below, so the sheet stays dismissed for them (both surfaces
|
|
126
|
+
// remain mounted, so the pick→crop dialog→sheet handoff has no unmount jank).
|
|
105
127
|
useEffect(() => {
|
|
106
128
|
if (!sheetRef.current) return;
|
|
107
129
|
|
|
108
|
-
if (isOpen) {
|
|
130
|
+
if (isOpen && !isDialogPresentation) {
|
|
109
131
|
sheetRef.current.present();
|
|
110
132
|
} else {
|
|
111
133
|
sheetRef.current.dismiss();
|
|
112
134
|
}
|
|
113
|
-
}, [isOpen]);
|
|
135
|
+
}, [isOpen, isDialogPresentation]);
|
|
114
136
|
|
|
115
137
|
// Android back button
|
|
116
138
|
useEffect(() => {
|
|
@@ -210,33 +232,56 @@ const BottomSheetRouter: React.FC<BottomSheetRouterProps> = ({ onScreenChange, o
|
|
|
210
232
|
};
|
|
211
233
|
}, [navigate, handleGoBack, theme.mode, currentScreen, currentStep, screenProps, handleStepChange, scrollTo]);
|
|
212
234
|
|
|
213
|
-
//
|
|
214
|
-
//
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
235
|
+
// The active screen, wrapped in its error boundary. Routed into EXACTLY ONE
|
|
236
|
+
// surface (the in-tree sheet OR the Dialog) based on the resolved
|
|
237
|
+
// presentation, so it never double-mounts.
|
|
238
|
+
const screenNode =
|
|
239
|
+
ScreenComponent && currentScreen ? (
|
|
240
|
+
<ScreenErrorBoundary screenName={currentScreen}>
|
|
241
|
+
<ScreenComponent {...screenPropsValue} />
|
|
242
|
+
</ScreenErrorBoundary>
|
|
243
|
+
) : null;
|
|
219
244
|
|
|
220
245
|
return (
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
246
|
+
<>
|
|
247
|
+
<BottomSheet
|
|
248
|
+
ref={sheetRef}
|
|
249
|
+
enablePanDownToClose
|
|
250
|
+
enableHandlePanningGesture
|
|
251
|
+
backgroundComponent={renderBackground}
|
|
252
|
+
style={styles.container}
|
|
253
|
+
onDismiss={handleDismiss}
|
|
254
|
+
onDismissAttempt={handleDismissAttempt}
|
|
255
|
+
scrollable={sheetConfig.scrollable}
|
|
256
|
+
manualActivation={sheetConfig.manualActivation}
|
|
257
|
+
dynamicBackdrop={sheetConfig.dynamicBackdrop}
|
|
258
|
+
handleComponent={sheetConfig.handleComponent}
|
|
259
|
+
>
|
|
260
|
+
{!isDialogPresentation && screenNode}
|
|
261
|
+
</BottomSheet>
|
|
262
|
+
{/* Always-mounted responsive Dialog surface for `presentation:
|
|
263
|
+
'dialog'` routes (currently the flagship photo picker). It stays
|
|
264
|
+
inert (`open={false}`, no child) for the ~28 sheet routes, so they
|
|
265
|
+
keep byte-for-byte identical behavior. `contentPadding={0}` +
|
|
266
|
+
the full-bleed black, rounded, clipped surface let the picker
|
|
267
|
+
paint edge-to-edge; `dismissOnBackdrop={false}` keeps the
|
|
268
|
+
controlled open state authoritative (dismissal flows through the
|
|
269
|
+
store, matching OxyAccountDialog). */}
|
|
270
|
+
<Dialog
|
|
271
|
+
open={isOpen && isDialogPresentation}
|
|
272
|
+
onClose={handleDismiss}
|
|
273
|
+
placement={DIALOG_PLACEMENT}
|
|
274
|
+
dismissOnBackdrop={false}
|
|
275
|
+
contentPadding={0}
|
|
276
|
+
maxWidth={640}
|
|
277
|
+
maxHeightRatio={0.9}
|
|
278
|
+
style={styles.dialogSurface}
|
|
279
|
+
panelStyle={styles.dialogSurface}
|
|
280
|
+
label={currentScreen ?? undefined}
|
|
281
|
+
>
|
|
282
|
+
{isDialogPresentation ? screenNode : null}
|
|
283
|
+
</Dialog>
|
|
284
|
+
</>
|
|
240
285
|
);
|
|
241
286
|
};
|
|
242
287
|
|
|
@@ -252,6 +297,14 @@ const styles = StyleSheet.create({
|
|
|
252
297
|
borderTopRightRadius: 20,
|
|
253
298
|
overflow: 'hidden',
|
|
254
299
|
},
|
|
300
|
+
// Full-bleed black surface for `presentation: 'dialog'` routes. `overflow:
|
|
301
|
+
// 'hidden'` clips the picker's edge-to-edge black content to the panel's
|
|
302
|
+
// rounded corners (Dialog supplies the radius per placement); the black
|
|
303
|
+
// fill covers any sub-pixel gap behind the picker during entrance.
|
|
304
|
+
dialogSurface: {
|
|
305
|
+
backgroundColor: '#000000',
|
|
306
|
+
overflow: 'hidden',
|
|
307
|
+
},
|
|
255
308
|
});
|
|
256
309
|
|
|
257
310
|
export default React.memo(BottomSheetRouter);
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
} from 'react-native';
|
|
11
11
|
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
|
12
12
|
import { Avatar } from '@oxyhq/bloom/avatar';
|
|
13
|
+
import * as Skeleton from '@oxyhq/bloom/skeleton';
|
|
13
14
|
import { Text } from '@oxyhq/bloom/typography';
|
|
14
15
|
import { useTheme } from '@oxyhq/bloom/theme';
|
|
15
16
|
import { getAccountDisplayName, getAccountFallbackHandle } from '@oxyhq/core';
|
|
@@ -140,16 +141,7 @@ const ProfileButton: React.FC<ProfileButtonProps> = ({
|
|
|
140
141
|
accessibilityElementsHidden
|
|
141
142
|
importantForAccessibility="no-hide-descendants"
|
|
142
143
|
>
|
|
143
|
-
<
|
|
144
|
-
style={[
|
|
145
|
-
styles.skeletonCircle,
|
|
146
|
-
{
|
|
147
|
-
width: resolvedAvatarSize,
|
|
148
|
-
height: resolvedAvatarSize,
|
|
149
|
-
backgroundColor: colors.backgroundSecondary,
|
|
150
|
-
},
|
|
151
|
-
]}
|
|
152
|
-
/>
|
|
144
|
+
<Skeleton.Circle size={resolvedAvatarSize} />
|
|
153
145
|
</View>
|
|
154
146
|
);
|
|
155
147
|
}
|
|
@@ -352,10 +344,6 @@ const styles = StyleSheet.create({
|
|
|
352
344
|
fullWidth: {
|
|
353
345
|
width: '100%',
|
|
354
346
|
},
|
|
355
|
-
// Neutral skeleton / signed-out avatar circle (`rounded-full`).
|
|
356
|
-
skeletonCircle: {
|
|
357
|
-
borderRadius: 9999,
|
|
358
|
-
},
|
|
359
347
|
// Shared trigger row (`flex-row items-center gap-3 rounded-full px-2 py-2`).
|
|
360
348
|
row: {
|
|
361
349
|
flexDirection: 'row',
|
|
@@ -65,7 +65,7 @@ const UploadPreviewContent: React.FC<{
|
|
|
65
65
|
const isImage = pendingFile.type.startsWith('image/');
|
|
66
66
|
return (
|
|
67
67
|
<View
|
|
68
|
-
key={index}
|
|
68
|
+
key={`${pendingFile.name}-${pendingFile.size}-${index}`}
|
|
69
69
|
className="bg-secondary border-border flex-row items-center p-[12px] rounded-[12px] border mb-[12px] gap-[12px]"
|
|
70
70
|
>
|
|
71
71
|
{isImage && pendingFile.preview ? (
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { getSheetConfig } from '../routes';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* `getSheetConfig` drives which surface `BottomSheetRouter` mounts a route in.
|
|
5
|
+
* The flagship image picker (FileManagement in image-only select mode) resolves
|
|
6
|
+
* to `presentation: 'dialog'`; every other route stays `'sheet'`. These guard
|
|
7
|
+
* the routing contract so the ~28 sheet screens keep the in-tree BottomSheet.
|
|
8
|
+
*/
|
|
9
|
+
describe('getSheetConfig presentation', () => {
|
|
10
|
+
const imageOnlyPickerProps = {
|
|
11
|
+
selectMode: true,
|
|
12
|
+
disabledMimeTypes: ['video/', 'audio/', 'application/pdf'],
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
it('defaults every route to the in-tree sheet', () => {
|
|
16
|
+
expect(getSheetConfig('ManageAccount', {}).presentation).toBe('sheet');
|
|
17
|
+
expect(getSheetConfig('AvatarCrop', {}).presentation).toBe('sheet');
|
|
18
|
+
expect(getSheetConfig(null, {}).presentation).toBe('sheet');
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('routes the FileManagement image-only picker to the Dialog', () => {
|
|
22
|
+
expect(getSheetConfig('FileManagement', imageOnlyPickerProps).presentation).toBe('dialog');
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('keeps FileManagement on the sheet when it is not an image-only picker', () => {
|
|
26
|
+
// Browse mode (no selectMode) — the standard file manager.
|
|
27
|
+
expect(getSheetConfig('FileManagement', {}).presentation).toBe('sheet');
|
|
28
|
+
// Select mode that still allows non-image types is not the flagship picker.
|
|
29
|
+
expect(
|
|
30
|
+
getSheetConfig('FileManagement', {
|
|
31
|
+
selectMode: true,
|
|
32
|
+
disabledMimeTypes: ['video/'],
|
|
33
|
+
}).presentation,
|
|
34
|
+
).toBe('sheet');
|
|
35
|
+
});
|
|
36
|
+
});
|
|
@@ -128,11 +128,23 @@ export const isValidRoute = (routeName: string): routeName is RouteName => {
|
|
|
128
128
|
* remove fields without bumping the consumer surface as a breaking change.
|
|
129
129
|
*/
|
|
130
130
|
export interface SheetRouteConfig {
|
|
131
|
+
/**
|
|
132
|
+
* Which surface hosts the route. `'sheet'` (default) renders the screen in
|
|
133
|
+
* the in-tree `BottomSheet`. `'dialog'` renders it in a responsive Bloom
|
|
134
|
+
* `<Dialog>` (bottom-sheet on narrow, centered card on `md+`) — used for the
|
|
135
|
+
* flagship image picker, which reads better as a centered panel on wide
|
|
136
|
+
* viewports than a full-width sheet. `BottomSheetRouter` keeps BOTH surfaces
|
|
137
|
+
* mounted and routes the active screen into exactly one based on this value.
|
|
138
|
+
*/
|
|
139
|
+
presentation: 'sheet' | 'dialog';
|
|
131
140
|
/**
|
|
132
141
|
* When `false`, BottomSheet skips its internal ScrollView and lets the
|
|
133
142
|
* screen own scrolling. Required for screens that render a FlatList,
|
|
134
143
|
* SectionList, or any other VirtualizedList — nesting one inside a
|
|
135
144
|
* plain ScrollView breaks windowing and triggers a RN warning.
|
|
145
|
+
*
|
|
146
|
+
* Ignored for `presentation: 'dialog'` routes — those don't render in the
|
|
147
|
+
* in-tree `BottomSheet` at all.
|
|
136
148
|
*/
|
|
137
149
|
scrollable: boolean;
|
|
138
150
|
/**
|
|
@@ -187,6 +199,7 @@ const isFileManagementImageOnlyPicker = (props: Record<string, unknown>): boolea
|
|
|
187
199
|
|
|
188
200
|
/** Defaults shared across all routes — preserves the historical in-tree BS UX. */
|
|
189
201
|
const DEFAULT_SHEET_CONFIG: SheetRouteConfig = {
|
|
202
|
+
presentation: 'sheet',
|
|
190
203
|
scrollable: true,
|
|
191
204
|
manualActivation: true,
|
|
192
205
|
dynamicBackdrop: true,
|
|
@@ -203,10 +216,12 @@ export const getSheetConfig = (
|
|
|
203
216
|
): SheetRouteConfig => {
|
|
204
217
|
if (!routeName) return DEFAULT_SHEET_CONFIG;
|
|
205
218
|
if (routeName === 'FileManagement' && isFileManagementImageOnlyPicker(screenProps)) {
|
|
206
|
-
//
|
|
207
|
-
//
|
|
208
|
-
//
|
|
209
|
-
|
|
219
|
+
// The flagship photo picker renders in a responsive Bloom `<Dialog>`
|
|
220
|
+
// (bottom-sheet on narrow, centered card on `md+`) rather than the
|
|
221
|
+
// in-tree BottomSheet — it owns its own FlatList and reads better as a
|
|
222
|
+
// centered panel on wide viewports. The Dialog surface, not this
|
|
223
|
+
// config's `scrollable`, governs its scrolling.
|
|
224
|
+
return { ...DEFAULT_SHEET_CONFIG, presentation: 'dialog' };
|
|
210
225
|
}
|
|
211
226
|
return DEFAULT_SHEET_CONFIG;
|
|
212
227
|
};
|
|
@@ -9,15 +9,13 @@ import {
|
|
|
9
9
|
RefreshControl,
|
|
10
10
|
TextInput,
|
|
11
11
|
Image,
|
|
12
|
-
Animated,
|
|
13
|
-
Easing,
|
|
14
12
|
AccessibilityInfo,
|
|
15
|
-
Platform,
|
|
16
13
|
useWindowDimensions,
|
|
17
14
|
} from 'react-native';
|
|
18
15
|
import { Image as ExpoImage } from 'expo-image';
|
|
19
16
|
import type { FileManagementScreenProps } from '../types/fileManagement';
|
|
20
17
|
import { Dialog, toast, useDialogControl } from '@oxyhq/bloom';
|
|
18
|
+
import * as Skeleton from '@oxyhq/bloom/skeleton';
|
|
21
19
|
import { Ionicons, MaterialCommunityIcons } from '@expo/vector-icons';
|
|
22
20
|
import type { FileMetadata } from '@oxyhq/core';
|
|
23
21
|
import { useFileStore, useFiles, useUploading as useUploadingStore, useUploadAggregateProgress, useDeleting as useDeletingStore } from '../stores/fileStore';
|
|
@@ -1121,193 +1119,57 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
|
|
|
1121
1119
|
</View>
|
|
1122
1120
|
);
|
|
1123
1121
|
|
|
1124
|
-
//
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
// Web has no native animated module; forcing the native
|
|
1136
|
-
// driver there warns and falls back to JS. The shimmer only
|
|
1137
|
-
// drives a transform, so JS-driving it on web is fine.
|
|
1138
|
-
useNativeDriver: Platform.OS !== 'web',
|
|
1139
|
-
})
|
|
1140
|
-
);
|
|
1141
|
-
shimmer.start();
|
|
1142
|
-
return () => shimmer.stop();
|
|
1143
|
-
}, [shimmerAnim]);
|
|
1144
|
-
|
|
1145
|
-
// Create a sweeping shimmer effect
|
|
1146
|
-
const shimmerTranslateX = shimmerAnim.interpolate({
|
|
1147
|
-
inputRange: [0, 1],
|
|
1148
|
-
outputRange: [-skeletonContainerWidth * 2, skeletonContainerWidth * 2],
|
|
1149
|
-
});
|
|
1150
|
-
|
|
1151
|
-
const SkeletonBox = ({ width, height, borderRadius = 8, style, delay = 0 }: { width: number | string; height: number; borderRadius?: number; style?: Record<string, unknown>; delay?: number }) => {
|
|
1152
|
-
const delayedTranslateX = shimmerAnim.interpolate({
|
|
1153
|
-
inputRange: [0, 1],
|
|
1154
|
-
outputRange: [-skeletonContainerWidth * 2 + delay, skeletonContainerWidth * 2 + delay],
|
|
1155
|
-
});
|
|
1156
|
-
|
|
1157
|
-
return (
|
|
1158
|
-
<View
|
|
1159
|
-
style={[
|
|
1160
|
-
{
|
|
1161
|
-
width,
|
|
1162
|
-
height,
|
|
1163
|
-
borderRadius,
|
|
1164
|
-
backgroundColor: colors.backgroundSecondary,
|
|
1165
|
-
overflow: 'hidden',
|
|
1166
|
-
position: 'relative',
|
|
1167
|
-
},
|
|
1168
|
-
style,
|
|
1169
|
-
]}
|
|
1170
|
-
>
|
|
1171
|
-
{/* Base background */}
|
|
1172
|
-
<View
|
|
1173
|
-
style={{
|
|
1174
|
-
position: 'absolute',
|
|
1175
|
-
top: 0,
|
|
1176
|
-
left: 0,
|
|
1177
|
-
right: 0,
|
|
1178
|
-
bottom: 0,
|
|
1179
|
-
backgroundColor: colors.backgroundSecondary,
|
|
1180
|
-
}}
|
|
1181
|
-
/>
|
|
1182
|
-
{/* Shimmer gradient effect */}
|
|
1183
|
-
<Animated.View
|
|
1184
|
-
style={{
|
|
1185
|
-
position: 'absolute',
|
|
1186
|
-
top: 0,
|
|
1187
|
-
left: 0,
|
|
1188
|
-
width: '100%',
|
|
1189
|
-
height: '100%',
|
|
1190
|
-
transform: [{ translateX: delayedTranslateX }],
|
|
1191
|
-
}}
|
|
1192
|
-
>
|
|
1193
|
-
<View
|
|
1194
|
-
style={{
|
|
1195
|
-
width: skeletonContainerWidth,
|
|
1196
|
-
height: '100%',
|
|
1197
|
-
backgroundColor: bloomTheme.isDark
|
|
1198
|
-
? 'rgba(255, 255, 255, 0.08)'
|
|
1199
|
-
: 'rgba(255, 255, 255, 0.8)',
|
|
1200
|
-
shadowColor: bloomTheme.isDark ? '#000' : '#FFF',
|
|
1201
|
-
shadowOffset: { width: 0, height: 0 },
|
|
1202
|
-
shadowOpacity: 0.3,
|
|
1203
|
-
shadowRadius: 10,
|
|
1204
|
-
}}
|
|
1205
|
-
/>
|
|
1206
|
-
</Animated.View>
|
|
1207
|
-
</View>
|
|
1208
|
-
);
|
|
1209
|
-
};
|
|
1210
|
-
|
|
1211
|
-
// Skeleton file item matching SettingsListItem structure
|
|
1212
|
-
const SkeletonFileItem = ({ index }: { index: number }) => (
|
|
1213
|
-
<View
|
|
1214
|
-
style={[
|
|
1215
|
-
{
|
|
1216
|
-
flexDirection: 'row',
|
|
1217
|
-
alignItems: 'center',
|
|
1218
|
-
paddingHorizontal: 16,
|
|
1219
|
-
paddingVertical: 12,
|
|
1220
|
-
backgroundColor: colors.background,
|
|
1221
|
-
borderBottomWidth: StyleSheet.hairlineWidth,
|
|
1222
|
-
borderBottomColor: colors.border,
|
|
1223
|
-
},
|
|
1224
|
-
]}
|
|
1225
|
-
>
|
|
1226
|
-
{/* Icon/Image skeleton */}
|
|
1227
|
-
<SkeletonBox width={44} height={44} borderRadius={8} delay={index * 50} />
|
|
1228
|
-
|
|
1229
|
-
{/* Content skeleton */}
|
|
1230
|
-
<View style={{ flex: 1, marginLeft: 12, justifyContent: 'center' }}>
|
|
1231
|
-
<SkeletonBox
|
|
1232
|
-
width={index % 3 === 0 ? '85%' : index % 3 === 1 ? '70%' : '90%'}
|
|
1233
|
-
height={16}
|
|
1234
|
-
style={{ marginBottom: 8 }}
|
|
1235
|
-
delay={index * 50 + 20}
|
|
1236
|
-
/>
|
|
1237
|
-
<SkeletonBox
|
|
1238
|
-
width={index % 2 === 0 ? '50%' : '60%'}
|
|
1239
|
-
height={12}
|
|
1240
|
-
delay={index * 50 + 40}
|
|
1241
|
-
/>
|
|
1242
|
-
</View>
|
|
1243
|
-
</View>
|
|
1122
|
+
// Loading state — Bloom's shared `Skeleton` primitives own the shimmer, so
|
|
1123
|
+
// this is pure layout: the top chrome (header / controls / search) plus a
|
|
1124
|
+
// photo grid of shimmering tiles sized with the same 3-per-row geometry the
|
|
1125
|
+
// real photo grid uses. No hand-rolled `Animated` shimmer.
|
|
1126
|
+
if (loading) {
|
|
1127
|
+
const GRID_PADDING = 10;
|
|
1128
|
+
const TILE_GAP = 4;
|
|
1129
|
+
const GRID_COLUMNS = 3;
|
|
1130
|
+
const GRID_ROWS = 6;
|
|
1131
|
+
const tileSize = Math.floor(
|
|
1132
|
+
(safeContainerWidth - GRID_PADDING * 2 - TILE_GAP * (GRID_COLUMNS - 1)) / GRID_COLUMNS,
|
|
1244
1133
|
);
|
|
1245
1134
|
|
|
1246
1135
|
return (
|
|
1247
1136
|
<View className="flex-1" style={{ backgroundColor }}>
|
|
1248
|
-
{/* Header
|
|
1137
|
+
{/* Header */}
|
|
1249
1138
|
<View
|
|
1250
|
-
className="flex-row items-center justify-between px-[16px] py-[12px]
|
|
1139
|
+
className="flex-row items-center justify-between px-[16px] py-[12px]"
|
|
1251
1140
|
style={{ borderBottomColor: colors.border, borderBottomWidth: StyleSheet.hairlineWidth }}
|
|
1252
1141
|
>
|
|
1253
|
-
<
|
|
1254
|
-
<
|
|
1255
|
-
<
|
|
1256
|
-
<
|
|
1257
|
-
</
|
|
1258
|
-
<
|
|
1142
|
+
<Skeleton.Box width={44} height={44} borderRadius={12} />
|
|
1143
|
+
<Skeleton.Col style={{ alignItems: 'center', marginHorizontal: 16, gap: 6 }}>
|
|
1144
|
+
<Skeleton.Box width={140} height={20} />
|
|
1145
|
+
<Skeleton.Box width={100} height={14} />
|
|
1146
|
+
</Skeleton.Col>
|
|
1147
|
+
<Skeleton.Box width={44} height={44} borderRadius={12} />
|
|
1259
1148
|
</View>
|
|
1260
1149
|
|
|
1261
|
-
{/* Controls
|
|
1262
|
-
<
|
|
1263
|
-
<
|
|
1264
|
-
<
|
|
1265
|
-
</
|
|
1150
|
+
{/* Controls bar */}
|
|
1151
|
+
<Skeleton.Row style={{ alignItems: 'center', justifyContent: 'space-between', paddingHorizontal: 12, paddingVertical: 12 }}>
|
|
1152
|
+
<Skeleton.Box width={100} height={36} borderRadius={18} />
|
|
1153
|
+
<Skeleton.Box width={44} height={44} borderRadius={22} />
|
|
1154
|
+
</Skeleton.Row>
|
|
1266
1155
|
|
|
1267
|
-
{/* Search
|
|
1268
|
-
<View
|
|
1269
|
-
|
|
1270
|
-
style={{ backgroundColor: colors.card }}
|
|
1271
|
-
>
|
|
1272
|
-
<SkeletonBox width="100%" height={44} borderRadius={12} />
|
|
1156
|
+
{/* Search bar */}
|
|
1157
|
+
<View className="mx-[12px] mb-[12px]">
|
|
1158
|
+
<Skeleton.Box width="100%" height={44} borderRadius={22} />
|
|
1273
1159
|
</View>
|
|
1274
1160
|
|
|
1275
|
-
{/*
|
|
1276
|
-
<View
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
<SkeletonBox width={40} height={14} delay={i * 30 + 15} />
|
|
1284
|
-
</View>
|
|
1161
|
+
{/* Photo grid */}
|
|
1162
|
+
<View style={{ padding: GRID_PADDING, gap: TILE_GAP }}>
|
|
1163
|
+
{Array.from({ length: GRID_ROWS }, (_, row) => (
|
|
1164
|
+
<Skeleton.Row key={row} style={{ gap: TILE_GAP }}>
|
|
1165
|
+
{Array.from({ length: GRID_COLUMNS }, (_, col) => (
|
|
1166
|
+
<Skeleton.Box key={col} width={tileSize} height={tileSize} borderRadius={6} />
|
|
1167
|
+
))}
|
|
1168
|
+
</Skeleton.Row>
|
|
1285
1169
|
))}
|
|
1286
1170
|
</View>
|
|
1287
|
-
|
|
1288
|
-
{/* File List Skeleton - Matching SettingsListItem */}
|
|
1289
|
-
<ScrollView
|
|
1290
|
-
className="flex-1"
|
|
1291
|
-
contentContainerClassName="px-[12px] pt-0 pb-[12px]"
|
|
1292
|
-
showsVerticalScrollIndicator={false}
|
|
1293
|
-
>
|
|
1294
|
-
<View style={{
|
|
1295
|
-
backgroundColor: colors.card,
|
|
1296
|
-
borderRadius: 18,
|
|
1297
|
-
overflow: 'hidden',
|
|
1298
|
-
marginTop: 8,
|
|
1299
|
-
}}>
|
|
1300
|
-
{[1, 2, 3, 4, 5, 6, 7, 8].map((i) => (
|
|
1301
|
-
<SkeletonFileItem key={i} index={i} />
|
|
1302
|
-
))}
|
|
1303
|
-
</View>
|
|
1304
|
-
</ScrollView>
|
|
1305
1171
|
</View>
|
|
1306
1172
|
);
|
|
1307
|
-
});
|
|
1308
|
-
|
|
1309
|
-
if (loading) {
|
|
1310
|
-
return <SkeletonLoader />;
|
|
1311
1173
|
}
|
|
1312
1174
|
|
|
1313
1175
|
// Dedicated flagship-style photo picker view used in the avatar-picker
|