@oxyhq/services 22.8.3 → 22.8.5
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/navigation/routes.js +7 -4
- package/lib/commonjs/ui/navigation/routes.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 -195
- 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/navigation/routes.js +7 -4
- package/lib/module/ui/navigation/routes.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 -196
- 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/navigation/routes.d.ts +12 -0
- package/lib/typescript/commonjs/ui/navigation/routes.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/navigation/routes.d.ts +12 -0
- package/lib/typescript/module/ui/navigation/routes.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 +2 -2
- package/src/ui/components/BottomSheetRouter.tsx +81 -28
- package/src/ui/navigation/__tests__/getSheetConfig.test.ts +36 -0
- package/src/ui/navigation/routes.ts +19 -4
- package/src/ui/screens/fileManagement/FileListSection.tsx +1 -0
- package/src/ui/screens/fileManagement/PhotoPickerSection.tsx +127 -192
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
} from 'react-native';
|
|
14
14
|
import { Image as ExpoImage } from 'expo-image';
|
|
15
15
|
import { Ionicons, MaterialCommunityIcons } from '@expo/vector-icons';
|
|
16
|
+
import { BREAKPOINTS } from '@oxyhq/bloom/styles';
|
|
16
17
|
import type { FileMetadata } from '@oxyhq/core';
|
|
17
18
|
import { computePhotoGridLayout } from './photoGridLayout';
|
|
18
19
|
|
|
@@ -91,20 +92,18 @@ const hapticNotification = async (type: HapticNotification): Promise<void> => {
|
|
|
91
92
|
};
|
|
92
93
|
|
|
93
94
|
/**
|
|
94
|
-
* The
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
* classes in the JSX MUST stay in sync with them.
|
|
95
|
+
* The picker renders inside a Bloom `<Dialog>` (bottom-sheet on narrow, centered
|
|
96
|
+
* card on `md+`). The Dialog owns its own chrome: it draws its OWN drag handle
|
|
97
|
+
* in bottom placement (a floating 28dp pill that paints ABOVE this header via a
|
|
98
|
+
* higher z-index) and none in center placement — so the picker's translucent
|
|
99
|
+
* header no longer reserves the handle strip itself. The header is just the 56dp
|
|
100
|
+
* app bar; grid content is pushed down by the app bar + 4dp of breathing room so
|
|
101
|
+
* the first row clears it. These are fixed layout constants — the equivalent
|
|
102
|
+
* `min-h-[56px]` / `pt-[60px]` utility classes in the JSX MUST stay in sync.
|
|
103
103
|
*/
|
|
104
|
-
const HANDLE_ZONE = 28;
|
|
105
104
|
const APP_BAR_HEIGHT = 56;
|
|
106
|
-
const HEADER_HEIGHT =
|
|
107
|
-
const GRID_CONTENT_PADDING_TOP = HEADER_HEIGHT + 4; //
|
|
105
|
+
const HEADER_HEIGHT = APP_BAR_HEIGHT; // 56
|
|
106
|
+
const GRID_CONTENT_PADDING_TOP = HEADER_HEIGHT + 4; // 60
|
|
108
107
|
|
|
109
108
|
/**
|
|
110
109
|
* Props for the dedicated photo picker view. Used by FileManagementScreen
|
|
@@ -138,8 +137,8 @@ export interface PhotoPickerViewProps {
|
|
|
138
137
|
}
|
|
139
138
|
|
|
140
139
|
/**
|
|
141
|
-
* Photo-cell animation tuning. The
|
|
142
|
-
*
|
|
140
|
+
* Photo-cell animation tuning. The cell uses RN's own `Animated` (NOT
|
|
141
|
+
* Reanimated): its entrance is a staggered opacity fade — replacing the
|
|
143
142
|
* Reanimated `entering={FadeIn}` LAYOUT animation, which cannot load on the
|
|
144
143
|
* RN-Web build (no `react-native-worklets` babel plugin) and only warns +
|
|
145
144
|
* no-ops there. RN `Animated` runs cross-platform: native-driven on device,
|
|
@@ -155,69 +154,13 @@ const RING_PULSE_UP_MS = 110;
|
|
|
155
154
|
const RING_SPRING_FRICTION = 9;
|
|
156
155
|
const RING_SPRING_TENSION = 120;
|
|
157
156
|
/**
|
|
158
|
-
*
|
|
159
|
-
*
|
|
160
|
-
*
|
|
161
|
-
* `Animated.View` under the RN-Web build), so these keep it pixel-identical to
|
|
162
|
-
* the static ring.
|
|
157
|
+
* The selection ring is an `Animated.View` (its pulse transform must be inline,
|
|
158
|
+
* and `className` interop on `Animated.View` is not guaranteed under the RN-Web
|
|
159
|
+
* build), so these mirror the `rounded-radius-8 border-[3px]` design tokens.
|
|
163
160
|
*/
|
|
164
161
|
const CELL_CORNER_RADIUS = 8; // `rounded-radius-8`
|
|
165
162
|
const SELECTION_RING_WIDTH = 3; // `border-[3px]`
|
|
166
163
|
|
|
167
|
-
/**
|
|
168
|
-
* A single photo cell. Memoized so re-renders during selection only touch
|
|
169
|
-
* affected cells — selection of one photo must not redraw the whole grid.
|
|
170
|
-
*
|
|
171
|
-
* Apple Photos pattern: when any cell is selected, *non-selected* siblings
|
|
172
|
-
* fade to 0.6 opacity to focus attention on the active selection.
|
|
173
|
-
*/
|
|
174
|
-
/**
|
|
175
|
-
* Shared cell chrome (image, dim, badge). Ring border is injected by the
|
|
176
|
-
* static vs animated wrappers.
|
|
177
|
-
*/
|
|
178
|
-
function PhotoPickerCellContent(props: {
|
|
179
|
-
photo: FileMetadata;
|
|
180
|
-
dim: boolean;
|
|
181
|
-
isSelected: boolean;
|
|
182
|
-
selectionIndex: number;
|
|
183
|
-
primaryColor: string;
|
|
184
|
-
thumbUrl: string | undefined;
|
|
185
|
-
ring: React.ReactNode;
|
|
186
|
-
}) {
|
|
187
|
-
const {
|
|
188
|
-
photo, dim, isSelected, selectionIndex, primaryColor, thumbUrl, ring,
|
|
189
|
-
} = props;
|
|
190
|
-
|
|
191
|
-
return (
|
|
192
|
-
<>
|
|
193
|
-
<View
|
|
194
|
-
className={`flex-1 rounded-radius-8 overflow-hidden bg-[#111111]${dim ? ' opacity-60' : ''}`}
|
|
195
|
-
>
|
|
196
|
-
<ExpoImage
|
|
197
|
-
source={{ uri: thumbUrl }}
|
|
198
|
-
style={{ width: '100%', height: '100%' }}
|
|
199
|
-
contentFit="cover"
|
|
200
|
-
transition={120}
|
|
201
|
-
cachePolicy="memory-disk"
|
|
202
|
-
accessibilityLabel={photo.filename}
|
|
203
|
-
/>
|
|
204
|
-
</View>
|
|
205
|
-
{ring}
|
|
206
|
-
{isSelected && (
|
|
207
|
-
<View
|
|
208
|
-
pointerEvents="none"
|
|
209
|
-
className="absolute top-1.5 right-1.5 min-w-[22px] h-[22px] px-1.5 rounded-full items-center justify-center"
|
|
210
|
-
style={{ backgroundColor: primaryColor }}
|
|
211
|
-
>
|
|
212
|
-
<Text className="text-white text-[12px] font-bold leading-[14px]">
|
|
213
|
-
{selectionIndex > 0 ? String(selectionIndex) : ''}
|
|
214
|
-
</Text>
|
|
215
|
-
</View>
|
|
216
|
-
)}
|
|
217
|
-
</>
|
|
218
|
-
);
|
|
219
|
-
}
|
|
220
|
-
|
|
221
164
|
type PhotoPickerCellProps = {
|
|
222
165
|
photo: FileMetadata;
|
|
223
166
|
size: number;
|
|
@@ -235,65 +178,33 @@ type PhotoPickerCellProps = {
|
|
|
235
178
|
a11yLabel: string;
|
|
236
179
|
};
|
|
237
180
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
};
|
|
250
|
-
|
|
251
|
-
const ring = isSelected ? (
|
|
252
|
-
<View
|
|
253
|
-
pointerEvents="none"
|
|
254
|
-
className="absolute inset-0 rounded-radius-8 border-[3px]"
|
|
255
|
-
style={{ borderColor: primaryColor }}
|
|
256
|
-
/>
|
|
257
|
-
) : null;
|
|
258
|
-
|
|
259
|
-
return (
|
|
260
|
-
<TouchableOpacity
|
|
261
|
-
activeOpacity={0.85}
|
|
262
|
-
onPress={onPress}
|
|
263
|
-
onLongPress={onLongPress}
|
|
264
|
-
className="relative"
|
|
265
|
-
style={cellWrapperStyle}
|
|
266
|
-
accessibilityRole="button"
|
|
267
|
-
accessibilityLabel={a11yLabel}
|
|
268
|
-
accessibilityState={{ selected: isSelected }}
|
|
269
|
-
>
|
|
270
|
-
<PhotoPickerCellContent
|
|
271
|
-
photo={photo}
|
|
272
|
-
dim={dim}
|
|
273
|
-
isSelected={isSelected}
|
|
274
|
-
selectionIndex={selectionIndex}
|
|
275
|
-
primaryColor={primaryColor}
|
|
276
|
-
thumbUrl={thumbUrl}
|
|
277
|
-
ring={ring}
|
|
278
|
-
/>
|
|
279
|
-
</TouchableOpacity>
|
|
280
|
-
);
|
|
281
|
-
});
|
|
282
|
-
|
|
283
|
-
const PhotoPickerCellAnimated = React.memo(function PhotoPickerCellAnimated(props: PhotoPickerCellProps) {
|
|
181
|
+
/**
|
|
182
|
+
* A single photo cell. Memoized so re-renders during selection only touch
|
|
183
|
+
* affected cells — selecting one photo must not redraw the whole grid.
|
|
184
|
+
*
|
|
185
|
+
* Apple Photos pattern: when any cell is selected, *non-selected* siblings fade
|
|
186
|
+
* to 0.6 opacity to focus attention on the active selection. A staggered
|
|
187
|
+
* entrance fade plays on mount; a spring pulse plays each time a cell enters the
|
|
188
|
+
* selected state. Both animations are pinned off under reduce-motion, so the
|
|
189
|
+
* one component covers both the animated and the reduced-motion paths.
|
|
190
|
+
*/
|
|
191
|
+
const PhotoPickerCell = React.memo(function PhotoPickerCell(props: PhotoPickerCellProps) {
|
|
284
192
|
const {
|
|
285
193
|
photo, size, marginRight, marginBottom, isSelected, selectionIndex,
|
|
286
|
-
dim, primaryColor, thumbUrl, enterIndex,
|
|
194
|
+
dim, primaryColor, thumbUrl, enterIndex, reduceMotion,
|
|
195
|
+
onPress, onLongPress, a11yLabel,
|
|
287
196
|
} = props;
|
|
288
197
|
|
|
289
198
|
const delay = Math.min(enterIndex * STAGGER_PER_CELL_MS, MAX_TOTAL_STAGGER_MS);
|
|
290
199
|
|
|
291
|
-
// Entrance
|
|
292
|
-
//
|
|
293
|
-
|
|
294
|
-
// which no-ops on the plugin-less RN-Web build).
|
|
295
|
-
const opacity = useRef(new Animated.Value(0)).current;
|
|
200
|
+
// Entrance fade, staggered by the cell's grid index. Reduce-motion pins
|
|
201
|
+
// opacity at 1 and never animates.
|
|
202
|
+
const opacity = useRef(new Animated.Value(reduceMotion ? 1 : 0)).current;
|
|
296
203
|
useEffect(() => {
|
|
204
|
+
if (reduceMotion) {
|
|
205
|
+
opacity.setValue(1);
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
297
208
|
const entrance = Animated.timing(opacity, {
|
|
298
209
|
toValue: 1,
|
|
299
210
|
duration: ENTRANCE_DURATION_MS,
|
|
@@ -302,16 +213,18 @@ const PhotoPickerCellAnimated = React.memo(function PhotoPickerCellAnimated(prop
|
|
|
302
213
|
});
|
|
303
214
|
entrance.start();
|
|
304
215
|
return () => entrance.stop();
|
|
305
|
-
}, [opacity, delay]);
|
|
216
|
+
}, [opacity, delay, reduceMotion]);
|
|
306
217
|
|
|
307
|
-
//
|
|
308
|
-
// selected state, springing back to rest.
|
|
218
|
+
// Selection-ring pulse: a quick scale bump each time the cell transitions
|
|
219
|
+
// INTO the selected state, springing back to rest. Reduce-motion holds the
|
|
220
|
+
// ring at rest scale (still tracks selection state so a later un-reduce does
|
|
221
|
+
// not fire a stale pulse).
|
|
309
222
|
const ringScale = useRef(new Animated.Value(1)).current;
|
|
310
223
|
const prevSelected = useRef(isSelected);
|
|
311
224
|
useEffect(() => {
|
|
312
225
|
if (prevSelected.current === isSelected) return;
|
|
313
226
|
prevSelected.current = isSelected;
|
|
314
|
-
if (!isSelected) {
|
|
227
|
+
if (reduceMotion || !isSelected) {
|
|
315
228
|
ringScale.setValue(1);
|
|
316
229
|
return;
|
|
317
230
|
}
|
|
@@ -330,33 +243,13 @@ const PhotoPickerCellAnimated = React.memo(function PhotoPickerCellAnimated(prop
|
|
|
330
243
|
]);
|
|
331
244
|
pulse.start();
|
|
332
245
|
return () => pulse.stop();
|
|
333
|
-
}, [isSelected, ringScale]);
|
|
334
|
-
|
|
335
|
-
// The animated ring reads `ringScale`; its border/radius/position are inline
|
|
336
|
-
// `style` because NativeWind `className` interop is not guaranteed on an
|
|
337
|
-
// `Animated.View` under the RN-Web build.
|
|
338
|
-
const ring = isSelected ? (
|
|
339
|
-
<Animated.View
|
|
340
|
-
pointerEvents="none"
|
|
341
|
-
style={{
|
|
342
|
-
position: 'absolute',
|
|
343
|
-
top: 0,
|
|
344
|
-
right: 0,
|
|
345
|
-
bottom: 0,
|
|
346
|
-
left: 0,
|
|
347
|
-
borderRadius: CELL_CORNER_RADIUS,
|
|
348
|
-
borderWidth: SELECTION_RING_WIDTH,
|
|
349
|
-
borderColor: primaryColor,
|
|
350
|
-
transform: [{ scale: ringScale }],
|
|
351
|
-
}}
|
|
352
|
-
/>
|
|
353
|
-
) : null;
|
|
246
|
+
}, [isSelected, ringScale, reduceMotion]);
|
|
354
247
|
|
|
355
248
|
return (
|
|
356
|
-
//
|
|
357
|
-
// `style
|
|
358
|
-
//
|
|
359
|
-
//
|
|
249
|
+
// The animated wrapper carries the entrance opacity + the layout box as
|
|
250
|
+
// inline `style`: animated values must be inline, and the box's
|
|
251
|
+
// size/margins are runtime pixels className cannot express. `className`
|
|
252
|
+
// drives the static chrome below (`flex-1` fills this fixed-size box).
|
|
360
253
|
<Animated.View
|
|
361
254
|
style={{
|
|
362
255
|
width: size,
|
|
@@ -376,30 +269,50 @@ const PhotoPickerCellAnimated = React.memo(function PhotoPickerCellAnimated(prop
|
|
|
376
269
|
accessibilityLabel={a11yLabel}
|
|
377
270
|
accessibilityState={{ selected: isSelected }}
|
|
378
271
|
>
|
|
379
|
-
<
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
272
|
+
<View
|
|
273
|
+
className={`flex-1 rounded-radius-8 overflow-hidden bg-[#111111]${dim ? ' opacity-60' : ''}`}
|
|
274
|
+
>
|
|
275
|
+
<ExpoImage
|
|
276
|
+
source={{ uri: thumbUrl }}
|
|
277
|
+
style={{ width: '100%', height: '100%' }}
|
|
278
|
+
contentFit="cover"
|
|
279
|
+
transition={120}
|
|
280
|
+
cachePolicy="memory-disk"
|
|
281
|
+
accessibilityLabel={photo.filename}
|
|
282
|
+
/>
|
|
283
|
+
</View>
|
|
284
|
+
{isSelected && (
|
|
285
|
+
<Animated.View
|
|
286
|
+
pointerEvents="none"
|
|
287
|
+
style={{
|
|
288
|
+
position: 'absolute',
|
|
289
|
+
top: 0,
|
|
290
|
+
right: 0,
|
|
291
|
+
bottom: 0,
|
|
292
|
+
left: 0,
|
|
293
|
+
borderRadius: CELL_CORNER_RADIUS,
|
|
294
|
+
borderWidth: SELECTION_RING_WIDTH,
|
|
295
|
+
borderColor: primaryColor,
|
|
296
|
+
transform: [{ scale: ringScale }],
|
|
297
|
+
}}
|
|
298
|
+
/>
|
|
299
|
+
)}
|
|
300
|
+
{isSelected && (
|
|
301
|
+
<View
|
|
302
|
+
pointerEvents="none"
|
|
303
|
+
className="absolute top-1.5 right-1.5 min-w-[22px] h-[22px] px-1.5 rounded-full items-center justify-center"
|
|
304
|
+
style={{ backgroundColor: primaryColor }}
|
|
305
|
+
>
|
|
306
|
+
<Text className="text-white text-[12px] font-bold leading-[14px]">
|
|
307
|
+
{selectionIndex > 0 ? String(selectionIndex) : ''}
|
|
308
|
+
</Text>
|
|
309
|
+
</View>
|
|
310
|
+
)}
|
|
388
311
|
</TouchableOpacity>
|
|
389
312
|
</Animated.View>
|
|
390
313
|
);
|
|
391
314
|
});
|
|
392
315
|
|
|
393
|
-
const PhotoPickerCell = React.memo(function PhotoPickerCell(props: PhotoPickerCellProps) {
|
|
394
|
-
// reduceMotion → the static (unanimated) cell on every platform. Otherwise
|
|
395
|
-
// the RN-`Animated` cell, which now plays on web too (no Reanimated layout
|
|
396
|
-
// entering, so no plugin-less RN-Web warning / no-op).
|
|
397
|
-
if (props.reduceMotion) {
|
|
398
|
-
return <PhotoPickerCellStatic {...props} />;
|
|
399
|
-
}
|
|
400
|
-
return <PhotoPickerCellAnimated {...props} />;
|
|
401
|
-
});
|
|
402
|
-
|
|
403
316
|
const PhotoPickerView: React.FC<PhotoPickerViewProps> = ({
|
|
404
317
|
photos,
|
|
405
318
|
selectedIds,
|
|
@@ -435,13 +348,33 @@ const PhotoPickerView: React.FC<PhotoPickerViewProps> = ({
|
|
|
435
348
|
// then snaps to the sheet width once `onLayout` reports it. The measured
|
|
436
349
|
// wrapper carries ONLY a `style` (no `className`) to maximise the chance
|
|
437
350
|
// `onLayout` fires on web.
|
|
438
|
-
const { width: windowWidth } = useWindowDimensions();
|
|
351
|
+
const { width: windowWidth, height: windowHeight } = useWindowDimensions();
|
|
439
352
|
const [gridWidth, setGridWidth] = useState(0);
|
|
440
353
|
const onRootLayout = useCallback((e: LayoutChangeEvent) => {
|
|
441
354
|
const w = Math.round(e.nativeEvent.layout.width);
|
|
442
355
|
setGridWidth((prev) => (prev === w ? prev : w));
|
|
443
356
|
}, []);
|
|
444
357
|
|
|
358
|
+
// The hosting Dialog uses `{ base: 'bottom', md: 'center' }`. In BOTH
|
|
359
|
+
// placements the panel/sheet has no intrinsic height that a `flex: 1` root
|
|
360
|
+
// could fill down to the FlatList: the centered card hugs its content, and
|
|
361
|
+
// the bottom sheet's own `maxHeight` (several flex levels above this root)
|
|
362
|
+
// does not propagate a definite bound through the chain. So the root owns a
|
|
363
|
+
// `maxHeight` DIRECTLY — the FlatList then scrolls within it when the grid is
|
|
364
|
+
// long, and the panel hugs its content when the grid is short (no huge empty
|
|
365
|
+
// panel). The cap is tighter for the centered card (a floating modal reads
|
|
366
|
+
// better below full height); the bottom sheet may run taller. Keep this
|
|
367
|
+
// breakpoint in sync with `DIALOG_PLACEMENT` in BottomSheetRouter.
|
|
368
|
+
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
|
+
}),
|
|
375
|
+
[isCenteredPanel, windowHeight],
|
|
376
|
+
);
|
|
377
|
+
|
|
445
378
|
const effectiveWidth = gridWidth > 0 ? gridWidth : windowWidth;
|
|
446
379
|
const { columns, cellSize, gutter } = useMemo(
|
|
447
380
|
() => computePhotoGridLayout(effectiveWidth),
|
|
@@ -571,17 +504,19 @@ const PhotoPickerView: React.FC<PhotoPickerViewProps> = ({
|
|
|
571
504
|
|
|
572
505
|
return (
|
|
573
506
|
// Measured wrapper: `style`-only (no className) so RN-Web fires onLayout.
|
|
574
|
-
// `
|
|
575
|
-
//
|
|
576
|
-
//
|
|
577
|
-
//
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
507
|
+
// `rootStyle` is placement-aware — a `maxHeight` cap that gives the
|
|
508
|
+
// FlatList a bounded scroll region while letting a short grid hug its
|
|
509
|
+
// content (see the `rootStyle` derivation). It must NOT carry a className
|
|
510
|
+
// (that can suppress web `onLayout`).
|
|
511
|
+
<View style={rootStyle} onLayout={onRootLayout}>
|
|
512
|
+
{/* `min-h-0` lets this flex child shrink below its content on web so
|
|
513
|
+
the FlatList gets a bounded scroll region inside the height-capped
|
|
514
|
+
root (the CSS flexbox `min-height:auto` trap; a no-op on native,
|
|
515
|
+
where Yoga already defaults min-height to 0). */}
|
|
516
|
+
<View className="flex-1 min-h-0 bg-black">
|
|
517
|
+
{/* Photo grid (renders behind the translucent header). */}
|
|
583
518
|
{isEmpty ? (
|
|
584
|
-
<View className="flex-1 items-center justify-center px-space-32 pt-[
|
|
519
|
+
<View className="flex-1 items-center justify-center px-space-32 pt-[60px]">
|
|
585
520
|
<View className="opacity-30 mb-space-16">
|
|
586
521
|
<MaterialCommunityIcons name="image-outline" size={64} color="#FFFFFF" />
|
|
587
522
|
</View>
|
|
@@ -621,9 +556,8 @@ const PhotoPickerView: React.FC<PhotoPickerViewProps> = ({
|
|
|
621
556
|
renderItem={renderItem}
|
|
622
557
|
keyExtractor={keyExtractor}
|
|
623
558
|
numColumns={columns}
|
|
624
|
-
className="flex-1"
|
|
625
|
-
|
|
626
|
-
contentContainerClassName="pt-[88px] pb-space-24"
|
|
559
|
+
className="flex-1 min-h-0"
|
|
560
|
+
contentContainerClassName="pt-[60px] pb-space-24"
|
|
627
561
|
showsVerticalScrollIndicator={false}
|
|
628
562
|
refreshControl={
|
|
629
563
|
<RefreshControl
|
|
@@ -643,11 +577,12 @@ const PhotoPickerView: React.FC<PhotoPickerViewProps> = ({
|
|
|
643
577
|
/>
|
|
644
578
|
)}
|
|
645
579
|
|
|
646
|
-
{/* Translucent black header
|
|
647
|
-
the
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
580
|
+
{/* Translucent black header floating over the grid. The Dialog owns
|
|
581
|
+
the drag handle (a floating pill above this bar in bottom
|
|
582
|
+
placement, drawn at a higher z-index; none in center), so the
|
|
583
|
+
header no longer reserves the 28dp handle strip — it is just the
|
|
584
|
+
56dp app bar (`min-h-[56px]`). */}
|
|
585
|
+
<View className="absolute top-0 left-0 right-0 flex-row items-center justify-between px-space-12 z-30 bg-[#000000EB] min-h-[56px]">
|
|
651
586
|
<View className="flex-row items-center justify-between w-full h-14">
|
|
652
587
|
<View className="basis-0 grow flex-row items-center justify-start">
|
|
653
588
|
<TouchableOpacity
|