@idealyst/files 1.2.96
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/package.json +94 -0
- package/src/components/DropZone.native.tsx +96 -0
- package/src/components/DropZone.styles.tsx +99 -0
- package/src/components/DropZone.web.tsx +178 -0
- package/src/components/FilePickerButton.native.tsx +82 -0
- package/src/components/FilePickerButton.styles.tsx +112 -0
- package/src/components/FilePickerButton.web.tsx +84 -0
- package/src/components/UploadProgress.native.tsx +203 -0
- package/src/components/UploadProgress.styles.tsx +90 -0
- package/src/components/UploadProgress.web.tsx +201 -0
- package/src/components/index.native.ts +8 -0
- package/src/components/index.ts +6 -0
- package/src/components/index.web.ts +8 -0
- package/src/constants.ts +336 -0
- package/src/examples/index.ts +181 -0
- package/src/hooks/createUseFilePickerHook.ts +169 -0
- package/src/hooks/createUseFileUploadHook.ts +173 -0
- package/src/hooks/index.native.ts +12 -0
- package/src/hooks/index.ts +12 -0
- package/src/hooks/index.web.ts +12 -0
- package/src/index.native.ts +142 -0
- package/src/index.ts +139 -0
- package/src/index.web.ts +142 -0
- package/src/permissions/index.native.ts +8 -0
- package/src/permissions/index.ts +8 -0
- package/src/permissions/index.web.ts +8 -0
- package/src/permissions/permissions.native.ts +177 -0
- package/src/permissions/permissions.web.ts +96 -0
- package/src/picker/FilePicker.native.ts +407 -0
- package/src/picker/FilePicker.web.ts +366 -0
- package/src/picker/index.native.ts +2 -0
- package/src/picker/index.ts +2 -0
- package/src/picker/index.web.ts +2 -0
- package/src/types.ts +990 -0
- package/src/uploader/ChunkedUploader.ts +312 -0
- package/src/uploader/FileUploader.native.ts +435 -0
- package/src/uploader/FileUploader.web.ts +350 -0
- package/src/uploader/UploadQueue.ts +519 -0
- package/src/uploader/index.native.ts +4 -0
- package/src/uploader/index.ts +4 -0
- package/src/uploader/index.web.ts +4 -0
- package/src/utils.ts +586 -0
package/package.json
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@idealyst/files",
|
|
3
|
+
"version": "1.2.96",
|
|
4
|
+
"description": "Cross-platform file picker, upload, and local file management for React and React Native",
|
|
5
|
+
"documentation": "https://github.com/IdealystIO/idealyst-framework/tree/main/packages/files#readme",
|
|
6
|
+
"readme": "README.md",
|
|
7
|
+
"main": "src/index.ts",
|
|
8
|
+
"module": "src/index.ts",
|
|
9
|
+
"types": "src/index.ts",
|
|
10
|
+
"react-native": "src/index.native.ts",
|
|
11
|
+
"repository": {
|
|
12
|
+
"type": "git",
|
|
13
|
+
"url": "https://github.com/IdealystIO/idealyst-framework.git",
|
|
14
|
+
"directory": "packages/files"
|
|
15
|
+
},
|
|
16
|
+
"author": "Idealyst <hello@idealyst.io>",
|
|
17
|
+
"license": "MIT",
|
|
18
|
+
"publishConfig": {
|
|
19
|
+
"access": "public"
|
|
20
|
+
},
|
|
21
|
+
"exports": {
|
|
22
|
+
".": {
|
|
23
|
+
"react-native": "./src/index.native.ts",
|
|
24
|
+
"browser": "./src/index.web.ts",
|
|
25
|
+
"import": "./src/index.ts",
|
|
26
|
+
"require": "./src/index.ts",
|
|
27
|
+
"types": "./src/index.ts"
|
|
28
|
+
},
|
|
29
|
+
"./examples": {
|
|
30
|
+
"import": "./src/examples/index.ts",
|
|
31
|
+
"require": "./src/examples/index.ts",
|
|
32
|
+
"types": "./src/examples/index.ts"
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
"scripts": {
|
|
36
|
+
"prepublishOnly": "echo 'Publishing TypeScript source directly'",
|
|
37
|
+
"publish:npm": "npm publish"
|
|
38
|
+
},
|
|
39
|
+
"peerDependencies": {
|
|
40
|
+
"@idealyst/components": "^1.2.96",
|
|
41
|
+
"@idealyst/theme": "^1.2.96",
|
|
42
|
+
"react": ">=16.8.0",
|
|
43
|
+
"react-native": ">=0.60.0",
|
|
44
|
+
"react-native-document-picker": ">=9.0.0",
|
|
45
|
+
"react-native-image-picker": ">=7.0.0",
|
|
46
|
+
"react-native-blob-util": ">=0.19.0"
|
|
47
|
+
},
|
|
48
|
+
"peerDependenciesMeta": {
|
|
49
|
+
"@idealyst/components": {
|
|
50
|
+
"optional": true
|
|
51
|
+
},
|
|
52
|
+
"@idealyst/theme": {
|
|
53
|
+
"optional": true
|
|
54
|
+
},
|
|
55
|
+
"react-native": {
|
|
56
|
+
"optional": true
|
|
57
|
+
},
|
|
58
|
+
"react-native-document-picker": {
|
|
59
|
+
"optional": true
|
|
60
|
+
},
|
|
61
|
+
"react-native-image-picker": {
|
|
62
|
+
"optional": true
|
|
63
|
+
},
|
|
64
|
+
"react-native-blob-util": {
|
|
65
|
+
"optional": true
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
"devDependencies": {
|
|
69
|
+
"@idealyst/components": "^1.2.96",
|
|
70
|
+
"@idealyst/theme": "^1.2.96",
|
|
71
|
+
"@types/react": "^19.1.0",
|
|
72
|
+
"@types/react-native": "^0.73.0",
|
|
73
|
+
"react": "^19.1.0",
|
|
74
|
+
"react-native": "^0.80.0",
|
|
75
|
+
"react-native-unistyles": "^3.0.10",
|
|
76
|
+
"typescript": "^5.0.0"
|
|
77
|
+
},
|
|
78
|
+
"files": [
|
|
79
|
+
"src",
|
|
80
|
+
"README.md"
|
|
81
|
+
],
|
|
82
|
+
"keywords": [
|
|
83
|
+
"react",
|
|
84
|
+
"react-native",
|
|
85
|
+
"file",
|
|
86
|
+
"upload",
|
|
87
|
+
"picker",
|
|
88
|
+
"document",
|
|
89
|
+
"image",
|
|
90
|
+
"cross-platform",
|
|
91
|
+
"chunked-upload",
|
|
92
|
+
"background-upload"
|
|
93
|
+
]
|
|
94
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import React, { useCallback } from 'react';
|
|
2
|
+
import { View, Text, TouchableOpacity } from 'react-native';
|
|
3
|
+
import type { DropZoneProps, DropZoneState } from '../types';
|
|
4
|
+
import { dropZoneStyles } from './DropZone.styles';
|
|
5
|
+
import { useFilePicker } from '../hooks';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* DropZone - A touch area for file selection (Native).
|
|
9
|
+
*
|
|
10
|
+
* Note: Drag and drop is not supported on native. This component acts as a
|
|
11
|
+
* styled button that opens the file picker when pressed.
|
|
12
|
+
*/
|
|
13
|
+
export const DropZone: React.FC<DropZoneProps> = (props) => {
|
|
14
|
+
const {
|
|
15
|
+
onDrop,
|
|
16
|
+
onReject,
|
|
17
|
+
config,
|
|
18
|
+
children,
|
|
19
|
+
disabled = false,
|
|
20
|
+
style,
|
|
21
|
+
testID,
|
|
22
|
+
} = props;
|
|
23
|
+
|
|
24
|
+
const { pick, isPicking } = useFilePicker({ config });
|
|
25
|
+
|
|
26
|
+
const state: DropZoneState = {
|
|
27
|
+
isDragActive: false,
|
|
28
|
+
isDragReject: false,
|
|
29
|
+
isProcessing: isPicking,
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
// Handle press to open file picker
|
|
33
|
+
const handlePress = useCallback(async () => {
|
|
34
|
+
if (disabled || isPicking) return;
|
|
35
|
+
|
|
36
|
+
const result = await pick();
|
|
37
|
+
if (!result.cancelled && result.files.length > 0) {
|
|
38
|
+
onDrop?.(result.files);
|
|
39
|
+
}
|
|
40
|
+
if (result.rejected.length > 0) {
|
|
41
|
+
onReject?.(result.rejected);
|
|
42
|
+
}
|
|
43
|
+
}, [disabled, isPicking, pick, onDrop, onReject]);
|
|
44
|
+
|
|
45
|
+
// Apply variants
|
|
46
|
+
dropZoneStyles.useVariants({
|
|
47
|
+
active: false,
|
|
48
|
+
reject: false,
|
|
49
|
+
disabled,
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
// Render children
|
|
53
|
+
const renderChildren = () => {
|
|
54
|
+
if (typeof children === 'function') {
|
|
55
|
+
return children(state);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (children) {
|
|
59
|
+
return children;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Default content
|
|
63
|
+
return (
|
|
64
|
+
<View style={dropZoneStyles.content({})}>
|
|
65
|
+
<Text style={dropZoneStyles.icon({})}>
|
|
66
|
+
📁
|
|
67
|
+
</Text>
|
|
68
|
+
<Text style={dropZoneStyles.text({})}>
|
|
69
|
+
Tap to select files
|
|
70
|
+
</Text>
|
|
71
|
+
<Text style={dropZoneStyles.hint({})}>
|
|
72
|
+
or use the camera
|
|
73
|
+
</Text>
|
|
74
|
+
</View>
|
|
75
|
+
);
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
return (
|
|
79
|
+
<TouchableOpacity
|
|
80
|
+
onPress={handlePress}
|
|
81
|
+
disabled={disabled || isPicking}
|
|
82
|
+
activeOpacity={0.75}
|
|
83
|
+
testID={testID}
|
|
84
|
+
accessibilityRole="button"
|
|
85
|
+
accessibilityLabel="Touch to select files"
|
|
86
|
+
style={[
|
|
87
|
+
dropZoneStyles.container({ disabled }),
|
|
88
|
+
style,
|
|
89
|
+
]}
|
|
90
|
+
>
|
|
91
|
+
{renderChildren()}
|
|
92
|
+
</TouchableOpacity>
|
|
93
|
+
);
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
DropZone.displayName = 'DropZone';
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DropZone styles using defineStyle.
|
|
3
|
+
*/
|
|
4
|
+
import { StyleSheet } from 'react-native-unistyles';
|
|
5
|
+
import { defineStyle, ThemeStyleWrapper } from '@idealyst/theme';
|
|
6
|
+
import type { Theme as BaseTheme, Intent } from '@idealyst/theme';
|
|
7
|
+
|
|
8
|
+
// Required: Unistyles must see StyleSheet usage to process this file
|
|
9
|
+
void StyleSheet;
|
|
10
|
+
|
|
11
|
+
// Wrap theme for $iterator support
|
|
12
|
+
type Theme = ThemeStyleWrapper<BaseTheme>;
|
|
13
|
+
|
|
14
|
+
export type DropZoneVariants = {
|
|
15
|
+
active: boolean;
|
|
16
|
+
reject: boolean;
|
|
17
|
+
disabled: boolean;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export type DropZoneDynamicProps = {
|
|
21
|
+
active?: boolean;
|
|
22
|
+
reject?: boolean;
|
|
23
|
+
disabled?: boolean;
|
|
24
|
+
intent?: Intent;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export const dropZoneStyles = defineStyle('DropZone', (theme: Theme) => ({
|
|
28
|
+
container: ({ active = false, reject = false, disabled = false }: DropZoneDynamicProps) => ({
|
|
29
|
+
borderWidth: 2,
|
|
30
|
+
borderStyle: 'dashed' as const,
|
|
31
|
+
borderRadius: 12,
|
|
32
|
+
padding: 32,
|
|
33
|
+
alignItems: 'center',
|
|
34
|
+
justifyContent: 'center',
|
|
35
|
+
backgroundColor: active
|
|
36
|
+
? theme.intents.primary.light
|
|
37
|
+
: reject
|
|
38
|
+
? theme.intents.danger.light
|
|
39
|
+
: theme.colors.surface.secondary,
|
|
40
|
+
borderColor: active
|
|
41
|
+
? theme.intents.primary.primary
|
|
42
|
+
: reject
|
|
43
|
+
? theme.intents.danger.primary
|
|
44
|
+
: theme.colors.border.secondary,
|
|
45
|
+
opacity: disabled ? 0.6 : 1,
|
|
46
|
+
_web: {
|
|
47
|
+
cursor: disabled ? 'not-allowed' : 'pointer',
|
|
48
|
+
transition: 'all 0.2s ease',
|
|
49
|
+
},
|
|
50
|
+
variants: {
|
|
51
|
+
active: {
|
|
52
|
+
true: {
|
|
53
|
+
backgroundColor: theme.intents.primary.light,
|
|
54
|
+
borderColor: theme.intents.primary.primary,
|
|
55
|
+
},
|
|
56
|
+
false: {},
|
|
57
|
+
},
|
|
58
|
+
reject: {
|
|
59
|
+
true: {
|
|
60
|
+
backgroundColor: theme.intents.danger.light,
|
|
61
|
+
borderColor: theme.intents.danger.primary,
|
|
62
|
+
},
|
|
63
|
+
false: {},
|
|
64
|
+
},
|
|
65
|
+
disabled: {
|
|
66
|
+
true: { opacity: 0.6, _web: { cursor: 'not-allowed' } },
|
|
67
|
+
false: { opacity: 1, _web: { cursor: 'pointer' } },
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
}),
|
|
71
|
+
content: (_props: DropZoneDynamicProps) => ({
|
|
72
|
+
alignItems: 'center',
|
|
73
|
+
justifyContent: 'center',
|
|
74
|
+
gap: 12,
|
|
75
|
+
}),
|
|
76
|
+
icon: ({ active = false, reject = false }: DropZoneDynamicProps) => ({
|
|
77
|
+
fontSize: 48,
|
|
78
|
+
color: active
|
|
79
|
+
? theme.intents.primary.primary
|
|
80
|
+
: reject
|
|
81
|
+
? theme.intents.danger.primary
|
|
82
|
+
: theme.colors.text.secondary,
|
|
83
|
+
}),
|
|
84
|
+
text: ({ active = false, reject = false }: DropZoneDynamicProps) => ({
|
|
85
|
+
fontSize: 16,
|
|
86
|
+
fontWeight: '500',
|
|
87
|
+
textAlign: 'center',
|
|
88
|
+
color: active
|
|
89
|
+
? theme.intents.primary.primary
|
|
90
|
+
: reject
|
|
91
|
+
? theme.intents.danger.primary
|
|
92
|
+
: theme.colors.text.primary,
|
|
93
|
+
}),
|
|
94
|
+
hint: (_props: DropZoneDynamicProps) => ({
|
|
95
|
+
fontSize: 14,
|
|
96
|
+
color: theme.colors.text.secondary,
|
|
97
|
+
textAlign: 'center',
|
|
98
|
+
}),
|
|
99
|
+
}));
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import React, { useState, useCallback, useRef, useEffect } from 'react';
|
|
2
|
+
import { View, Text, Pressable } from 'react-native';
|
|
3
|
+
import type { DropZoneProps, DropZoneState, PickedFile, RejectedFile } from '../types';
|
|
4
|
+
import { dropZoneStyles } from './DropZone.styles';
|
|
5
|
+
import { useFilePicker } from '../hooks';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* DropZone - A drag-and-drop area for file selection (Web).
|
|
9
|
+
*/
|
|
10
|
+
export const DropZone: React.FC<DropZoneProps> = (props) => {
|
|
11
|
+
const {
|
|
12
|
+
onDrop,
|
|
13
|
+
onReject,
|
|
14
|
+
config,
|
|
15
|
+
children,
|
|
16
|
+
disabled = false,
|
|
17
|
+
style,
|
|
18
|
+
activeStyle,
|
|
19
|
+
rejectStyle,
|
|
20
|
+
testID,
|
|
21
|
+
} = props;
|
|
22
|
+
|
|
23
|
+
const { validateFiles, pick, isPicking } = useFilePicker({ config });
|
|
24
|
+
const dropRef = useRef<HTMLDivElement>(null);
|
|
25
|
+
const dragCounter = useRef(0);
|
|
26
|
+
|
|
27
|
+
const [state, setState] = useState<DropZoneState>({
|
|
28
|
+
isDragActive: false,
|
|
29
|
+
isDragReject: false,
|
|
30
|
+
isProcessing: false,
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
// Handle drag enter
|
|
34
|
+
const handleDragEnter = useCallback((e: DragEvent) => {
|
|
35
|
+
e.preventDefault();
|
|
36
|
+
e.stopPropagation();
|
|
37
|
+
|
|
38
|
+
if (disabled) return;
|
|
39
|
+
|
|
40
|
+
dragCounter.current++;
|
|
41
|
+
|
|
42
|
+
if (e.dataTransfer?.items?.length) {
|
|
43
|
+
setState(s => ({ ...s, isDragActive: true }));
|
|
44
|
+
}
|
|
45
|
+
}, [disabled]);
|
|
46
|
+
|
|
47
|
+
// Handle drag over
|
|
48
|
+
const handleDragOver = useCallback((e: DragEvent) => {
|
|
49
|
+
e.preventDefault();
|
|
50
|
+
e.stopPropagation();
|
|
51
|
+
}, []);
|
|
52
|
+
|
|
53
|
+
// Handle drag leave
|
|
54
|
+
const handleDragLeave = useCallback((e: DragEvent) => {
|
|
55
|
+
e.preventDefault();
|
|
56
|
+
e.stopPropagation();
|
|
57
|
+
|
|
58
|
+
dragCounter.current--;
|
|
59
|
+
|
|
60
|
+
if (dragCounter.current === 0) {
|
|
61
|
+
setState(s => ({ ...s, isDragActive: false, isDragReject: false }));
|
|
62
|
+
}
|
|
63
|
+
}, []);
|
|
64
|
+
|
|
65
|
+
// Handle drop
|
|
66
|
+
const handleDrop = useCallback(async (e: DragEvent) => {
|
|
67
|
+
e.preventDefault();
|
|
68
|
+
e.stopPropagation();
|
|
69
|
+
|
|
70
|
+
dragCounter.current = 0;
|
|
71
|
+
|
|
72
|
+
if (disabled) {
|
|
73
|
+
setState(s => ({ ...s, isDragActive: false, isDragReject: false }));
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
setState(s => ({ ...s, isDragActive: false, isProcessing: true }));
|
|
78
|
+
|
|
79
|
+
const files = Array.from(e.dataTransfer?.files || []);
|
|
80
|
+
const { accepted, rejected } = validateFiles(files);
|
|
81
|
+
|
|
82
|
+
if (rejected.length > 0) {
|
|
83
|
+
onReject?.(rejected);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (accepted.length > 0) {
|
|
87
|
+
onDrop?.(accepted);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
setState(s => ({ ...s, isProcessing: false }));
|
|
91
|
+
}, [disabled, validateFiles, onDrop, onReject]);
|
|
92
|
+
|
|
93
|
+
// Handle click to open file picker
|
|
94
|
+
const handleClick = useCallback(async () => {
|
|
95
|
+
if (disabled || isPicking) return;
|
|
96
|
+
|
|
97
|
+
const result = await pick();
|
|
98
|
+
if (!result.cancelled && result.files.length > 0) {
|
|
99
|
+
onDrop?.(result.files);
|
|
100
|
+
}
|
|
101
|
+
if (result.rejected.length > 0) {
|
|
102
|
+
onReject?.(result.rejected);
|
|
103
|
+
}
|
|
104
|
+
}, [disabled, isPicking, pick, onDrop, onReject]);
|
|
105
|
+
|
|
106
|
+
// Set up drag event listeners
|
|
107
|
+
useEffect(() => {
|
|
108
|
+
const element = dropRef.current;
|
|
109
|
+
if (!element) return;
|
|
110
|
+
|
|
111
|
+
element.addEventListener('dragenter', handleDragEnter);
|
|
112
|
+
element.addEventListener('dragover', handleDragOver);
|
|
113
|
+
element.addEventListener('dragleave', handleDragLeave);
|
|
114
|
+
element.addEventListener('drop', handleDrop);
|
|
115
|
+
|
|
116
|
+
return () => {
|
|
117
|
+
element.removeEventListener('dragenter', handleDragEnter);
|
|
118
|
+
element.removeEventListener('dragover', handleDragOver);
|
|
119
|
+
element.removeEventListener('dragleave', handleDragLeave);
|
|
120
|
+
element.removeEventListener('drop', handleDrop);
|
|
121
|
+
};
|
|
122
|
+
}, [handleDragEnter, handleDragOver, handleDragLeave, handleDrop]);
|
|
123
|
+
|
|
124
|
+
// Apply variants
|
|
125
|
+
dropZoneStyles.useVariants({
|
|
126
|
+
active: state.isDragActive,
|
|
127
|
+
reject: state.isDragReject,
|
|
128
|
+
disabled,
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
// Render children
|
|
132
|
+
const renderChildren = () => {
|
|
133
|
+
if (typeof children === 'function') {
|
|
134
|
+
return children(state);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (children) {
|
|
138
|
+
return children;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Default content
|
|
142
|
+
return (
|
|
143
|
+
<View style={dropZoneStyles.content({})}>
|
|
144
|
+
<Text style={dropZoneStyles.icon({ active: state.isDragActive, reject: state.isDragReject })}>
|
|
145
|
+
{state.isDragActive ? '📂' : '📁'}
|
|
146
|
+
</Text>
|
|
147
|
+
<Text style={dropZoneStyles.text({ active: state.isDragActive, reject: state.isDragReject })}>
|
|
148
|
+
{state.isDragActive ? 'Drop files here' : 'Drag & drop files here'}
|
|
149
|
+
</Text>
|
|
150
|
+
<Text style={dropZoneStyles.hint({})}>
|
|
151
|
+
or click to browse
|
|
152
|
+
</Text>
|
|
153
|
+
</View>
|
|
154
|
+
);
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
return (
|
|
158
|
+
<Pressable
|
|
159
|
+
onPress={handleClick}
|
|
160
|
+
disabled={disabled}
|
|
161
|
+
testID={testID}
|
|
162
|
+
accessibilityRole="button"
|
|
163
|
+
accessibilityLabel="Drop zone for file upload"
|
|
164
|
+
style={[
|
|
165
|
+
dropZoneStyles.container({ active: state.isDragActive, reject: state.isDragReject, disabled }),
|
|
166
|
+
style,
|
|
167
|
+
state.isDragActive && activeStyle,
|
|
168
|
+
state.isDragReject && rejectStyle,
|
|
169
|
+
]}
|
|
170
|
+
>
|
|
171
|
+
<View ref={dropRef as any}>
|
|
172
|
+
{renderChildren()}
|
|
173
|
+
</View>
|
|
174
|
+
</Pressable>
|
|
175
|
+
);
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
DropZone.displayName = 'DropZone';
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import React, { forwardRef, useCallback } from 'react';
|
|
2
|
+
import { View, Text, TouchableOpacity, ActivityIndicator } from 'react-native';
|
|
3
|
+
import type { FilePickerButtonProps, FilePickerResult, FilePickerError } from '../types';
|
|
4
|
+
import { filePickerButtonStyles } from './FilePickerButton.styles';
|
|
5
|
+
import { useFilePicker } from '../hooks';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* FilePickerButton - A styled button that opens the file picker (Native).
|
|
9
|
+
*/
|
|
10
|
+
export const FilePickerButton = forwardRef<View, FilePickerButtonProps>((props, ref) => {
|
|
11
|
+
const {
|
|
12
|
+
children = 'Select File',
|
|
13
|
+
pickerConfig,
|
|
14
|
+
onPick,
|
|
15
|
+
onError,
|
|
16
|
+
disabled = false,
|
|
17
|
+
loading = false,
|
|
18
|
+
variant = 'solid',
|
|
19
|
+
size = 'md',
|
|
20
|
+
intent = 'primary',
|
|
21
|
+
leftIcon = 'file-upload',
|
|
22
|
+
style,
|
|
23
|
+
testID,
|
|
24
|
+
} = props;
|
|
25
|
+
|
|
26
|
+
const { pick, isPicking } = useFilePicker({ config: pickerConfig });
|
|
27
|
+
|
|
28
|
+
const isDisabled = disabled || loading || isPicking;
|
|
29
|
+
const isLoading = loading || isPicking;
|
|
30
|
+
|
|
31
|
+
const handlePress = useCallback(async () => {
|
|
32
|
+
if (isDisabled) return;
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
const result: FilePickerResult = await pick();
|
|
36
|
+
if (!result.cancelled) {
|
|
37
|
+
onPick?.(result);
|
|
38
|
+
}
|
|
39
|
+
} catch (err) {
|
|
40
|
+
onError?.(err as FilePickerError);
|
|
41
|
+
}
|
|
42
|
+
}, [isDisabled, pick, onPick, onError]);
|
|
43
|
+
|
|
44
|
+
// Apply variants
|
|
45
|
+
filePickerButtonStyles.useVariants({
|
|
46
|
+
size,
|
|
47
|
+
intent,
|
|
48
|
+
variant,
|
|
49
|
+
disabled: isDisabled,
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
<TouchableOpacity
|
|
54
|
+
ref={ref}
|
|
55
|
+
onPress={handlePress}
|
|
56
|
+
disabled={isDisabled}
|
|
57
|
+
activeOpacity={0.75}
|
|
58
|
+
style={[filePickerButtonStyles.button({ intent, variant }), style]}
|
|
59
|
+
testID={testID}
|
|
60
|
+
accessibilityRole="button"
|
|
61
|
+
accessibilityState={{ disabled: isDisabled }}
|
|
62
|
+
accessibilityLabel={typeof children === 'string' ? children : 'Select file'}
|
|
63
|
+
>
|
|
64
|
+
{isLoading ? (
|
|
65
|
+
<ActivityIndicator
|
|
66
|
+
size="small"
|
|
67
|
+
color={filePickerButtonStyles.spinner({ intent, variant }).color as string}
|
|
68
|
+
/>
|
|
69
|
+
) : leftIcon ? (
|
|
70
|
+
<Text style={filePickerButtonStyles.icon({ intent, variant })}>
|
|
71
|
+
{/* Icon placeholder - in a real implementation, use MDI icon */}
|
|
72
|
+
📁
|
|
73
|
+
</Text>
|
|
74
|
+
) : null}
|
|
75
|
+
<Text style={filePickerButtonStyles.text({ intent, variant })}>
|
|
76
|
+
{children}
|
|
77
|
+
</Text>
|
|
78
|
+
</TouchableOpacity>
|
|
79
|
+
);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
FilePickerButton.displayName = 'FilePickerButton';
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FilePickerButton styles using defineStyle.
|
|
3
|
+
*/
|
|
4
|
+
import { StyleSheet } from 'react-native-unistyles';
|
|
5
|
+
import { defineStyle, ThemeStyleWrapper } from '@idealyst/theme';
|
|
6
|
+
import type { Theme as BaseTheme, Intent, Size } from '@idealyst/theme';
|
|
7
|
+
|
|
8
|
+
// Required: Unistyles must see StyleSheet usage to process this file
|
|
9
|
+
void StyleSheet;
|
|
10
|
+
|
|
11
|
+
// Wrap theme for $iterator support
|
|
12
|
+
type Theme = ThemeStyleWrapper<BaseTheme>;
|
|
13
|
+
|
|
14
|
+
type ButtonType = 'solid' | 'outline' | 'ghost';
|
|
15
|
+
|
|
16
|
+
export type FilePickerButtonVariants = {
|
|
17
|
+
size: Size;
|
|
18
|
+
intent: Intent;
|
|
19
|
+
variant: ButtonType;
|
|
20
|
+
disabled: boolean;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export type FilePickerButtonDynamicProps = {
|
|
24
|
+
intent?: Intent;
|
|
25
|
+
variant?: ButtonType;
|
|
26
|
+
size?: Size;
|
|
27
|
+
disabled?: boolean;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export const filePickerButtonStyles = defineStyle('FilePickerButton', (theme: Theme) => ({
|
|
31
|
+
button: ({ intent = 'primary', variant = 'solid' }: FilePickerButtonDynamicProps) => ({
|
|
32
|
+
boxSizing: 'border-box',
|
|
33
|
+
alignItems: 'center',
|
|
34
|
+
justifyContent: 'center',
|
|
35
|
+
borderRadius: 8,
|
|
36
|
+
fontWeight: '600',
|
|
37
|
+
textAlign: 'center',
|
|
38
|
+
flexDirection: 'row',
|
|
39
|
+
gap: 8,
|
|
40
|
+
backgroundColor: variant === 'solid'
|
|
41
|
+
? theme.intents[intent].primary
|
|
42
|
+
: variant === 'outline'
|
|
43
|
+
? theme.colors.surface.primary
|
|
44
|
+
: 'transparent',
|
|
45
|
+
borderColor: variant === 'outline'
|
|
46
|
+
? theme.intents[intent].primary
|
|
47
|
+
: 'transparent',
|
|
48
|
+
borderWidth: variant === 'outline' ? 1 : 0,
|
|
49
|
+
borderStyle: variant === 'outline' ? 'solid' as const : undefined,
|
|
50
|
+
_web: {
|
|
51
|
+
display: 'flex',
|
|
52
|
+
transition: 'all 0.1s ease',
|
|
53
|
+
cursor: 'pointer',
|
|
54
|
+
},
|
|
55
|
+
variants: {
|
|
56
|
+
size: {
|
|
57
|
+
paddingVertical: theme.sizes.$button.paddingVertical,
|
|
58
|
+
paddingHorizontal: theme.sizes.$button.paddingHorizontal,
|
|
59
|
+
minHeight: theme.sizes.$button.minHeight,
|
|
60
|
+
},
|
|
61
|
+
disabled: {
|
|
62
|
+
true: { opacity: 0.6, _web: { cursor: 'not-allowed' } },
|
|
63
|
+
false: { opacity: 1, _web: { cursor: 'pointer', _hover: { opacity: 0.90 }, _active: { opacity: 0.75 } } },
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
}),
|
|
67
|
+
text: ({ intent = 'primary', variant = 'solid' }: FilePickerButtonDynamicProps) => ({
|
|
68
|
+
fontWeight: '600',
|
|
69
|
+
textAlign: 'center',
|
|
70
|
+
color: variant === 'solid'
|
|
71
|
+
? theme.intents[intent].contrast
|
|
72
|
+
: theme.intents[intent].primary,
|
|
73
|
+
variants: {
|
|
74
|
+
size: {
|
|
75
|
+
fontSize: theme.sizes.$button.fontSize,
|
|
76
|
+
lineHeight: theme.sizes.$button.fontSize,
|
|
77
|
+
},
|
|
78
|
+
disabled: {
|
|
79
|
+
true: { opacity: 0.6 },
|
|
80
|
+
false: { opacity: 1 },
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
}),
|
|
84
|
+
icon: ({ intent = 'primary', variant = 'solid' }: FilePickerButtonDynamicProps) => ({
|
|
85
|
+
display: 'flex',
|
|
86
|
+
alignItems: 'center',
|
|
87
|
+
justifyContent: 'center',
|
|
88
|
+
color: variant === 'solid'
|
|
89
|
+
? theme.intents[intent].contrast
|
|
90
|
+
: theme.intents[intent].primary,
|
|
91
|
+
variants: {
|
|
92
|
+
size: {
|
|
93
|
+
width: theme.sizes.$button.iconSize,
|
|
94
|
+
height: theme.sizes.$button.iconSize,
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
}),
|
|
98
|
+
spinner: ({ intent = 'primary', variant = 'solid' }: FilePickerButtonDynamicProps) => ({
|
|
99
|
+
display: 'flex',
|
|
100
|
+
alignItems: 'center',
|
|
101
|
+
justifyContent: 'center',
|
|
102
|
+
color: variant === 'solid'
|
|
103
|
+
? theme.intents[intent].contrast
|
|
104
|
+
: theme.intents[intent].primary,
|
|
105
|
+
variants: {
|
|
106
|
+
size: {
|
|
107
|
+
width: theme.sizes.$button.iconSize,
|
|
108
|
+
height: theme.sizes.$button.iconSize,
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
}),
|
|
112
|
+
}));
|