@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
|
@@ -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,77 +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
|
-
{/* Inline `flex: 1` is load-bearing on web and NOT interchangeable with
|
|
194
|
-
the `flex-1` class: on RN-Web the NativeWind/react-native-css `flex-1`
|
|
195
|
-
utility does not reliably resolve to `flex: 1` (the class is present in
|
|
196
|
-
the DOM but inert — RN-Web's base View atomic styles out-rank it), so
|
|
197
|
-
this content View collapses to 0 height and the image (`height: 100%` of
|
|
198
|
-
a 0-height box) becomes invisible — every tile blank. Inline style always
|
|
199
|
-
wins, so it is the sanctioned exception here. Native is unaffected. */}
|
|
200
|
-
<View
|
|
201
|
-
style={{ flex: 1 }}
|
|
202
|
-
className={`flex-1 rounded-radius-8 overflow-hidden bg-[#111111]${dim ? ' opacity-60' : ''}`}
|
|
203
|
-
>
|
|
204
|
-
<ExpoImage
|
|
205
|
-
source={{ uri: thumbUrl }}
|
|
206
|
-
style={{ width: '100%', height: '100%' }}
|
|
207
|
-
contentFit="cover"
|
|
208
|
-
transition={120}
|
|
209
|
-
cachePolicy="memory-disk"
|
|
210
|
-
accessibilityLabel={photo.filename}
|
|
211
|
-
/>
|
|
212
|
-
</View>
|
|
213
|
-
{ring}
|
|
214
|
-
{isSelected && (
|
|
215
|
-
<View
|
|
216
|
-
pointerEvents="none"
|
|
217
|
-
className="absolute top-1.5 right-1.5 min-w-[22px] h-[22px] px-1.5 rounded-full items-center justify-center"
|
|
218
|
-
style={{ backgroundColor: primaryColor }}
|
|
219
|
-
>
|
|
220
|
-
<Text className="text-white text-[12px] font-bold leading-[14px]">
|
|
221
|
-
{selectionIndex > 0 ? String(selectionIndex) : ''}
|
|
222
|
-
</Text>
|
|
223
|
-
</View>
|
|
224
|
-
)}
|
|
225
|
-
</>
|
|
226
|
-
);
|
|
227
|
-
}
|
|
228
|
-
|
|
229
164
|
type PhotoPickerCellProps = {
|
|
230
165
|
photo: FileMetadata;
|
|
231
166
|
size: number;
|
|
@@ -243,65 +178,33 @@ type PhotoPickerCellProps = {
|
|
|
243
178
|
a11yLabel: string;
|
|
244
179
|
};
|
|
245
180
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
};
|
|
258
|
-
|
|
259
|
-
const ring = isSelected ? (
|
|
260
|
-
<View
|
|
261
|
-
pointerEvents="none"
|
|
262
|
-
className="absolute inset-0 rounded-radius-8 border-[3px]"
|
|
263
|
-
style={{ borderColor: primaryColor }}
|
|
264
|
-
/>
|
|
265
|
-
) : null;
|
|
266
|
-
|
|
267
|
-
return (
|
|
268
|
-
<TouchableOpacity
|
|
269
|
-
activeOpacity={0.85}
|
|
270
|
-
onPress={onPress}
|
|
271
|
-
onLongPress={onLongPress}
|
|
272
|
-
className="relative"
|
|
273
|
-
style={cellWrapperStyle}
|
|
274
|
-
accessibilityRole="button"
|
|
275
|
-
accessibilityLabel={a11yLabel}
|
|
276
|
-
accessibilityState={{ selected: isSelected }}
|
|
277
|
-
>
|
|
278
|
-
<PhotoPickerCellContent
|
|
279
|
-
photo={photo}
|
|
280
|
-
dim={dim}
|
|
281
|
-
isSelected={isSelected}
|
|
282
|
-
selectionIndex={selectionIndex}
|
|
283
|
-
primaryColor={primaryColor}
|
|
284
|
-
thumbUrl={thumbUrl}
|
|
285
|
-
ring={ring}
|
|
286
|
-
/>
|
|
287
|
-
</TouchableOpacity>
|
|
288
|
-
);
|
|
289
|
-
});
|
|
290
|
-
|
|
291
|
-
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) {
|
|
292
192
|
const {
|
|
293
193
|
photo, size, marginRight, marginBottom, isSelected, selectionIndex,
|
|
294
|
-
dim, primaryColor, thumbUrl, enterIndex,
|
|
194
|
+
dim, primaryColor, thumbUrl, enterIndex, reduceMotion,
|
|
195
|
+
onPress, onLongPress, a11yLabel,
|
|
295
196
|
} = props;
|
|
296
197
|
|
|
297
198
|
const delay = Math.min(enterIndex * STAGGER_PER_CELL_MS, MAX_TOTAL_STAGGER_MS);
|
|
298
199
|
|
|
299
|
-
// Entrance
|
|
300
|
-
//
|
|
301
|
-
|
|
302
|
-
// which no-ops on the plugin-less RN-Web build).
|
|
303
|
-
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;
|
|
304
203
|
useEffect(() => {
|
|
204
|
+
if (reduceMotion) {
|
|
205
|
+
opacity.setValue(1);
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
305
208
|
const entrance = Animated.timing(opacity, {
|
|
306
209
|
toValue: 1,
|
|
307
210
|
duration: ENTRANCE_DURATION_MS,
|
|
@@ -310,16 +213,18 @@ const PhotoPickerCellAnimated = React.memo(function PhotoPickerCellAnimated(prop
|
|
|
310
213
|
});
|
|
311
214
|
entrance.start();
|
|
312
215
|
return () => entrance.stop();
|
|
313
|
-
}, [opacity, delay]);
|
|
216
|
+
}, [opacity, delay, reduceMotion]);
|
|
314
217
|
|
|
315
|
-
//
|
|
316
|
-
// 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).
|
|
317
222
|
const ringScale = useRef(new Animated.Value(1)).current;
|
|
318
223
|
const prevSelected = useRef(isSelected);
|
|
319
224
|
useEffect(() => {
|
|
320
225
|
if (prevSelected.current === isSelected) return;
|
|
321
226
|
prevSelected.current = isSelected;
|
|
322
|
-
if (!isSelected) {
|
|
227
|
+
if (reduceMotion || !isSelected) {
|
|
323
228
|
ringScale.setValue(1);
|
|
324
229
|
return;
|
|
325
230
|
}
|
|
@@ -338,33 +243,13 @@ const PhotoPickerCellAnimated = React.memo(function PhotoPickerCellAnimated(prop
|
|
|
338
243
|
]);
|
|
339
244
|
pulse.start();
|
|
340
245
|
return () => pulse.stop();
|
|
341
|
-
}, [isSelected, ringScale]);
|
|
342
|
-
|
|
343
|
-
// The animated ring reads `ringScale`; its border/radius/position are inline
|
|
344
|
-
// `style` because NativeWind `className` interop is not guaranteed on an
|
|
345
|
-
// `Animated.View` under the RN-Web build.
|
|
346
|
-
const ring = isSelected ? (
|
|
347
|
-
<Animated.View
|
|
348
|
-
pointerEvents="none"
|
|
349
|
-
style={{
|
|
350
|
-
position: 'absolute',
|
|
351
|
-
top: 0,
|
|
352
|
-
right: 0,
|
|
353
|
-
bottom: 0,
|
|
354
|
-
left: 0,
|
|
355
|
-
borderRadius: CELL_CORNER_RADIUS,
|
|
356
|
-
borderWidth: SELECTION_RING_WIDTH,
|
|
357
|
-
borderColor: primaryColor,
|
|
358
|
-
transform: [{ scale: ringScale }],
|
|
359
|
-
}}
|
|
360
|
-
/>
|
|
361
|
-
) : null;
|
|
246
|
+
}, [isSelected, ringScale, reduceMotion]);
|
|
362
247
|
|
|
363
248
|
return (
|
|
364
|
-
//
|
|
365
|
-
// `style
|
|
366
|
-
//
|
|
367
|
-
//
|
|
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).
|
|
368
253
|
<Animated.View
|
|
369
254
|
style={{
|
|
370
255
|
width: size,
|
|
@@ -375,44 +260,59 @@ const PhotoPickerCellAnimated = React.memo(function PhotoPickerCellAnimated(prop
|
|
|
375
260
|
opacity,
|
|
376
261
|
}}
|
|
377
262
|
>
|
|
378
|
-
{/* Inline `flex: 1` (not just `className="flex-1"`): on RN-Web the
|
|
379
|
-
`flex-1` utility is inert on this TouchableOpacity, so the button would
|
|
380
|
-
collapse to 0 height inside the fixed-size wrapper and hide the tile.
|
|
381
|
-
See PhotoPickerCellContent for the full rationale. */}
|
|
382
263
|
<TouchableOpacity
|
|
383
264
|
activeOpacity={0.85}
|
|
384
265
|
onPress={onPress}
|
|
385
266
|
onLongPress={onLongPress}
|
|
386
|
-
style={{ flex: 1 }}
|
|
387
267
|
className="flex-1"
|
|
388
268
|
accessibilityRole="button"
|
|
389
269
|
accessibilityLabel={a11yLabel}
|
|
390
270
|
accessibilityState={{ selected: isSelected }}
|
|
391
271
|
>
|
|
392
|
-
<
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
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
|
+
)}
|
|
401
311
|
</TouchableOpacity>
|
|
402
312
|
</Animated.View>
|
|
403
313
|
);
|
|
404
314
|
});
|
|
405
315
|
|
|
406
|
-
const PhotoPickerCell = React.memo(function PhotoPickerCell(props: PhotoPickerCellProps) {
|
|
407
|
-
// reduceMotion → the static (unanimated) cell on every platform. Otherwise
|
|
408
|
-
// the RN-`Animated` cell, which now plays on web too (no Reanimated layout
|
|
409
|
-
// entering, so no plugin-less RN-Web warning / no-op).
|
|
410
|
-
if (props.reduceMotion) {
|
|
411
|
-
return <PhotoPickerCellStatic {...props} />;
|
|
412
|
-
}
|
|
413
|
-
return <PhotoPickerCellAnimated {...props} />;
|
|
414
|
-
});
|
|
415
|
-
|
|
416
316
|
const PhotoPickerView: React.FC<PhotoPickerViewProps> = ({
|
|
417
317
|
photos,
|
|
418
318
|
selectedIds,
|
|
@@ -448,13 +348,33 @@ const PhotoPickerView: React.FC<PhotoPickerViewProps> = ({
|
|
|
448
348
|
// then snaps to the sheet width once `onLayout` reports it. The measured
|
|
449
349
|
// wrapper carries ONLY a `style` (no `className`) to maximise the chance
|
|
450
350
|
// `onLayout` fires on web.
|
|
451
|
-
const { width: windowWidth } = useWindowDimensions();
|
|
351
|
+
const { width: windowWidth, height: windowHeight } = useWindowDimensions();
|
|
452
352
|
const [gridWidth, setGridWidth] = useState(0);
|
|
453
353
|
const onRootLayout = useCallback((e: LayoutChangeEvent) => {
|
|
454
354
|
const w = Math.round(e.nativeEvent.layout.width);
|
|
455
355
|
setGridWidth((prev) => (prev === w ? prev : w));
|
|
456
356
|
}, []);
|
|
457
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
|
+
|
|
458
378
|
const effectiveWidth = gridWidth > 0 ? gridWidth : windowWidth;
|
|
459
379
|
const { columns, cellSize, gutter } = useMemo(
|
|
460
380
|
() => computePhotoGridLayout(effectiveWidth),
|
|
@@ -584,27 +504,19 @@ const PhotoPickerView: React.FC<PhotoPickerViewProps> = ({
|
|
|
584
504
|
|
|
585
505
|
return (
|
|
586
506
|
// Measured wrapper: `style`-only (no className) so RN-Web fires onLayout.
|
|
587
|
-
//
|
|
588
|
-
//
|
|
589
|
-
//
|
|
590
|
-
//
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
// content (the list overflows the clamp and is clipped — "renders half", no
|
|
599
|
-
// scroll). RN-Web emits inline `style` flex as a HIGHER-precedence atomic class
|
|
600
|
-
// that beats the base reset, so inline `flex: 1` reliably yields `flex: 1 1 0%`.
|
|
601
|
-
// No explicit `min-height: 0` is needed: RN-Web's base View already sets it (and
|
|
602
|
-
// native Yoga defaults min to 0), so `flex: 1` alone shrinks into the clamp.
|
|
603
|
-
<View style={{ flex: 1 }} onLayout={onRootLayout}>
|
|
604
|
-
<View className="flex-1 bg-black" style={{ flex: 1 }}>
|
|
605
|
-
{/* Photo grid (renders behind translucent header) */}
|
|
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). */}
|
|
606
518
|
{isEmpty ? (
|
|
607
|
-
<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]">
|
|
608
520
|
<View className="opacity-30 mb-space-16">
|
|
609
521
|
<MaterialCommunityIcons name="image-outline" size={64} color="#FFFFFF" />
|
|
610
522
|
</View>
|
|
@@ -644,9 +556,8 @@ const PhotoPickerView: React.FC<PhotoPickerViewProps> = ({
|
|
|
644
556
|
renderItem={renderItem}
|
|
645
557
|
keyExtractor={keyExtractor}
|
|
646
558
|
numColumns={columns}
|
|
647
|
-
className="flex-1"
|
|
648
|
-
|
|
649
|
-
contentContainerClassName="pt-[88px] pb-space-24"
|
|
559
|
+
className="flex-1 min-h-0"
|
|
560
|
+
contentContainerClassName="pt-[60px] pb-space-24"
|
|
650
561
|
showsVerticalScrollIndicator={false}
|
|
651
562
|
refreshControl={
|
|
652
563
|
<RefreshControl
|
|
@@ -666,19 +577,12 @@ const PhotoPickerView: React.FC<PhotoPickerViewProps> = ({
|
|
|
666
577
|
/>
|
|
667
578
|
)}
|
|
668
579
|
|
|
669
|
-
{/* Translucent black header
|
|
670
|
-
the
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
base View forces `position: relative; z-index: 0`), so without the
|
|
676
|
-
inline overrides the header renders in normal flow below the grid
|
|
677
|
-
instead of floating over it. Inline wins; native is unchanged. */}
|
|
678
|
-
<View
|
|
679
|
-
style={{ position: 'absolute', top: 0, left: 0, right: 0, zIndex: 30 }}
|
|
680
|
-
className="absolute top-0 left-0 right-0 flex-row items-center justify-between px-space-12 z-30 bg-[#000000EB] pt-[28px] min-h-[84px]"
|
|
681
|
-
>
|
|
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]">
|
|
682
586
|
<View className="flex-row items-center justify-between w-full h-14">
|
|
683
587
|
<View className="basis-0 grow flex-row items-center justify-start">
|
|
684
588
|
<TouchableOpacity
|