@oxyhq/services 22.11.0 → 22.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/lib/commonjs/ui/components/OxyAuthChooser.js +1 -1
  2. package/lib/commonjs/ui/components/OxyAuthChooser.js.map +1 -1
  3. package/lib/commonjs/ui/hooks/useSurfaceHeader.js +7 -5
  4. package/lib/commonjs/ui/hooks/useSurfaceHeader.js.map +1 -1
  5. package/lib/commonjs/ui/navigation/surfaceRegistry.js +11 -37
  6. package/lib/commonjs/ui/navigation/surfaceRegistry.js.map +1 -1
  7. package/lib/commonjs/ui/screens/AvatarCropScreen.js +9 -13
  8. package/lib/commonjs/ui/screens/AvatarCropScreen.js.map +1 -1
  9. package/lib/commonjs/ui/screens/FileManagementScreen.js +55 -13
  10. package/lib/commonjs/ui/screens/FileManagementScreen.js.map +1 -1
  11. package/lib/commonjs/ui/screens/PaymentGatewayScreen.js +11 -0
  12. package/lib/commonjs/ui/screens/PaymentGatewayScreen.js.map +1 -1
  13. package/lib/commonjs/ui/screens/ProfileScreen.js +91 -117
  14. package/lib/commonjs/ui/screens/ProfileScreen.js.map +1 -1
  15. package/lib/commonjs/ui/screens/WelcomeNewUserScreen.js +22 -54
  16. package/lib/commonjs/ui/screens/WelcomeNewUserScreen.js.map +1 -1
  17. package/lib/commonjs/ui/screens/fileManagement/PhotoPickerSection.js +64 -155
  18. package/lib/commonjs/ui/screens/fileManagement/PhotoPickerSection.js.map +1 -1
  19. package/lib/module/ui/components/OxyAuthChooser.js +1 -1
  20. package/lib/module/ui/components/OxyAuthChooser.js.map +1 -1
  21. package/lib/module/ui/hooks/useSurfaceHeader.js +7 -5
  22. package/lib/module/ui/hooks/useSurfaceHeader.js.map +1 -1
  23. package/lib/module/ui/navigation/surfaceRegistry.js +11 -37
  24. package/lib/module/ui/navigation/surfaceRegistry.js.map +1 -1
  25. package/lib/module/ui/screens/AvatarCropScreen.js +9 -13
  26. package/lib/module/ui/screens/AvatarCropScreen.js.map +1 -1
  27. package/lib/module/ui/screens/FileManagementScreen.js +55 -13
  28. package/lib/module/ui/screens/FileManagementScreen.js.map +1 -1
  29. package/lib/module/ui/screens/PaymentGatewayScreen.js +11 -0
  30. package/lib/module/ui/screens/PaymentGatewayScreen.js.map +1 -1
  31. package/lib/module/ui/screens/ProfileScreen.js +93 -119
  32. package/lib/module/ui/screens/ProfileScreen.js.map +1 -1
  33. package/lib/module/ui/screens/WelcomeNewUserScreen.js +23 -54
  34. package/lib/module/ui/screens/WelcomeNewUserScreen.js.map +1 -1
  35. package/lib/module/ui/screens/fileManagement/PhotoPickerSection.js +64 -155
  36. package/lib/module/ui/screens/fileManagement/PhotoPickerSection.js.map +1 -1
  37. package/lib/typescript/commonjs/ui/hooks/useSurfaceHeader.d.ts +7 -5
  38. package/lib/typescript/commonjs/ui/hooks/useSurfaceHeader.d.ts.map +1 -1
  39. package/lib/typescript/commonjs/ui/navigation/surfaceRegistry.d.ts +8 -9
  40. package/lib/typescript/commonjs/ui/navigation/surfaceRegistry.d.ts.map +1 -1
  41. package/lib/typescript/commonjs/ui/screens/AvatarCropScreen.d.ts.map +1 -1
  42. package/lib/typescript/commonjs/ui/screens/FileManagementScreen.d.ts.map +1 -1
  43. package/lib/typescript/commonjs/ui/screens/PaymentGatewayScreen.d.ts.map +1 -1
  44. package/lib/typescript/commonjs/ui/screens/ProfileScreen.d.ts.map +1 -1
  45. package/lib/typescript/commonjs/ui/screens/WelcomeNewUserScreen.d.ts +2 -1
  46. package/lib/typescript/commonjs/ui/screens/WelcomeNewUserScreen.d.ts.map +1 -1
  47. package/lib/typescript/commonjs/ui/screens/fileManagement/PhotoPickerSection.d.ts +0 -6
  48. package/lib/typescript/commonjs/ui/screens/fileManagement/PhotoPickerSection.d.ts.map +1 -1
  49. package/lib/typescript/module/ui/hooks/useSurfaceHeader.d.ts +7 -5
  50. package/lib/typescript/module/ui/hooks/useSurfaceHeader.d.ts.map +1 -1
  51. package/lib/typescript/module/ui/navigation/surfaceRegistry.d.ts +8 -9
  52. package/lib/typescript/module/ui/navigation/surfaceRegistry.d.ts.map +1 -1
  53. package/lib/typescript/module/ui/screens/AvatarCropScreen.d.ts.map +1 -1
  54. package/lib/typescript/module/ui/screens/FileManagementScreen.d.ts.map +1 -1
  55. package/lib/typescript/module/ui/screens/PaymentGatewayScreen.d.ts.map +1 -1
  56. package/lib/typescript/module/ui/screens/ProfileScreen.d.ts.map +1 -1
  57. package/lib/typescript/module/ui/screens/WelcomeNewUserScreen.d.ts +2 -1
  58. package/lib/typescript/module/ui/screens/WelcomeNewUserScreen.d.ts.map +1 -1
  59. package/lib/typescript/module/ui/screens/fileManagement/PhotoPickerSection.d.ts +0 -6
  60. package/lib/typescript/module/ui/screens/fileManagement/PhotoPickerSection.d.ts.map +1 -1
  61. package/package.json +6 -6
  62. package/src/ui/components/OxyAuthChooser.tsx +1 -1
  63. package/src/ui/hooks/useSurfaceHeader.ts +25 -5
  64. package/src/ui/navigation/__tests__/getSurfaceConfig.test.ts +16 -2
  65. package/src/ui/navigation/surfaceRegistry.ts +14 -42
  66. package/src/ui/screens/AvatarCropScreen.tsx +14 -23
  67. package/src/ui/screens/FileManagementScreen.tsx +69 -25
  68. package/src/ui/screens/PaymentGatewayScreen.tsx +11 -0
  69. package/src/ui/screens/ProfileScreen.tsx +96 -111
  70. package/src/ui/screens/WelcomeNewUserScreen.tsx +23 -63
  71. package/src/ui/screens/fileManagement/PhotoPickerSection.tsx +68 -172
@@ -25,13 +25,10 @@ import FileLibraryError from './FileLibraryError';
25
25
  * import. Matches the pattern used by AvatarCropScreen.
26
26
  */
27
27
  type HapticImpact = 'light' | 'medium' | 'heavy';
28
- type HapticNotification = 'success' | 'warning' | 'error';
29
28
  interface HapticsModule {
30
29
  impactAsync: (style: unknown) => Promise<void>;
31
- notificationAsync: (type: unknown) => Promise<void>;
32
30
  selectionAsync: () => Promise<void>;
33
31
  ImpactFeedbackStyle: { Light: unknown; Medium: unknown; Heavy: unknown };
34
- NotificationFeedbackType: { Success: unknown; Warning: unknown; Error: unknown };
35
32
  }
36
33
 
37
34
  let hapticsModulePromise: Promise<HapticsModule | null> | null = null;
@@ -76,35 +73,27 @@ const hapticSelection = async (): Promise<void> => {
76
73
  }
77
74
  };
78
75
 
79
- const hapticNotification = async (type: HapticNotification): Promise<void> => {
80
- const h = await getHaptics();
81
- if (!h) return;
82
- const typeEnum =
83
- type === 'error'
84
- ? h.NotificationFeedbackType.Error
85
- : type === 'warning'
86
- ? h.NotificationFeedbackType.Warning
87
- : h.NotificationFeedbackType.Success;
88
- try {
89
- await h.notificationAsync(typeEnum);
90
- } catch {
91
- // Silent.
92
- }
93
- };
94
-
95
76
  /**
96
- * The picker renders inside a Bloom `<Dialog>` (bottom-sheet on narrow, centered
97
- * card on `md+`). The Dialog owns its own chrome: it draws its OWN drag handle
98
- * in bottom placement (a floating 28dp pill that paints ABOVE this header via a
99
- * higher z-index) and none in center placement so the picker's translucent
100
- * header no longer reserves the handle strip itself. The header is just the 56dp
101
- * app bar; grid content is pushed down by the app bar + 4dp of breathing room so
102
- * the first row clears it. These are fixed layout constants the equivalent
103
- * `min-h-[56px]` / `pt-[60px]` utility classes in the JSX MUST stay in sync.
77
+ * The picker renders inside a Bloom `<Dialog>` in NAV-HEADER mode (bottom-sheet on
78
+ * narrow, centered card on `md+`). The Dialog owns the shared gradient nav bar
79
+ * (Cancel / "Choose Photo" / Upload, wired by `FileManagementScreen` through
80
+ * `useSurfaceHeader`) the picker no longer paints a bar of its own. In this mode
81
+ * the Dialog reserves a fixed nav-bar-height SPACER above the picker's content and
82
+ * its gradient overlay fades a little further into the content; the grid's own
83
+ * top padding just clears that fade so the first row reads cleanly. Two fixed
84
+ * layout constants mirror Bloom internals:
85
+ * - `DIALOG_NAV_BAR_SPACER` mirrors Bloom's `DIALOG_NAV_BAR_HEIGHT` (not exported
86
+ * from `@oxyhq/bloom/dialog`) — the spacer the Dialog inserts above header-mode
87
+ * `scrollable:false` content. The picker's own height is reduced by it so the
88
+ * spacer + grid together fill the `frameSize` morph target (and never overflow
89
+ * the panel's `maxHeightRatio: 0.9` cap, which would clip the last row).
90
+ * - `HEADER_GRADIENT_FADE` is the gradient tail that bleeds past the bar into the
91
+ * content; the grid's `pt-[24px]` (fade + 4dp breathing room) clears it. The
92
+ * `GRID_CONTENT_PADDING_TOP` constant MUST stay in sync with that class.
104
93
  */
105
- const APP_BAR_HEIGHT = 56;
106
- const HEADER_HEIGHT = APP_BAR_HEIGHT; // 56
107
- const GRID_CONTENT_PADDING_TOP = HEADER_HEIGHT + 4; // 60
94
+ const DIALOG_NAV_BAR_SPACER = 52;
95
+ const HEADER_GRADIENT_FADE = 20;
96
+ const GRID_CONTENT_PADDING_TOP = HEADER_GRADIENT_FADE + 4; // 24 — matches `pt-[24px]`
108
97
 
109
98
  /**
110
99
  * Props for the dedicated photo picker view. Used by FileManagementScreen
@@ -120,7 +109,6 @@ export interface PhotoPickerViewProps {
120
109
  refreshing: boolean;
121
110
  uploading: boolean;
122
111
  isPickingDocument: boolean;
123
- uploadProgress: { current: number; total: number } | null;
124
112
  hasMore: boolean;
125
113
  loadingMore: boolean;
126
114
  /**
@@ -143,8 +131,6 @@ export interface PhotoPickerViewProps {
143
131
  onUpload: () => void;
144
132
  onRefresh: () => void;
145
133
  onLoadMore: () => void;
146
- onCancel: () => void;
147
- onConfirm: () => void;
148
134
  t: (key: string, vars?: Record<string, string | number>) => string;
149
135
  }
150
136
 
@@ -224,9 +210,20 @@ const PhotoPickerCell = React.memo(function PhotoPickerCell(props: PhotoPickerCe
224
210
  // Entrance fade, staggered by the cell's grid index. Started from the wrapper's
225
211
  // ref callback (fires on mount) rather than an effect. Reduce-motion pins
226
212
  // opacity at 1 and never animates.
213
+ //
214
+ // CRITICAL: `Animated.View` rebuilds its merged ref on every render, so React
215
+ // re-invokes this callback on EVERY commit (not just mount) — and re-running
216
+ // `Animated.timing().start()`/`setValue` each time drives a web re-render that
217
+ // re-fires the ref → "Maximum update depth exceeded" once the grid has cells.
218
+ // The `startedRef` latch makes it genuinely run-once per mount, breaking the
219
+ // loop. (jest/react-test-renderer never exercises the Animated merged-ref path,
220
+ // so this only reproduces in a real browser.)
227
221
  const opacity = useRef(new Animated.Value(reduceMotion ? 1 : 0)).current;
222
+ const startedRef = useRef(false);
228
223
  const startEntrance = useCallback((node: unknown) => {
229
224
  if (!node) return; // unmount
225
+ if (startedRef.current) return; // ref re-fired on a later commit — already ran
226
+ startedRef.current = true;
230
227
  if (reduceMotion) {
231
228
  opacity.setValue(1);
232
229
  return;
@@ -243,10 +240,18 @@ const PhotoPickerCell = React.memo(function PhotoPickerCell(props: PhotoPickerCe
243
240
  // Selection-ring pulse: a quick scale bump that springs back to rest. The ring
244
241
  // renders ONLY while selected, so its own mount IS the "became selected"
245
242
  // signal — driven from the ring's ref callback, not an effect. Reduce-motion
246
- // holds the ring at rest scale.
243
+ // holds the ring at rest scale. Same run-once latch as the entrance (the ring's
244
+ // `Animated.View` re-fires this ref every commit too) — reset on unmount so a
245
+ // later re-selection pulses again.
247
246
  const ringScale = useRef(new Animated.Value(1)).current;
247
+ const pulsedRef = useRef(false);
248
248
  const startRingPulse = useCallback((node: unknown) => {
249
- if (!node) return; // ring unmounting (deselected)
249
+ if (!node) {
250
+ pulsedRef.current = false; // ring unmounted (deselected) — arm the next pulse
251
+ return;
252
+ }
253
+ if (pulsedRef.current) return; // ref re-fired on a later commit — already pulsed
254
+ pulsedRef.current = true;
250
255
  ringScale.setValue(1);
251
256
  if (reduceMotion) return;
252
257
  Animated.sequence([
@@ -343,7 +348,6 @@ const PhotoPickerView: React.FC<PhotoPickerViewProps> = ({
343
348
  refreshing,
344
349
  uploading,
345
350
  isPickingDocument,
346
- uploadProgress,
347
351
  hasMore,
348
352
  loadingMore,
349
353
  loading,
@@ -358,8 +362,6 @@ const PhotoPickerView: React.FC<PhotoPickerViewProps> = ({
358
362
  onUpload,
359
363
  onRefresh,
360
364
  onLoadMore,
361
- onCancel,
362
- onConfirm,
363
365
  t,
364
366
  }) => {
365
367
  // Grid sizing width. We PREFER the measured grid-container width (the
@@ -381,12 +383,18 @@ const PhotoPickerView: React.FC<PhotoPickerViewProps> = ({
381
383
 
382
384
  // The picker FILLS its panel: the surface morphs the container UP to an
383
385
  // explicit large target (`frameSize.heightRatio: 0.9` in `surfaceRegistry`),
384
- // and this root takes that SAME 0.9-of-viewport height so the grid uses the
385
- // whole grown card (a `flex: 1` root can't fill no definite bound propagates
386
- // through the sheet's flex chain, and the panel is pinned, not measured). The
387
- // FlatList then scrolls within this fixed height. Keep the 0.9 ratio in sync
388
- // with the picker's `frameSize` in `surfaceRegistry.ts`.
389
- const panelHeight = useMemo(() => Math.round(windowHeight * 0.9), [windowHeight]);
386
+ // and this root takes that SAME 0.9-of-viewport height MINUS the Dialog's
387
+ // nav-bar spacer (which the Dialog inserts ABOVE this content in header mode).
388
+ // spacer + root then exactly fill the morph target and stay within the panel's
389
+ // `maxHeightRatio: 0.9` cap without the subtraction the grid's last row would
390
+ // clip. A `flex: 1` root can't fill (no definite bound propagates through the
391
+ // sheet's flex chain at rest, where the panel is height:auto), so the fixed
392
+ // height is required; the FlatList scrolls within it. Keep the 0.9 ratio in
393
+ // sync with the picker's `frameSize` in `surfaceRegistry.ts`.
394
+ const panelHeight = useMemo(
395
+ () => Math.round(windowHeight * 0.9) - DIALOG_NAV_BAR_SPACER,
396
+ [windowHeight],
397
+ );
390
398
  const rootStyle = useMemo(() => ({ height: panelHeight }), [panelHeight]);
391
399
 
392
400
  const effectiveWidth = gridWidth > 0 ? gridWidth : windowWidth;
@@ -421,9 +429,6 @@ const PhotoPickerView: React.FC<PhotoPickerViewProps> = ({
421
429
 
422
430
  const hasAnySelection = selectedIds.size > 0;
423
431
 
424
- // Compact icon-only upload pill on narrow sheets; full pill otherwise.
425
- const showUploadLabel = effectiveWidth >= 360;
426
-
427
432
  const isEmpty = photos.length === 0;
428
433
  const atSelectionLimit =
429
434
  multiSelect && maxSelection != null && selectedIds.size >= maxSelection;
@@ -448,16 +453,6 @@ const PhotoPickerView: React.FC<PhotoPickerViewProps> = ({
448
453
  [onPreviewPhoto],
449
454
  );
450
455
 
451
- const handleConfirm = useCallback(() => {
452
- if (multiSelect) {
453
- if (selectedIds.size === 0) return;
454
- if (Platform.OS !== 'web') {
455
- void hapticNotification('success');
456
- }
457
- }
458
- onConfirm();
459
- }, [multiSelect, selectedIds.size, onConfirm]);
460
-
461
456
  // FlatList renderItem: each cell knows its enterIndex (for stagger).
462
457
  const renderItem = useCallback(
463
458
  ({ item, index }: { item: FileMetadata; index: number }) => {
@@ -518,16 +513,6 @@ const PhotoPickerView: React.FC<PhotoPickerViewProps> = ({
518
513
  onLoadMore();
519
514
  }, [loadingMore, hasMore, onLoadMore]);
520
515
 
521
- const confirmDisabled = multiSelect && selectedIds.size === 0;
522
- const confirmLabel = multiSelect
523
- ? t('fileManagement.doneWithCount', { count: selectedIds.size })
524
- : t('fileManagement.done');
525
-
526
- // The progress fill width. Guard against zero division.
527
- const progressFraction = uploadProgress && uploadProgress.total > 0
528
- ? Math.min(1, Math.max(0, uploadProgress.current / uploadProgress.total))
529
- : 0;
530
-
531
516
  return (
532
517
  // Measured wrapper: `style`-only (no className) so RN-Web fires onLayout.
533
518
  // `rootStyle` is placement-aware — a `maxHeight` cap that gives the
@@ -540,12 +525,15 @@ const PhotoPickerView: React.FC<PhotoPickerViewProps> = ({
540
525
  root (the CSS flexbox `min-height:auto` trap; a no-op on native,
541
526
  where Yoga already defaults min-height to 0). */}
542
527
  <View className="flex-1 min-h-0 bg-black">
543
- {/* Photo grid (renders behind the translucent header). */}
528
+ {/* Photo grid full-bleed black canvas CONTENT under the shared
529
+ Dialog nav bar (the Dialog reserves the bar's height above this
530
+ and its gradient fades a little further down; the top padding
531
+ clears that fade). */}
544
532
  {isEmpty && loadError ? (
545
533
  /* Terminal load failure — a DISTINCT error surface (not the
546
- empty state), pushed below the translucent header so Cancel
547
- stays reachable. Retry re-runs the list query. */
548
- <View className="flex-1 items-center justify-center pt-[60px]">
534
+ empty state), cleared below the nav bar's gradient fade.
535
+ Retry re-runs the list query. */
536
+ <View className="flex-1 items-center justify-center pt-[24px]">
549
537
  <FileLibraryError
550
538
  title={t('fileManagement.loadError.title')}
551
539
  description={t('fileManagement.loadError.description')}
@@ -560,14 +548,13 @@ const PhotoPickerView: React.FC<PhotoPickerViewProps> = ({
560
548
  ) : isEmpty && loading ? (
561
549
  /* Loading skeleton — the picker's OWN shape: shimmer tiles laid
562
550
  out with the SAME placement-aware geometry the real grid uses
563
- (`computePhotoGridLayout(effectiveWidth)`), pushed below the
564
- translucent header by the grid content padding. `flexWrap`
565
- breaks rows exactly at `columns` because the last tile in each
566
- row carries no right margin (a full row + gutters fits the
567
- measured width by construction). The real translucent header
568
- still floats on top, so Cancel stays reachable while loading. */
551
+ (`computePhotoGridLayout(effectiveWidth)`), cleared below the
552
+ nav bar's gradient fade. `flexWrap` breaks rows exactly at
553
+ `columns` because the last tile in each row carries no right
554
+ margin (a full row + gutters fits the measured width by
555
+ construction). */
569
556
  <View
570
- className="pt-[60px]"
557
+ className="pt-[24px]"
571
558
  style={{ flexDirection: 'row', flexWrap: 'wrap' }}
572
559
  pointerEvents="none"
573
560
  accessibilityElementsHidden
@@ -575,7 +562,7 @@ const PhotoPickerView: React.FC<PhotoPickerViewProps> = ({
575
562
  >
576
563
  {Array.from({ length: skeletonTileCount }, (_, index) => (
577
564
  <Skeleton.Box
578
- key={index}
565
+ key={`skeleton-${index}`}
579
566
  width={cellSize}
580
567
  height={cellSize}
581
568
  borderRadius={CELL_CORNER_RADIUS}
@@ -588,7 +575,7 @@ const PhotoPickerView: React.FC<PhotoPickerViewProps> = ({
588
575
  ))}
589
576
  </View>
590
577
  ) : isEmpty ? (
591
- <View className="flex-1 items-center justify-center px-space-32 pt-[60px]">
578
+ <View className="flex-1 items-center justify-center px-space-32 pt-[24px]">
592
579
  <View className="opacity-30 mb-space-16">
593
580
  <MaterialCommunityIcons name="image-outline" size={64} color="#FFFFFF" />
594
581
  </View>
@@ -629,7 +616,7 @@ const PhotoPickerView: React.FC<PhotoPickerViewProps> = ({
629
616
  keyExtractor={keyExtractor}
630
617
  numColumns={columns}
631
618
  className="flex-1 min-h-0"
632
- contentContainerClassName="pt-[60px] pb-space-24"
619
+ contentContainerClassName="pt-[24px] pb-space-24"
633
620
  showsVerticalScrollIndicator={false}
634
621
  refreshControl={
635
622
  <RefreshControl
@@ -648,97 +635,6 @@ const PhotoPickerView: React.FC<PhotoPickerViewProps> = ({
648
635
  windowSize={9}
649
636
  />
650
637
  )}
651
-
652
- {/* Translucent black header floating over the grid. The Dialog owns
653
- the drag handle (a floating pill above this bar in bottom
654
- placement, drawn at a higher z-index; none in center), so the
655
- header no longer reserves the 28dp handle strip — it is just the
656
- 56dp app bar (`min-h-[56px]`). */}
657
- <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]">
658
- <View className="flex-row items-center justify-between w-full h-14">
659
- <View className="basis-0 grow flex-row items-center justify-start">
660
- <TouchableOpacity
661
- onPress={onCancel}
662
- className="px-1.5 py-space-8 min-h-9 min-w-11 justify-center"
663
- accessibilityRole="button"
664
- accessibilityLabel={t('fileManagement.a11y.cancelPicker')}
665
- hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}
666
- >
667
- <Text className="text-white text-[17px] font-medium">
668
- {t('fileManagement.cancel')}
669
- </Text>
670
- </TouchableOpacity>
671
- </View>
672
- <View pointerEvents="none">
673
- <Text
674
- className="text-white text-[17px] font-semibold text-center"
675
- style={{ letterSpacing: -0.2 }}
676
- numberOfLines={1}
677
- >
678
- {t('fileManagement.choosePhoto')}
679
- </Text>
680
- </View>
681
- <View className="basis-0 grow flex-row items-center justify-end">
682
- {multiSelect ? (
683
- <TouchableOpacity
684
- onPress={handleConfirm}
685
- disabled={confirmDisabled}
686
- className="flex-row items-center gap-1.5 px-3.5 py-space-8 rounded-full min-h-9"
687
- style={{
688
- backgroundColor: confirmDisabled
689
- ? 'rgba(255,255,255,0.18)'
690
- : primaryColor,
691
- opacity: confirmDisabled ? 0.6 : 1,
692
- }}
693
- accessibilityRole="button"
694
- accessibilityLabel={t('fileManagement.a11y.confirmSelection')}
695
- accessibilityState={{ disabled: confirmDisabled }}
696
- >
697
- <Text className="text-white text-[15px] font-semibold">
698
- {confirmLabel}
699
- </Text>
700
- </TouchableOpacity>
701
- ) : (
702
- isOwner && allowUpload && (
703
- <TouchableOpacity
704
- onPress={onUpload}
705
- disabled={uploading || isPickingDocument}
706
- className={`flex-row items-center gap-1.5 py-space-8 rounded-full min-h-9${showUploadLabel ? ' px-3.5' : ' px-space-8 w-9 justify-center'}`}
707
- style={{ backgroundColor: primaryColor }}
708
- accessibilityRole="button"
709
- accessibilityLabel={t('fileManagement.a11y.uploadFromDevice')}
710
- accessibilityState={{ busy: uploading || isPickingDocument }}
711
- >
712
- {(uploading || isPickingDocument) ? (
713
- <ActivityIndicator size="small" color="#FFFFFF" />
714
- ) : (
715
- <>
716
- <Ionicons name="cloud-upload" size={16} color="#FFFFFF" />
717
- {showUploadLabel && (
718
- <Text className="text-white text-[15px] font-semibold">
719
- {t('fileManagement.upload')}
720
- </Text>
721
- )}
722
- </>
723
- )}
724
- </TouchableOpacity>
725
- )
726
- )}
727
- </View>
728
- </View>
729
- {/* Subtle top progress bar during upload (non-blocking). */}
730
- {uploading && (
731
- <View className="absolute left-0 right-0 bottom-0 h-0.5 bg-[#FFFFFF1F]">
732
- <View
733
- className="h-full"
734
- style={{
735
- width: `${Math.round(progressFraction * 100)}%`,
736
- backgroundColor: primaryColor,
737
- }}
738
- />
739
- </View>
740
- )}
741
- </View>
742
638
  </View>
743
639
  </View>
744
640
  );