@oxyhq/services 22.12.1 → 22.12.2

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 (55) hide show
  1. package/lib/commonjs/ui/components/FollowButton.js +5 -8
  2. package/lib/commonjs/ui/components/FollowButton.js.map +1 -1
  3. package/lib/commonjs/ui/navigation/surfaceNavStack.js +8 -7
  4. package/lib/commonjs/ui/navigation/surfaceNavStack.js.map +1 -1
  5. package/lib/commonjs/ui/screens/ChangeAvatarScreen.js +48 -42
  6. package/lib/commonjs/ui/screens/ChangeAvatarScreen.js.map +1 -1
  7. package/lib/commonjs/ui/screens/CreateAccountScreen.js +55 -98
  8. package/lib/commonjs/ui/screens/CreateAccountScreen.js.map +1 -1
  9. package/lib/commonjs/ui/screens/FileManagementScreen.js +21 -8
  10. package/lib/commonjs/ui/screens/FileManagementScreen.js.map +1 -1
  11. package/lib/commonjs/ui/screens/fileManagement/hooks/useFileUploadState.js +14 -6
  12. package/lib/commonjs/ui/screens/fileManagement/hooks/useFileUploadState.js.map +1 -1
  13. package/lib/module/ui/components/FollowButton.js +5 -8
  14. package/lib/module/ui/components/FollowButton.js.map +1 -1
  15. package/lib/module/ui/navigation/surfaceNavStack.js +8 -7
  16. package/lib/module/ui/navigation/surfaceNavStack.js.map +1 -1
  17. package/lib/module/ui/screens/ChangeAvatarScreen.js +48 -42
  18. package/lib/module/ui/screens/ChangeAvatarScreen.js.map +1 -1
  19. package/lib/module/ui/screens/CreateAccountScreen.js +58 -101
  20. package/lib/module/ui/screens/CreateAccountScreen.js.map +1 -1
  21. package/lib/module/ui/screens/FileManagementScreen.js +21 -8
  22. package/lib/module/ui/screens/FileManagementScreen.js.map +1 -1
  23. package/lib/module/ui/screens/fileManagement/hooks/useFileUploadState.js +14 -6
  24. package/lib/module/ui/screens/fileManagement/hooks/useFileUploadState.js.map +1 -1
  25. package/lib/typescript/commonjs/ui/components/FollowButton.d.ts.map +1 -1
  26. package/lib/typescript/commonjs/ui/navigation/surfaceNavStack.d.ts +7 -4
  27. package/lib/typescript/commonjs/ui/navigation/surfaceNavStack.d.ts.map +1 -1
  28. package/lib/typescript/commonjs/ui/screens/ChangeAvatarScreen.d.ts +9 -5
  29. package/lib/typescript/commonjs/ui/screens/ChangeAvatarScreen.d.ts.map +1 -1
  30. package/lib/typescript/commonjs/ui/screens/CreateAccountScreen.d.ts.map +1 -1
  31. package/lib/typescript/commonjs/ui/screens/FileManagementScreen.d.ts.map +1 -1
  32. package/lib/typescript/commonjs/ui/screens/fileManagement/hooks/useFileUploadState.d.ts +3 -1
  33. package/lib/typescript/commonjs/ui/screens/fileManagement/hooks/useFileUploadState.d.ts.map +1 -1
  34. package/lib/typescript/commonjs/ui/types/fileManagement.d.ts +10 -0
  35. package/lib/typescript/commonjs/ui/types/fileManagement.d.ts.map +1 -1
  36. package/lib/typescript/module/ui/components/FollowButton.d.ts.map +1 -1
  37. package/lib/typescript/module/ui/navigation/surfaceNavStack.d.ts +7 -4
  38. package/lib/typescript/module/ui/navigation/surfaceNavStack.d.ts.map +1 -1
  39. package/lib/typescript/module/ui/screens/ChangeAvatarScreen.d.ts +9 -5
  40. package/lib/typescript/module/ui/screens/ChangeAvatarScreen.d.ts.map +1 -1
  41. package/lib/typescript/module/ui/screens/CreateAccountScreen.d.ts.map +1 -1
  42. package/lib/typescript/module/ui/screens/FileManagementScreen.d.ts.map +1 -1
  43. package/lib/typescript/module/ui/screens/fileManagement/hooks/useFileUploadState.d.ts +3 -1
  44. package/lib/typescript/module/ui/screens/fileManagement/hooks/useFileUploadState.d.ts.map +1 -1
  45. package/lib/typescript/module/ui/types/fileManagement.d.ts +10 -0
  46. package/lib/typescript/module/ui/types/fileManagement.d.ts.map +1 -1
  47. package/package.json +4 -4
  48. package/src/ui/components/FollowButton.tsx +5 -8
  49. package/src/ui/navigation/__tests__/surfaceNavStack.test.ts +49 -17
  50. package/src/ui/navigation/surfaceNavStack.ts +15 -11
  51. package/src/ui/screens/ChangeAvatarScreen.tsx +46 -39
  52. package/src/ui/screens/CreateAccountScreen.tsx +123 -167
  53. package/src/ui/screens/FileManagementScreen.tsx +21 -8
  54. package/src/ui/screens/fileManagement/hooks/useFileUploadState.ts +16 -6
  55. package/src/ui/types/fileManagement.ts +10 -0
@@ -1,10 +1,6 @@
1
1
  import type React from 'react';
2
- import { useState, useCallback, useRef, useEffect } from 'react';
3
- import {
4
- View,
5
- ActivityIndicator,
6
- TouchableOpacity,
7
- } from 'react-native';
2
+ import { useState, useCallback, useRef } from 'react';
3
+ import { View, ActivityIndicator } from 'react-native';
8
4
  import { Ionicons } from '@expo/vector-icons';
9
5
  import type { AccountKind, CreateAccountInput, OrganizationCategory } from '@oxyhq/core';
10
6
  import { ORGANIZATION_CATEGORIES } from '@oxyhq/core';
@@ -15,6 +11,7 @@ import { useTheme } from '@oxyhq/bloom/theme';
15
11
  import { Text } from '@oxyhq/bloom/typography';
16
12
  import { Button } from '@oxyhq/bloom/button';
17
13
  import { TextField, TextFieldInput } from '@oxyhq/bloom/text-field';
14
+ import { SettingsListGroup, SettingsListItem } from '@oxyhq/bloom/settings-list';
18
15
  import { useOxy } from '../context/OxyContext';
19
16
  import { toast } from '@oxyhq/bloom';
20
17
 
@@ -172,15 +169,6 @@ const CreateAccountScreen: React.FC<BaseScreenProps> = ({
172
169
  checkUsername(cleaned);
173
170
  }, [checkUsername]);
174
171
 
175
- // Cleanup debounce timer
176
- useEffect(() => {
177
- return () => {
178
- if (debounceTimerRef.current) {
179
- clearTimeout(debounceTimerRef.current);
180
- }
181
- };
182
- }, []);
183
-
184
172
  const canCreate = usernameStatus === 'available' && displayName.trim().length > 0 && !isCreating;
185
173
 
186
174
  const handleCreate = useCallback(async () => {
@@ -234,168 +222,136 @@ const CreateAccountScreen: React.FC<BaseScreenProps> = ({
234
222
  const title = t('accounts.create.title') || 'Create account';
235
223
 
236
224
  return (
237
- <>
238
-
239
- <View className="px-screen-margin pt-space-16 pb-space-32 gap-space-24">
240
-
241
- {/* Kind picker */}
225
+ <View className="px-screen-margin pt-space-16 pb-space-32">
226
+
227
+ {/* Account type canonical grouped selection rows (checkmark on the chosen one) */}
228
+ <SettingsListGroup title={t('accounts.create.typeSection') || 'Account type'}>
229
+ {KIND_OPTIONS.map((option) => {
230
+ const selected = option.value === kind;
231
+ return (
232
+ <SettingsListItem
233
+ key={option.value}
234
+ icon={(
235
+ <Ionicons
236
+ name={option.icon}
237
+ size={22}
238
+ color={selected ? bloomTheme.colors.primary : bloomTheme.colors.icon}
239
+ />
240
+ )}
241
+ title={kindLabel(t, option.value)}
242
+ description={kindDescription(t, option.value)}
243
+ onPress={() => setKind(option.value)}
244
+ showChevron={false}
245
+ rightElement={selected ? (
246
+ <Ionicons name="checkmark-circle" size={20} color={bloomTheme.colors.primary} />
247
+ ) : undefined}
248
+ accessibilityLabel={kindLabel(t, option.value)}
249
+ />
250
+ );
251
+ })}
252
+ </SettingsListGroup>
253
+
254
+ {/* Organization category — grouped selection rows, shown only for organizations */}
255
+ {kind === 'organization' ? (
256
+ <SettingsListGroup title={t('accounts.create.organizationCategory.label')}>
257
+ {ORGANIZATION_CATEGORY_OPTIONS.map((option) => {
258
+ const selected = option === organizationCategory;
259
+ return (
260
+ <SettingsListItem
261
+ key={option}
262
+ title={organizationCategoryLabel(t, option)}
263
+ onPress={() => setOrganizationCategory(option)}
264
+ showChevron={false}
265
+ rightElement={selected ? (
266
+ <Ionicons name="checkmark-circle" size={20} color={bloomTheme.colors.primary} />
267
+ ) : undefined}
268
+ accessibilityLabel={organizationCategoryLabel(t, option)}
269
+ />
270
+ );
271
+ })}
272
+ </SettingsListGroup>
273
+ ) : null}
274
+
275
+ {/* Details — a grouped section card hosting the form fields */}
276
+ <SettingsListGroup title={t('accounts.create.detailsSection') || 'Details'}>
277
+ <View className="p-space-16 gap-space-16">
278
+ {/* Username */}
242
279
  <View className="gap-space-8">
243
- {KIND_OPTIONS.map((option) => {
244
- const selected = option.value === kind;
245
- return (
246
- <TouchableOpacity
247
- key={option.value}
248
- accessibilityRole="radio"
249
- accessibilityState={{ selected }}
250
- accessibilityLabel={kindLabel(t, option.value)}
251
- onPress={() => setKind(option.value)}
252
- activeOpacity={0.7}
253
- className="flex-row items-center gap-space-12 p-space-16 rounded-radius-20"
254
- style={{
255
- backgroundColor: selected ? bloomTheme.colors.primarySubtle : bloomTheme.colors.card,
256
- borderWidth: 1,
257
- borderColor: selected ? bloomTheme.colors.primary : bloomTheme.colors.border,
258
- }}
259
- >
260
- <Ionicons
261
- name={option.icon}
262
- size={22}
263
- color={selected ? bloomTheme.colors.primary : bloomTheme.colors.icon}
264
- />
265
- <View className="flex-1">
266
- <Text
267
- className="text-body font-bodyBold"
268
- style={{ color: selected ? bloomTheme.colors.primary : bloomTheme.colors.text }}
269
- >
270
- {kindLabel(t, option.value)}
271
- </Text>
272
- <Text className="text-caption font-caption text-text-secondary">
273
- {kindDescription(t, option.value)}
274
- </Text>
275
- </View>
276
- {selected ? (
277
- <Ionicons name="checkmark-circle" size={20} color={bloomTheme.colors.primary} />
278
- ) : null}
279
- </TouchableOpacity>
280
- );
281
- })}
280
+ <TextField isInvalid={usernameIsInvalid}>
281
+ <TextFieldInput
282
+ floatingLabel
283
+ label={t('accounts.create.username.label') || 'Username'}
284
+ value={username}
285
+ onChangeText={handleUsernameChange}
286
+ isInvalid={usernameIsInvalid}
287
+ autoCapitalize="none"
288
+ autoCorrect={false}
289
+ autoComplete="off"
290
+ maxLength={USERNAME_MAX}
291
+ />
292
+ </TextField>
293
+ {(usernameStatus === 'checking' || usernameMessage) ? (
294
+ <View className="flex-row items-center gap-space-4 px-space-4">
295
+ {usernameStatus === 'checking' ? (
296
+ <ActivityIndicator size="small" color={bloomTheme.colors.primary} />
297
+ ) : usernameStatus === 'available' ? (
298
+ <Ionicons name="checkmark-circle" size={16} color={bloomTheme.colors.success} />
299
+ ) : usernameIsInvalid ? (
300
+ <Ionicons name="alert-circle" size={16} color={bloomTheme.colors.negative} />
301
+ ) : null}
302
+ {usernameMessage ? (
303
+ <Text className="text-caption font-caption" style={{ color: statusColor }}>
304
+ {usernameMessage}
305
+ </Text>
306
+ ) : null}
307
+ </View>
308
+ ) : null}
282
309
  </View>
283
310
 
284
- {kind === 'organization' ? (
285
- <View className="gap-space-8">
286
- <Text className="text-body font-bodyBold text-text">
287
- {t('accounts.create.organizationCategory.label')}
288
- </Text>
289
- <View className="flex-row flex-wrap gap-space-8">
290
- {ORGANIZATION_CATEGORY_OPTIONS.map((option) => {
291
- const selected = option === organizationCategory;
292
- return (
293
- <TouchableOpacity
294
- key={option}
295
- accessibilityRole="radio"
296
- accessibilityState={{ selected }}
297
- accessibilityLabel={organizationCategoryLabel(t, option)}
298
- onPress={() => setOrganizationCategory(option)}
299
- activeOpacity={0.7}
300
- className="px-space-12 py-space-8 rounded-radius-full"
301
- style={{
302
- backgroundColor: selected ? bloomTheme.colors.primarySubtle : bloomTheme.colors.card,
303
- borderWidth: 1,
304
- borderColor: selected ? bloomTheme.colors.primary : bloomTheme.colors.border,
305
- }}
306
- >
307
- <Text
308
- className="text-caption font-captionBold"
309
- style={{ color: selected ? bloomTheme.colors.primary : bloomTheme.colors.text }}
310
- >
311
- {organizationCategoryLabel(t, option)}
312
- </Text>
313
- </TouchableOpacity>
314
- );
315
- })}
316
- </View>
317
- </View>
318
- ) : null}
319
-
320
- {/* Form Content */}
321
- <View className="gap-space-16 p-space-16 rounded-radius-20 bg-fill">
322
- {/* Username */}
323
- <View className="gap-space-8">
324
- <TextField isInvalid={usernameIsInvalid}>
325
- <TextFieldInput
326
- floatingLabel
327
- label={t('accounts.create.username.label') || 'Username'}
328
- value={username}
329
- onChangeText={handleUsernameChange}
330
- isInvalid={usernameIsInvalid}
331
- autoCapitalize="none"
332
- autoCorrect={false}
333
- autoComplete="off"
334
- maxLength={USERNAME_MAX}
335
- />
336
- </TextField>
337
- {(usernameStatus === 'checking' || usernameMessage) ? (
338
- <View className="flex-row items-center gap-space-4 px-space-4">
339
- {usernameStatus === 'checking' ? (
340
- <ActivityIndicator size="small" color={bloomTheme.colors.primary} />
341
- ) : usernameStatus === 'available' ? (
342
- <Ionicons name="checkmark-circle" size={16} color={bloomTheme.colors.success} />
343
- ) : usernameIsInvalid ? (
344
- <Ionicons name="alert-circle" size={16} color={bloomTheme.colors.negative} />
345
- ) : null}
346
- {usernameMessage ? (
347
- <Text className="text-caption font-caption" style={{ color: statusColor }}>
348
- {usernameMessage}
349
- </Text>
350
- ) : null}
351
- </View>
352
- ) : null}
353
- </View>
354
-
355
- {/* Display Name */}
311
+ {/* Display Name */}
312
+ <TextField>
313
+ <TextFieldInput
314
+ floatingLabel
315
+ label={t('accounts.create.displayName.label') || 'Display name'}
316
+ value={displayName}
317
+ onChangeText={setDisplayName}
318
+ maxLength={DISPLAY_NAME_MAX}
319
+ />
320
+ </TextField>
321
+
322
+ {/* Bio */}
323
+ <View className="gap-space-4">
356
324
  <TextField>
357
325
  <TextFieldInput
358
326
  floatingLabel
359
- label={t('accounts.create.displayName.label') || 'Display name'}
360
- value={displayName}
361
- onChangeText={setDisplayName}
362
- maxLength={DISPLAY_NAME_MAX}
327
+ label={t('accounts.create.bio.label') || 'Bio (optional)'}
328
+ value={bio}
329
+ onChangeText={setBio}
330
+ maxLength={BIO_MAX}
331
+ multiline
332
+ numberOfLines={3}
333
+ textAlignVertical="top"
363
334
  />
364
335
  </TextField>
365
-
366
- {/* Bio */}
367
- <View className="gap-space-4">
368
- <TextField>
369
- <TextFieldInput
370
- floatingLabel
371
- label={t('accounts.create.bio.label') || 'Bio (optional)'}
372
- value={bio}
373
- onChangeText={setBio}
374
- maxLength={BIO_MAX}
375
- multiline
376
- numberOfLines={3}
377
- textAlignVertical="top"
378
- />
379
- </TextField>
380
- <Text className="text-caption font-caption text-text-tertiary px-space-4 text-right">
381
- {bio.length}/{BIO_MAX}
382
- </Text>
383
- </View>
336
+ <Text className="text-caption font-caption text-text-tertiary px-space-4 text-right">
337
+ {bio.length}/{BIO_MAX}
338
+ </Text>
384
339
  </View>
385
-
386
- {/* Create Button */}
387
- <Button
388
- variant="primary"
389
- onPress={handleCreate}
390
- disabled={!canCreate}
391
- loading={isCreating}
392
- accessibilityLabel={title}
393
- className="w-full"
394
- >
395
- {title}
396
- </Button>
397
340
  </View>
398
- </>
341
+ </SettingsListGroup>
342
+
343
+ {/* Create Button */}
344
+ <Button
345
+ variant="primary"
346
+ onPress={handleCreate}
347
+ disabled={!canCreate}
348
+ loading={isCreating}
349
+ accessibilityLabel={title}
350
+ className="w-full"
351
+ >
352
+ {title}
353
+ </Button>
354
+ </View>
399
355
  );
400
356
  };
401
357
 
@@ -88,6 +88,7 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
88
88
  selectMode = false,
89
89
  multiSelect = false,
90
90
  onSelect,
91
+ onPicked,
91
92
  onConfirmSelection,
92
93
  initialSelectedIds = [],
93
94
  maxSelection,
@@ -305,7 +306,13 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
305
306
  }
306
307
 
307
308
  if (!multiSelect) {
308
- if (onSelect) {
309
+ if (onPicked) {
310
+ // Forward-navigation picker (e.g. the avatar flow): hand the file
311
+ // to the caller, which pushes the NEXT frame (the cropper) on top
312
+ // of this picker. The picker frame stays below so Back returns here
313
+ // to re-pick — no dismiss, no session close.
314
+ onPicked(file);
315
+ } else if (onSelect) {
309
316
  // Legacy callback caller (inbox, WelcomeNewUser): keep the
310
317
  // callback + afterSelect behaviour unchanged.
311
318
  onSelect(file);
@@ -315,8 +322,8 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
315
322
  onClose?.();
316
323
  }
317
324
  } else {
318
- // Promise-based picker (avatar picker): resolve the surface's
319
- // present() promise with the picked file.
325
+ // Promise-based picker: resolve the surface's present() promise
326
+ // with the picked file.
320
327
  dismiss?.(file);
321
328
  }
322
329
  return;
@@ -336,7 +343,7 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
336
343
  return next;
337
344
  });
338
345
  scrollToSelectedFile(file.id);
339
- }, [multiSelect, onSelect, onClose, goBack, dismiss, disabledMimeTypes, maxSelection, afterSelect, defaultVisibility, oxyServices, linkContext, t, scrollToSelectedFile]);
346
+ }, [multiSelect, onPicked, onSelect, onClose, goBack, dismiss, disabledMimeTypes, maxSelection, afterSelect, defaultVisibility, oxyServices, linkContext, t, scrollToSelectedFile]);
340
347
 
341
348
  const confirmMultiSelection = useCallback(async () => {
342
349
  if (!selectMode || !multiSelect) return;
@@ -427,6 +434,7 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
427
434
  multiSelect,
428
435
  afterSelect,
429
436
  onSelect,
437
+ onPicked,
430
438
  goBack,
431
439
  onClose,
432
440
  selectedIds,
@@ -999,16 +1007,21 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
999
1007
  const isOwner = user?.id === targetUserId;
1000
1008
  const allowUpload = isOwner && allowUploadInSelectMode;
1001
1009
 
1002
- // Cancel = the nav bar's back affordance. Legacy callback callers keep their
1003
- // close/back behaviour; the promise-based avatar picker dismisses its surface.
1010
+ // Cancel = the nav bar's back affordance. A forward-nav picker (`onPicked`,
1011
+ // e.g. the avatar flow) pops its own frame; legacy callback callers keep their
1012
+ // close/back behaviour; a promise-based picker dismisses its surface.
1004
1013
  const pickerCancel = useCallback(() => {
1005
- if (onSelect || onConfirmSelection) {
1014
+ if (onPicked) {
1015
+ // Forward-nav picker: Cancel/back pops THIS frame, returning to the
1016
+ // frame that opened the picker — never closes the whole session.
1017
+ goBack?.();
1018
+ } else if (onSelect || onConfirmSelection) {
1006
1019
  if (onClose) onClose();
1007
1020
  else goBack?.();
1008
1021
  } else {
1009
1022
  dismiss?.();
1010
1023
  }
1011
- }, [onSelect, onConfirmSelection, onClose, goBack, dismiss]);
1024
+ }, [onPicked, onSelect, onConfirmSelection, onClose, goBack, dismiss]);
1012
1025
 
1013
1026
  // The picker's ONE trailing CTA, declared through the Dialog header's
1014
1027
  // `primaryAction` (a proper Bloom Button with loading/disabled) — the
@@ -58,6 +58,8 @@ export interface UseFileUploadStateParams {
58
58
  multiSelect: boolean;
59
59
  afterSelect: 'close' | 'back' | 'none';
60
60
  onSelect?: (file: FileMetadata) => void;
61
+ /** Forward-navigation picker: see {@link FileManagementScreenProps.onPicked}. */
62
+ onPicked?: (file: FileMetadata) => void;
61
63
  goBack?: () => void;
62
64
  onClose?: () => void;
63
65
  selectedIds: Set<string>;
@@ -94,6 +96,7 @@ export const useFileUploadState = ({
94
96
  multiSelect,
95
97
  afterSelect,
96
98
  onSelect,
99
+ onPicked,
97
100
  goBack,
98
101
  onClose,
99
102
  selectedIds,
@@ -335,12 +338,19 @@ export const useFileUploadState = ({
335
338
  setTimeout(() => {
336
339
  const fileToSelect = uploadedFiles[0];
337
340
  if (!multiSelect && fileToSelect) {
338
- // Single select mode - directly call onSelect callback
339
- onSelect?.(fileToSelect);
340
- if (afterSelect === 'back') {
341
- goBack?.();
342
- } else if (afterSelect === 'close') {
343
- onClose?.();
341
+ if (onPicked) {
342
+ // Forward-navigation picker (avatar flow): hand the
343
+ // freshly-uploaded file onward (to the cropper). No
344
+ // dismiss/close — the picker frame stays below.
345
+ onPicked(fileToSelect);
346
+ } else {
347
+ // Single select mode - directly call onSelect callback
348
+ onSelect?.(fileToSelect);
349
+ if (afterSelect === 'back') {
350
+ goBack?.();
351
+ } else if (afterSelect === 'close') {
352
+ onClose?.();
353
+ }
344
354
  }
345
355
  } else if (multiSelect) {
346
356
  // Multi-select mode - add all uploaded files to selection
@@ -14,6 +14,16 @@ export interface FileManagementScreenProps extends BaseScreenProps {
14
14
  multiSelect?: boolean;
15
15
  // Callback when a file is selected (single select mode)
16
16
  onSelect?: (file: FileMetadata) => void;
17
+ /**
18
+ * Forward-navigation picker: called with the picked file in single-select
19
+ * mode when the picker is one FRAME of a larger in-surface flow. Unlike
20
+ * {@link onSelect}, the caller is expected to NAVIGATE the surface onward
21
+ * (e.g. the avatar flow pushes the cropper on top of this picker) — so on
22
+ * select the picker neither dismisses nor closes the session, and its
23
+ * Cancel/back pops THIS frame (returning to the frame that opened the picker)
24
+ * rather than closing. Takes precedence over `onSelect`/`dismiss` when set.
25
+ */
26
+ onPicked?: (file: FileMetadata) => void;
17
27
  // Callback when confirm pressed in multi-select mode
18
28
  onConfirmSelection?: OnConfirmFileSelection;
19
29
  // Initial selected file IDs for multi-select