@herca/rn-kit 0.1.41 → 0.1.42

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 (36) hide show
  1. package/lib/module/Icon/Icon.js +7 -1
  2. package/lib/module/Icon/Icon.js.map +1 -1
  3. package/lib/module/Icon/README.md +3 -0
  4. package/lib/module/Icon/icons/ExclamationTriangle.js +22 -0
  5. package/lib/module/Icon/icons/ExclamationTriangle.js.map +1 -0
  6. package/lib/module/Icon/icons/PdfFile.js +36 -0
  7. package/lib/module/Icon/icons/PdfFile.js.map +1 -0
  8. package/lib/module/Icon/icons/RotateRight.js +22 -0
  9. package/lib/module/Icon/icons/RotateRight.js.map +1 -0
  10. package/lib/module/Input/InputFile.js +194 -59
  11. package/lib/module/Input/InputFile.js.map +1 -1
  12. package/lib/module/Input/README_INPUT_FILE.md +132 -0
  13. package/lib/module/Ui/Container.js.map +1 -1
  14. package/lib/typescript/src/Icon/Icon.d.ts.map +1 -1
  15. package/lib/typescript/src/Icon/icons/ExclamationTriangle.d.ts +9 -0
  16. package/lib/typescript/src/Icon/icons/ExclamationTriangle.d.ts.map +1 -0
  17. package/lib/typescript/src/Icon/icons/PdfFile.d.ts +11 -0
  18. package/lib/typescript/src/Icon/icons/PdfFile.d.ts.map +1 -0
  19. package/lib/typescript/src/Icon/icons/RotateRight.d.ts +9 -0
  20. package/lib/typescript/src/Icon/icons/RotateRight.d.ts.map +1 -0
  21. package/lib/typescript/src/Icon/type.d.ts +1 -1
  22. package/lib/typescript/src/Icon/type.d.ts.map +1 -1
  23. package/lib/typescript/src/Input/InputFile.d.ts +23 -3
  24. package/lib/typescript/src/Input/InputFile.d.ts.map +1 -1
  25. package/lib/typescript/src/Ui/Container.d.ts +7 -1
  26. package/lib/typescript/src/Ui/Container.d.ts.map +1 -1
  27. package/package.json +1 -1
  28. package/src/Icon/Icon.tsx +6 -0
  29. package/src/Icon/README.md +3 -0
  30. package/src/Icon/icons/ExclamationTriangle.tsx +23 -0
  31. package/src/Icon/icons/PdfFile.tsx +40 -0
  32. package/src/Icon/icons/RotateRight.tsx +23 -0
  33. package/src/Icon/type.tsx +3 -0
  34. package/src/Input/InputFile.tsx +235 -64
  35. package/src/Input/README_INPUT_FILE.md +132 -0
  36. package/src/Ui/Container.tsx +7 -2
@@ -1,41 +1,67 @@
1
+ import { useState, useEffect } from 'react';
1
2
  import { Image, StyleSheet, TouchableOpacity, View } from 'react-native';
3
+ import BottomSheet from '../BottomSheet/BottomSheet';
2
4
  import Button from '../Button/Button';
3
5
  import Color from '../Color/Color';
4
- import Card from '../Ui/Card';
5
- import Center from '../Ui/Center';
6
- import Typography from '../Typography/Typography';
7
- import BottomSheet from '../BottomSheet/BottomSheet';
8
- import { useState } from 'react';
9
6
  import List from '../List/List';
10
7
  import ListItem from '../List/ListItem';
8
+ import Typography from '../Typography/Typography';
9
+ import Card from '../Ui/Card';
10
+ import Center from '../Ui/Center';
11
11
 
12
12
  import { pick } from '@react-native-documents/picker';
13
13
  import { viewDocument } from '@react-native-documents/viewer';
14
14
  import { launchCamera, launchImageLibrary } from 'react-native-image-picker';
15
15
  import Icon from '../Icon';
16
+ import Input from './Input';
17
+
18
+ export interface ModalOption {
19
+ title?: string;
20
+ description?: string;
21
+ }
22
+ export interface modalPickFileText {
23
+ title?: string;
24
+ description?: string;
25
+ camera?: ModalOption;
26
+ gallery?: ModalOption;
27
+ document?: ModalOption;
28
+ }
16
29
 
17
30
  type InputFileProps = {
18
31
  title?: string;
19
32
  description?: string;
20
- maxFileSize?: number;
21
33
  accept?: string[];
22
34
  multiple?: boolean;
23
- onChange?: (files: any[]) => void;
35
+ onChange?: (files: any) => void;
36
+ value?: any[];
37
+ modalPickFileText?: modalPickFileText;
38
+ useChangeLabel?: boolean;
39
+ btnChooseFileText?: string;
40
+ modalDeleteText?: ModalOption & {
41
+ confirmBtn?: {
42
+ confirm?: string;
43
+ cancel?: string;
44
+ };
45
+ };
24
46
  };
25
47
 
26
48
  export default function InputFile({
27
49
  title = 'Upload File',
28
50
  description = 'File harus berformat JPG, PNG dan PDF',
29
- maxFileSize = 5,
30
51
  accept = ['application/pdf', 'image/jpeg', 'image/png'],
52
+ btnChooseFileText = 'Choose File',
31
53
  multiple = false,
54
+ value,
55
+ modalPickFileText,
56
+ modalDeleteText,
57
+ useChangeLabel = false,
32
58
  onChange,
33
59
  }: InputFileProps) {
34
60
  const [isOpenBottomSheetTypeFile, setIsOpenBottomSheetTypeFile] =
35
61
  useState(false);
36
62
  const [isOpenBottomSheetDeleteFile, setIsOpenBottomSheetDeleteFile] =
37
63
  useState(false);
38
- const [files, setFiles] = useState<any[]>([]);
64
+ const [files, setFiles] = useState<any[]>(value || []);
39
65
  const [selectedFileIndex, setSelectedFileIndex] = useState<number | null>(
40
66
  null
41
67
  );
@@ -46,6 +72,12 @@ export default function InputFile({
46
72
 
47
73
  const handlePickFile = async () => {
48
74
  try {
75
+ if (selectedFileIndex !== null) {
76
+ console.log(selectedFileIndex);
77
+ confirmReplaceFile(selectedFileIndex);
78
+ return;
79
+ }
80
+
49
81
  const result = await pick({ type: accept, multiple });
50
82
  const pickedFiles = multiple ? result : [result[0]];
51
83
  const newFiles = [...files, ...pickedFiles];
@@ -60,12 +92,19 @@ export default function InputFile({
60
92
 
61
93
  const handlePreviewFile = (file: any) => {
62
94
  if (file?.uri) {
63
- viewDocument(file.uri);
95
+ viewDocument({ uri: file.uri, mimeType: file.type }).catch((error) =>
96
+ console.error('Error opening document:', error)
97
+ );
64
98
  }
65
99
  };
66
100
 
67
101
  const handleOnPressButtonCamera = async () => {
68
102
  try {
103
+ if (selectedFileIndex !== null) {
104
+ confirmReplaceFile(selectedFileIndex, 'camera');
105
+ return;
106
+ }
107
+
69
108
  const result = await launchCamera({ mediaType: 'photo' });
70
109
  if (result.didCancel || !result.assets) return;
71
110
  const newFiles = [...files, result.assets[0]];
@@ -79,6 +118,11 @@ export default function InputFile({
79
118
 
80
119
  const handleOnPressButtonGallery = async () => {
81
120
  try {
121
+ if (selectedFileIndex !== null) {
122
+ confirmReplaceFile(selectedFileIndex, 'gallery');
123
+ return;
124
+ }
125
+
82
126
  const result = await launchImageLibrary({
83
127
  mediaType: 'photo',
84
128
  selectionLimit: multiple ? 0 : 1,
@@ -108,6 +152,46 @@ export default function InputFile({
108
152
  setIsOpenBottomSheetDeleteFile(false);
109
153
  };
110
154
 
155
+ const handleReplaceFile = (index: number) => {
156
+ setIsOpenBottomSheetTypeFile(true);
157
+ setSelectedFileIndex(index);
158
+ };
159
+
160
+ const confirmReplaceFile = async (
161
+ index: number,
162
+ type?: 'gallery' | 'camera'
163
+ ) => {
164
+ let replacedFile: any | null = null;
165
+
166
+ if (type === 'gallery') {
167
+ const result = await launchImageLibrary({
168
+ mediaType: 'photo',
169
+ selectionLimit: multiple ? 0 : 1,
170
+ });
171
+ if (!result.assets || result.assets.length === 0) return;
172
+ replacedFile = { ...result.assets[0], labelFile: files[index].labelFile };
173
+ } else if (type === 'camera') {
174
+ const result = await launchCamera({ mediaType: 'photo' });
175
+ if (!result.assets || result.assets.length === 0) return;
176
+ replacedFile = { ...result.assets[0], labelFile: files[index].labelFile };
177
+ } else {
178
+ const result = await pick({ type: accept, multiple });
179
+ if (!result || result.length === 0) return;
180
+ replacedFile = { ...result[0], labelFile: files[index].labelFile };
181
+ }
182
+
183
+ const updatedFiles = files.map((f, i) => (i === index ? replacedFile : f));
184
+
185
+ setFiles(updatedFiles);
186
+ onChange?.(updatedFiles);
187
+ setSelectedFileIndex(null);
188
+ setIsOpenBottomSheetTypeFile(false);
189
+ };
190
+
191
+ useEffect(() => {
192
+ setFiles(value ?? []);
193
+ }, [value]);
194
+
111
195
  return (
112
196
  <View style={styles.wrapper}>
113
197
  <Card style={styles.containerInput}>
@@ -121,50 +205,103 @@ export default function InputFile({
121
205
  </Typography>
122
206
  <Button
123
207
  color="primary"
124
- title="Choose File"
208
+ title={btnChooseFileText}
209
+ size="small"
125
210
  onPress={handleOnPresChoseFile}
126
211
  />
127
212
  </Center>
128
213
  </Card>
129
214
 
130
215
  {files.map((file, index) => (
131
- <Card key={index}>
132
- <TouchableOpacity onPress={() => handlePreviewFile(file)}>
133
- <View style={styles.containerPreview}>
134
- <View style={styles.containerListItem}>
135
- <Image
136
- source={
137
- file.uri?.endsWith('.pdf')
138
- ? require('../Input/assets/input-file.png')
139
- : { uri: file.uri }
140
- }
141
- style={styles.previewImage}
142
- />
143
- <View style={styles.fileDetails}>
144
- <Typography
145
- variant="t1"
146
- weight="semibold"
147
- color={Color.gray[600]}
148
- >
149
- {file.name || `Image_${index + 1}`}
150
- </Typography>
151
- <Typography
152
- variant="t2"
153
- weight="regular"
154
- color={Color.gray[600]}
155
- >
156
- {(file.size / (1024 * 1024)).toFixed(2)} MB
157
- </Typography>
216
+ <View style={styles.previewWrapper} key={index}>
217
+ {useChangeLabel && (
218
+ <Input
219
+ label="Nama Dokumen #1"
220
+ placeholder="Masukkan Nama Dokumen"
221
+ value={file.labelFile}
222
+ onChangeText={(text) => {
223
+ const updatedFiles = files.map((f, i) =>
224
+ i === index ? { ...f, labelFile: text } : f
225
+ );
226
+ setFiles(updatedFiles);
227
+ onChange?.(updatedFiles);
228
+ }}
229
+ />
230
+ )}
231
+ <View style={styles.gap4}>
232
+ <Card>
233
+ <TouchableOpacity onPress={() => handlePreviewFile(file)}>
234
+ <View style={styles.containerPreview}>
235
+ <View style={[styles.containerListItem, styles.flex1]}>
236
+ {file.type.startsWith('image') ? (
237
+ <Image
238
+ source={
239
+ file.uri?.endsWith('.pdf')
240
+ ? require('../Input/assets/input-file.png')
241
+ : { uri: file.uri }
242
+ }
243
+ style={styles.previewImage}
244
+ />
245
+ ) : (
246
+ <View style={styles.previewImage}>
247
+ <Icon name="Document" color={Color.primary[1000]} />
248
+ </View>
249
+ )}
250
+ <View style={styles.fileDetails}>
251
+ <Typography
252
+ variant="t1"
253
+ weight="semibold"
254
+ color={Color.gray[600]}
255
+ numberOfLines={1}
256
+ >
257
+ {file.name || `Image_${index + 1}`}
258
+ </Typography>
259
+ <Typography
260
+ variant="t2"
261
+ weight="regular"
262
+ color={Color.gray[600]}
263
+ >
264
+ {(
265
+ (file?.size ?? file?.fileSize ?? 0) /
266
+ (1024 * 1024)
267
+ ).toFixed(2)}
268
+ MB
269
+ </Typography>
270
+ </View>
271
+ </View>
272
+ <View style={styles.action}>
273
+ {file.error && (
274
+ <Icon
275
+ name="exclamation-triangle"
276
+ color={Color.danger[500]}
277
+ size={20}
278
+ />
279
+ )}
280
+ <TouchableOpacity onPress={() => handleReplaceFile(index)}>
281
+ <Icon
282
+ name="rotate-right"
283
+ size={18}
284
+ color={Color.gray[700]}
285
+ />
286
+ </TouchableOpacity>
287
+ <TouchableOpacity onPress={() => confirmDeleteFile(index)}>
288
+ <Icon name="Times" size={12} color={Color.gray[700]} />
289
+ </TouchableOpacity>
290
+ </View>
158
291
  </View>
159
- </View>
160
- <Button
161
- variant="tertiary"
162
- title="X"
163
- onPress={() => confirmDeleteFile(index)}
164
- />
165
- </View>
166
- </TouchableOpacity>
167
- </Card>
292
+ </TouchableOpacity>
293
+ </Card>
294
+
295
+ {file.hint && (
296
+ <Typography
297
+ variant="t3"
298
+ color={!file.error ? Color.gray[700] : Color.danger[500]}
299
+ >
300
+ {file.hint}
301
+ </Typography>
302
+ )}
303
+ </View>
304
+ </View>
168
305
  ))}
169
306
 
170
307
  <BottomSheet
@@ -174,10 +311,10 @@ export default function InputFile({
174
311
  <View style={styles.bottomSheetContent}>
175
312
  <Center style={styles.bottomSheetCenter}>
176
313
  <Typography variant="p2" weight="semibold" color={Color.gray[800]}>
177
- Upload Dokumen
314
+ {modalPickFileText?.title || 'Upload Dokumen'}
178
315
  </Typography>
179
- <Typography variant="t1" weight="regular" color={Color.gray[800]}>
180
- Unggah dokumen maksimal {maxFileSize}MB
316
+ <Typography variant="t1" weight="regular" color={Color.gray[600]}>
317
+ {modalPickFileText?.description || 'Unggah dokumen maksimal'}
181
318
  </Typography>
182
319
  </Center>
183
320
  <List>
@@ -191,10 +328,11 @@ export default function InputFile({
191
328
  weight="semibold"
192
329
  color={Color.gray[800]}
193
330
  >
194
- Ambil Foto
331
+ {modalPickFileText?.camera?.title || 'Ambil Foto'}
195
332
  </Typography>
196
333
  <Typography variant="t2" color={Color.gray[600]}>
197
- Langsung ambil gambar dari kamera
334
+ {modalPickFileText?.camera?.description ||
335
+ 'Langsung ambil gambar dari kamera'}
198
336
  </Typography>
199
337
  </View>
200
338
  </View>
@@ -210,10 +348,11 @@ export default function InputFile({
210
348
  weight="semibold"
211
349
  color={Color.gray[800]}
212
350
  >
213
- Pilih dari Galeri
351
+ {modalPickFileText?.gallery?.title || 'Pilih dari Galeri'}
214
352
  </Typography>
215
353
  <Typography variant="t2" color={Color.gray[600]}>
216
- Cari gambar dari galeri perangkatmu
354
+ {modalPickFileText?.gallery?.description ||
355
+ 'Cari gambar dari galeri perangkatmu'}
217
356
  </Typography>
218
357
  </View>
219
358
  </View>
@@ -229,10 +368,11 @@ export default function InputFile({
229
368
  weight="semibold"
230
369
  color={Color.gray[800]}
231
370
  >
232
- Pilih dari Dokumen
371
+ {modalPickFileText?.document?.title || 'Pilih Dokumen'}
233
372
  </Typography>
234
373
  <Typography variant="t2" color={Color.gray[600]}>
235
- Cari file dari penyimpanan dokumen
374
+ {modalPickFileText?.document?.description ||
375
+ 'Pilih dokumen dari perangkatmu'}
236
376
  </Typography>
237
377
  </View>
238
378
  </View>
@@ -248,23 +388,34 @@ export default function InputFile({
248
388
  >
249
389
  <View style={styles.deleteFileContent}>
250
390
  <Center style={styles.deleteFileCenter}>
251
- <Typography variant="p2" weight="semibold" color={Color.gray[800]}>
252
- Hapus Dokumen
391
+ <Typography
392
+ variant="p2"
393
+ weight="semibold"
394
+ color={Color.gray[800]}
395
+ center
396
+ >
397
+ {modalDeleteText?.title || 'Hapus Dokumen'}
253
398
  </Typography>
254
- <Typography variant="t1" weight="regular" color={Color.gray[500]}>
255
- Apakah kamu akan menghapus dokumen yang dipilih?
399
+ <Typography
400
+ variant="t1"
401
+ weight="regular"
402
+ color={Color.gray[500]}
403
+ center
404
+ >
405
+ {modalDeleteText?.description ||
406
+ 'Apakah kamu akan menghapus dokumen yang dipilih?'}
256
407
  </Typography>
257
408
  </Center>
258
409
  <View>
259
410
  <Button
260
411
  color="danger"
261
- title="Ya, Hapus File"
412
+ title={modalDeleteText?.confirmBtn?.confirm || 'Hapus'}
262
413
  onPress={deleteFile}
263
414
  />
264
415
  <Button
265
416
  color="primary"
266
417
  variant="tertiary"
267
- title="Batal"
418
+ title={modalDeleteText?.confirmBtn?.cancel || 'Batal'}
268
419
  onPress={() => setIsOpenBottomSheetDeleteFile(false)}
269
420
  />
270
421
  </View>
@@ -280,7 +431,8 @@ const styles = StyleSheet.create({
280
431
  },
281
432
  containerInput: {
282
433
  borderStyle: 'dashed',
283
- backgroundColor: Color.gray[100],
434
+ backgroundColor: Color.gray[50],
435
+ borderRadius: 16,
284
436
  },
285
437
  centerInput: {
286
438
  gap: 14,
@@ -295,14 +447,18 @@ const styles = StyleSheet.create({
295
447
  width: 44,
296
448
  height: 44,
297
449
  borderRadius: 6,
450
+ alignItems: 'center',
451
+ justifyContent: 'center',
298
452
  },
299
453
  fileDetails: {
300
454
  justifyContent: 'center',
301
- gap: 8,
455
+ gap: 4,
456
+ flex: 1,
302
457
  },
303
458
  containerListItem: {
304
459
  flexDirection: 'row',
305
460
  gap: 8,
461
+ alignItems: 'center',
306
462
  },
307
463
  bottomSheetContent: {
308
464
  gap: 16,
@@ -317,4 +473,19 @@ const styles = StyleSheet.create({
317
473
  alignItems: 'center',
318
474
  gap: 4,
319
475
  },
476
+ action: {
477
+ flexDirection: 'row',
478
+ alignItems: 'center',
479
+ gap: 12,
480
+ flexShrink: 0,
481
+ },
482
+ previewWrapper: {
483
+ gap: 12,
484
+ },
485
+ flex1: {
486
+ flex: 1,
487
+ },
488
+ gap4: {
489
+ gap: 4,
490
+ },
320
491
  });
@@ -0,0 +1,132 @@
1
+ # 📂 InputFile Component
2
+
3
+ Komponen `InputFile` digunakan untuk mengunggah, menampilkan, mengganti, menghapus, serta memberi label pada dokumen atau gambar di aplikasi React Native.
4
+ Mendukung **camera**, **gallery**, dan **document picker** dengan tampilan preview dan konfirmasi aksi.
5
+
6
+ ---
7
+
8
+ ## ✨ Fitur
9
+ - Upload file dari **kamera**, **galeri**, atau **dokumen perangkat**.
10
+ - Preview file (gambar atau dokumen).
11
+ - Mendukung **multiple file upload**.
12
+ - Ganti file (replace) atau hapus file dengan konfirmasi.
13
+ - Tambahkan label/nama dokumen secara manual.
14
+ - Konfigurasi teks modal (custom title, description, button).
15
+ - Callback `onChange` untuk mengirim data file terbaru ke parent.
16
+
17
+ ---
18
+
19
+ ## 📦 Dependencies
20
+ Komponen ini membutuhkan beberapa library:
21
+ - [`@react-native-documents/picker`](https://github.com/react-native-documents/picker)
22
+ - [`@react-native-documents/viewer`](https://github.com/react-native-documents/viewer)
23
+ - [`react-native-image-picker`](https://github.com/react-native-image-picker/react-native-image-picker)
24
+
25
+ ---
26
+
27
+ ## 🚀 Penggunaan
28
+
29
+ ```tsx
30
+ import InputFile from "@/components/Input/InputFile";
31
+ import { useState } from "react";
32
+
33
+ export default function ExampleScreen() {
34
+ const [files, setFiles] = useState<any[]>([]);
35
+
36
+ return (
37
+ <InputFile
38
+ title="Upload Dokumen"
39
+ description="Format file: JPG, PNG, PDF"
40
+ btnChooseFileText="Pilih File"
41
+ multiple={true}
42
+ value={files}
43
+ useChangeLabel={true}
44
+ modalPickFileText={{
45
+ title: "Pilih Sumber",
46
+ description: "Unggah dokumen atau ambil foto",
47
+ camera: { title: "Kamera", description: "Ambil foto langsung" },
48
+ gallery: { title: "Galeri", description: "Pilih gambar dari galeri" },
49
+ document: { title: "Dokumen", description: "Pilih file dari perangkat" },
50
+ }}
51
+ modalDeleteText={{
52
+ title: "Hapus File?",
53
+ description: "Apakah kamu yakin ingin menghapus file ini?",
54
+ confirmBtn: {
55
+ confirm: "Ya, Hapus",
56
+ cancel: "Batal",
57
+ },
58
+ }}
59
+ onChange={(newFiles) => setFiles(newFiles)}
60
+ />
61
+ );
62
+ }
63
+ ```
64
+
65
+ ---
66
+
67
+ ## ⚙️ Props
68
+
69
+ | Prop | Tipe | Default | Deskripsi |
70
+ |--------------------|-----------------------------------|-----------------------------------------------------------|---------------------------------------------------------------------------|
71
+ | `title` | `string` | `"Upload File"` | Judul input file. |
72
+ | `description` | `string` | `"File harus berformat JPG, PNG dan PDF"` | Deskripsi singkat tentang file yang diterima. |
73
+ | `accept` | `string[]` | `["application/pdf", "image/jpeg", "image/png"]` | Tipe MIME file yang diizinkan. |
74
+ | `multiple` | `boolean` | `false` | Jika `true`, memungkinkan memilih banyak file sekaligus. |
75
+ | `value` | `any[]` | `[]` | Daftar file yang sudah dipilih (state dari parent). |
76
+ | `onChange` | `(files: any[]) => void` | `undefined` | Callback saat daftar file berubah (add, replace, delete). |
77
+ | `btnChooseFileText`| `string` | `"Choose File"` | Label tombol untuk memilih file. |
78
+ | `modalPickFileText`| `modalPickFileText` (lihat bawah) | `undefined` | Teks kustom pada modal pemilihan sumber file. |
79
+ | `modalDeleteText` | `ModalOption & confirmBtn` | `undefined` | Teks kustom pada modal konfirmasi hapus file. |
80
+ | `useChangeLabel` | `boolean` | `false` | Jika `true`, menampilkan input untuk memberi nama/label pada file. |
81
+
82
+ ---
83
+
84
+ ## 🔤 Tipe Tambahan
85
+
86
+ ### `ModalOption`
87
+ ```ts
88
+ interface ModalOption {
89
+ title?: string;
90
+ description?: string;
91
+ }
92
+ ```
93
+
94
+ ### `modalPickFileText`
95
+ ```ts
96
+ interface modalPickFileText {
97
+ title?: string;
98
+ description?: string;
99
+ camera?: ModalOption;
100
+ gallery?: ModalOption;
101
+ document?: ModalOption;
102
+ }
103
+ ```
104
+
105
+ ### `modalDeleteText`
106
+ ```ts
107
+ interface ModalOption {
108
+ title?: string;
109
+ description?: string;
110
+ confirmBtn?: {
111
+ confirm?: string;
112
+ cancel?: string;
113
+ };
114
+ }
115
+ ```
116
+
117
+ ---
118
+
119
+ ## 📸 Preview UI
120
+ - **Card input awal** → tombol pilih file + icon ilustrasi.
121
+ - **Preview file** → menampilkan nama, ukuran, icon (atau thumbnail jika image).
122
+ - **Aksi file**:
123
+ - 🔄 Replace
124
+ - ❌ Delete
125
+ - 👁 Preview (buka dokumen dengan `viewDocument`)
126
+
127
+ ---
128
+
129
+ ## 🛠 Catatan
130
+ 1. Pastikan permission kamera & storage sudah diatur (Android/iOS).
131
+ 2. File hasil picker akan memiliki property standar (`uri`, `type`, `name`, `size`).
132
+ 3. Untuk **multiple upload**, `value` berupa array file yang akan dikelola oleh parent.
@@ -1,6 +1,11 @@
1
- import { StyleSheet, View } from 'react-native';
1
+ import { StyleSheet, View, type ViewStyle } from 'react-native';
2
2
 
3
- export default function Container({ children, style }: any) {
3
+ interface Props {
4
+ children: React.ReactNode;
5
+ style?: ViewStyle;
6
+ }
7
+
8
+ export default function Container({ children, style }: Props) {
4
9
  return <View style={[styles.container, style && style]}>{children}</View>;
5
10
  }
6
11