@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.
Files changed (42) hide show
  1. package/package.json +94 -0
  2. package/src/components/DropZone.native.tsx +96 -0
  3. package/src/components/DropZone.styles.tsx +99 -0
  4. package/src/components/DropZone.web.tsx +178 -0
  5. package/src/components/FilePickerButton.native.tsx +82 -0
  6. package/src/components/FilePickerButton.styles.tsx +112 -0
  7. package/src/components/FilePickerButton.web.tsx +84 -0
  8. package/src/components/UploadProgress.native.tsx +203 -0
  9. package/src/components/UploadProgress.styles.tsx +90 -0
  10. package/src/components/UploadProgress.web.tsx +201 -0
  11. package/src/components/index.native.ts +8 -0
  12. package/src/components/index.ts +6 -0
  13. package/src/components/index.web.ts +8 -0
  14. package/src/constants.ts +336 -0
  15. package/src/examples/index.ts +181 -0
  16. package/src/hooks/createUseFilePickerHook.ts +169 -0
  17. package/src/hooks/createUseFileUploadHook.ts +173 -0
  18. package/src/hooks/index.native.ts +12 -0
  19. package/src/hooks/index.ts +12 -0
  20. package/src/hooks/index.web.ts +12 -0
  21. package/src/index.native.ts +142 -0
  22. package/src/index.ts +139 -0
  23. package/src/index.web.ts +142 -0
  24. package/src/permissions/index.native.ts +8 -0
  25. package/src/permissions/index.ts +8 -0
  26. package/src/permissions/index.web.ts +8 -0
  27. package/src/permissions/permissions.native.ts +177 -0
  28. package/src/permissions/permissions.web.ts +96 -0
  29. package/src/picker/FilePicker.native.ts +407 -0
  30. package/src/picker/FilePicker.web.ts +366 -0
  31. package/src/picker/index.native.ts +2 -0
  32. package/src/picker/index.ts +2 -0
  33. package/src/picker/index.web.ts +2 -0
  34. package/src/types.ts +990 -0
  35. package/src/uploader/ChunkedUploader.ts +312 -0
  36. package/src/uploader/FileUploader.native.ts +435 -0
  37. package/src/uploader/FileUploader.web.ts +350 -0
  38. package/src/uploader/UploadQueue.ts +519 -0
  39. package/src/uploader/index.native.ts +4 -0
  40. package/src/uploader/index.ts +4 -0
  41. package/src/uploader/index.web.ts +4 -0
  42. package/src/utils.ts +586 -0
@@ -0,0 +1,84 @@
1
+ import React, { forwardRef, useCallback, useState } from 'react';
2
+ import { View, Text, Pressable, 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.
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
+ const [isHovered, setIsHovered] = useState(false);
28
+
29
+ const isDisabled = disabled || loading || isPicking;
30
+ const isLoading = loading || isPicking;
31
+
32
+ const handlePress = useCallback(async () => {
33
+ if (isDisabled) return;
34
+
35
+ try {
36
+ const result: FilePickerResult = await pick();
37
+ if (!result.cancelled) {
38
+ onPick?.(result);
39
+ }
40
+ } catch (err) {
41
+ onError?.(err as FilePickerError);
42
+ }
43
+ }, [isDisabled, pick, onPick, onError]);
44
+
45
+ // Apply variants
46
+ filePickerButtonStyles.useVariants({
47
+ size,
48
+ intent,
49
+ variant,
50
+ disabled: isDisabled,
51
+ });
52
+
53
+ return (
54
+ <Pressable
55
+ ref={ref}
56
+ onPress={handlePress}
57
+ disabled={isDisabled}
58
+ onHoverIn={() => setIsHovered(true)}
59
+ onHoverOut={() => setIsHovered(false)}
60
+ style={[filePickerButtonStyles.button({ intent, variant }), style]}
61
+ testID={testID}
62
+ accessibilityRole="button"
63
+ accessibilityState={{ disabled: isDisabled }}
64
+ accessibilityLabel={typeof children === 'string' ? children : 'Select file'}
65
+ >
66
+ {isLoading ? (
67
+ <ActivityIndicator
68
+ size="small"
69
+ color={filePickerButtonStyles.spinner({ intent, variant }).color as string}
70
+ />
71
+ ) : leftIcon ? (
72
+ <Text style={filePickerButtonStyles.icon({ intent, variant })}>
73
+ {/* Icon placeholder - in a real implementation, use MDI icon */}
74
+ 📁
75
+ </Text>
76
+ ) : null}
77
+ <Text style={filePickerButtonStyles.text({ intent, variant })}>
78
+ {children}
79
+ </Text>
80
+ </Pressable>
81
+ );
82
+ });
83
+
84
+ FilePickerButton.displayName = 'FilePickerButton';
@@ -0,0 +1,203 @@
1
+ import React from 'react';
2
+ import { View, Text, TouchableOpacity } from 'react-native';
3
+ import type { UploadProgressProps, Intent } from '../types';
4
+ import { uploadProgressStyles } from './UploadProgress.styles';
5
+ import { formatBytes, formatDuration } from '../utils';
6
+
7
+ /**
8
+ * UploadProgress - Shows upload progress with controls (Native).
9
+ */
10
+ export const UploadProgress: React.FC<UploadProgressProps> = (props) => {
11
+ const {
12
+ upload,
13
+ showFileName = true,
14
+ showFileSize = true,
15
+ showSpeed = false,
16
+ showETA = false,
17
+ showCancel = true,
18
+ showRetry = true,
19
+ onCancel,
20
+ onRetry,
21
+ variant = 'linear',
22
+ size = 'md',
23
+ style,
24
+ testID,
25
+ } = props;
26
+
27
+ // Determine intent based on state
28
+ const getIntent = (): Intent => {
29
+ switch (upload.state) {
30
+ case 'completed':
31
+ return 'success';
32
+ case 'failed':
33
+ return 'error';
34
+ case 'cancelled':
35
+ return 'secondary';
36
+ default:
37
+ return 'primary';
38
+ }
39
+ };
40
+
41
+ const intent = getIntent();
42
+
43
+ // Apply variants
44
+ uploadProgressStyles.useVariants({
45
+ size,
46
+ intent,
47
+ });
48
+
49
+ // Render state icon
50
+ const renderStateIcon = () => {
51
+ switch (upload.state) {
52
+ case 'completed':
53
+ return <Text style={uploadProgressStyles.stateIcon({ intent: 'success' })}>✓</Text>;
54
+ case 'failed':
55
+ return <Text style={uploadProgressStyles.stateIcon({ intent: 'error' })}>✗</Text>;
56
+ case 'cancelled':
57
+ return <Text style={uploadProgressStyles.stateIcon({ intent: 'secondary' })}>⊘</Text>;
58
+ case 'paused':
59
+ return <Text style={uploadProgressStyles.stateIcon({ intent: 'secondary' })}>⏸</Text>;
60
+ case 'pending':
61
+ return <Text style={uploadProgressStyles.stateIcon({ intent: 'secondary' })}>⏳</Text>;
62
+ default:
63
+ return null;
64
+ }
65
+ };
66
+
67
+ // Render progress bar
68
+ const renderProgressBar = () => {
69
+ if (upload.state !== 'uploading' && upload.state !== 'paused') {
70
+ return null;
71
+ }
72
+
73
+ return (
74
+ <View style={uploadProgressStyles.progressContainer({})}>
75
+ <View
76
+ style={[
77
+ uploadProgressStyles.progressBar({ intent }),
78
+ { width: `${upload.percentage}%` },
79
+ ]}
80
+ />
81
+ </View>
82
+ );
83
+ };
84
+
85
+ // Render details
86
+ const renderDetails = () => {
87
+ const details: string[] = [];
88
+
89
+ if (showFileSize) {
90
+ details.push(`${formatBytes(upload.bytesUploaded)} / ${formatBytes(upload.bytesTotal)}`);
91
+ }
92
+
93
+ if (showSpeed && upload.state === 'uploading' && upload.speed > 0) {
94
+ details.push(`${formatBytes(upload.speed)}/s`);
95
+ }
96
+
97
+ if (showETA && upload.state === 'uploading' && upload.estimatedTimeRemaining > 0) {
98
+ details.push(`${formatDuration(upload.estimatedTimeRemaining)} remaining`);
99
+ }
100
+
101
+ if (upload.currentChunk !== undefined && upload.totalChunks !== undefined) {
102
+ details.push(`Chunk ${upload.currentChunk}/${upload.totalChunks}`);
103
+ }
104
+
105
+ if (details.length === 0) {
106
+ return null;
107
+ }
108
+
109
+ return (
110
+ <View style={uploadProgressStyles.detailsRow({})}>
111
+ {details.map((detail, index) => (
112
+ <Text key={index} style={uploadProgressStyles.detail({})}>
113
+ {detail}
114
+ </Text>
115
+ ))}
116
+ </View>
117
+ );
118
+ };
119
+
120
+ // Render actions
121
+ const renderActions = () => {
122
+ const actions: React.ReactNode[] = [];
123
+
124
+ if (showCancel && (upload.state === 'uploading' || upload.state === 'pending')) {
125
+ actions.push(
126
+ <TouchableOpacity
127
+ key="cancel"
128
+ onPress={onCancel}
129
+ style={uploadProgressStyles.actionButton({})}
130
+ accessibilityRole="button"
131
+ accessibilityLabel="Cancel upload"
132
+ activeOpacity={0.7}
133
+ >
134
+ <Text style={uploadProgressStyles.actionIcon({})}>✗</Text>
135
+ </TouchableOpacity>
136
+ );
137
+ }
138
+
139
+ if (showRetry && upload.state === 'failed') {
140
+ actions.push(
141
+ <TouchableOpacity
142
+ key="retry"
143
+ onPress={onRetry}
144
+ style={uploadProgressStyles.actionButton({})}
145
+ accessibilityRole="button"
146
+ accessibilityLabel="Retry upload"
147
+ activeOpacity={0.7}
148
+ >
149
+ <Text style={uploadProgressStyles.actionIcon({})}>↻</Text>
150
+ </TouchableOpacity>
151
+ );
152
+ }
153
+
154
+ if (actions.length === 0) {
155
+ return null;
156
+ }
157
+
158
+ return (
159
+ <View style={uploadProgressStyles.actions({})}>
160
+ {actions}
161
+ </View>
162
+ );
163
+ };
164
+
165
+ // Render error message
166
+ const renderError = () => {
167
+ if (upload.state !== 'failed' || !upload.error) {
168
+ return null;
169
+ }
170
+
171
+ return (
172
+ <Text style={uploadProgressStyles.errorText({})}>
173
+ {upload.error.message}
174
+ </Text>
175
+ );
176
+ };
177
+
178
+ return (
179
+ <View style={[uploadProgressStyles.container({}), style]} testID={testID}>
180
+ {/* File info row */}
181
+ <View style={uploadProgressStyles.infoRow({})}>
182
+ {showFileName && (
183
+ <Text style={uploadProgressStyles.fileName({})} numberOfLines={1}>
184
+ {upload.file.name}
185
+ </Text>
186
+ )}
187
+ {renderStateIcon()}
188
+ {renderActions()}
189
+ </View>
190
+
191
+ {/* Progress bar */}
192
+ {renderProgressBar()}
193
+
194
+ {/* Details row */}
195
+ {renderDetails()}
196
+
197
+ {/* Error message */}
198
+ {renderError()}
199
+ </View>
200
+ );
201
+ };
202
+
203
+ UploadProgress.displayName = 'UploadProgress';
@@ -0,0 +1,90 @@
1
+ /**
2
+ * UploadProgress 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
+ export type UploadProgressVariants = {
15
+ size: Size;
16
+ intent: Intent;
17
+ };
18
+
19
+ export type UploadProgressDynamicProps = {
20
+ intent?: Intent;
21
+ size?: Size;
22
+ };
23
+
24
+ export const uploadProgressStyles = defineStyle('UploadProgress', (theme: Theme) => ({
25
+ container: (_props: UploadProgressDynamicProps) => ({
26
+ backgroundColor: theme.colors.surface.secondary,
27
+ borderRadius: 8,
28
+ padding: 12,
29
+ gap: 8,
30
+ }),
31
+ infoRow: (_props: UploadProgressDynamicProps) => ({
32
+ flexDirection: 'row',
33
+ alignItems: 'center',
34
+ justifyContent: 'space-between',
35
+ gap: 8,
36
+ }),
37
+ fileName: (_props: UploadProgressDynamicProps) => ({
38
+ flex: 1,
39
+ fontSize: 14,
40
+ fontWeight: '500',
41
+ color: theme.colors.text.primary,
42
+ }),
43
+ stateIcon: ({ intent = 'primary' }: UploadProgressDynamicProps) => ({
44
+ fontSize: 20,
45
+ color: theme.intents[intent].primary,
46
+ }),
47
+ progressContainer: (_props: UploadProgressDynamicProps) => ({
48
+ height: 8,
49
+ backgroundColor: theme.colors.surface.tertiary,
50
+ borderRadius: 4,
51
+ overflow: 'hidden',
52
+ }),
53
+ progressBar: ({ intent = 'primary' }: UploadProgressDynamicProps) => ({
54
+ height: '100%',
55
+ backgroundColor: theme.intents[intent].primary,
56
+ borderRadius: 4,
57
+ }),
58
+ detailsRow: (_props: UploadProgressDynamicProps) => ({
59
+ flexDirection: 'row',
60
+ alignItems: 'center',
61
+ justifyContent: 'space-between',
62
+ flexWrap: 'wrap',
63
+ gap: 8,
64
+ }),
65
+ detail: (_props: UploadProgressDynamicProps) => ({
66
+ fontSize: 12,
67
+ color: theme.colors.text.secondary,
68
+ }),
69
+ actions: (_props: UploadProgressDynamicProps) => ({
70
+ flexDirection: 'row',
71
+ alignItems: 'center',
72
+ gap: 8,
73
+ }),
74
+ actionButton: (_props: UploadProgressDynamicProps) => ({
75
+ padding: 4,
76
+ borderRadius: 4,
77
+ _web: {
78
+ cursor: 'pointer',
79
+ },
80
+ }),
81
+ actionIcon: ({ intent = 'neutral' }: UploadProgressDynamicProps) => ({
82
+ fontSize: 20,
83
+ color: theme.intents[intent].primary,
84
+ }),
85
+ errorText: (_props: UploadProgressDynamicProps) => ({
86
+ fontSize: 12,
87
+ color: theme.intents.danger.primary,
88
+ marginTop: 4,
89
+ }),
90
+ }));
@@ -0,0 +1,201 @@
1
+ import React from 'react';
2
+ import { View, Text, Pressable } from 'react-native';
3
+ import type { UploadProgressProps, Intent } from '../types';
4
+ import { uploadProgressStyles } from './UploadProgress.styles';
5
+ import { formatBytes, formatDuration } from '../utils';
6
+
7
+ /**
8
+ * UploadProgress - Shows upload progress with controls (Web).
9
+ */
10
+ export const UploadProgress: React.FC<UploadProgressProps> = (props) => {
11
+ const {
12
+ upload,
13
+ showFileName = true,
14
+ showFileSize = true,
15
+ showSpeed = false,
16
+ showETA = false,
17
+ showCancel = true,
18
+ showRetry = true,
19
+ onCancel,
20
+ onRetry,
21
+ variant = 'linear',
22
+ size = 'md',
23
+ style,
24
+ testID,
25
+ } = props;
26
+
27
+ // Determine intent based on state
28
+ const getIntent = (): Intent => {
29
+ switch (upload.state) {
30
+ case 'completed':
31
+ return 'success';
32
+ case 'failed':
33
+ return 'error';
34
+ case 'cancelled':
35
+ return 'secondary';
36
+ default:
37
+ return 'primary';
38
+ }
39
+ };
40
+
41
+ const intent = getIntent();
42
+
43
+ // Apply variants
44
+ uploadProgressStyles.useVariants({
45
+ size,
46
+ intent,
47
+ });
48
+
49
+ // Render state icon
50
+ const renderStateIcon = () => {
51
+ switch (upload.state) {
52
+ case 'completed':
53
+ return <Text style={uploadProgressStyles.stateIcon({ intent: 'success' })}>✓</Text>;
54
+ case 'failed':
55
+ return <Text style={uploadProgressStyles.stateIcon({ intent: 'error' })}>✗</Text>;
56
+ case 'cancelled':
57
+ return <Text style={uploadProgressStyles.stateIcon({ intent: 'secondary' })}>⊘</Text>;
58
+ case 'paused':
59
+ return <Text style={uploadProgressStyles.stateIcon({ intent: 'secondary' })}>⏸</Text>;
60
+ case 'pending':
61
+ return <Text style={uploadProgressStyles.stateIcon({ intent: 'secondary' })}>⏳</Text>;
62
+ default:
63
+ return null;
64
+ }
65
+ };
66
+
67
+ // Render progress bar
68
+ const renderProgressBar = () => {
69
+ if (upload.state !== 'uploading' && upload.state !== 'paused') {
70
+ return null;
71
+ }
72
+
73
+ return (
74
+ <View style={uploadProgressStyles.progressContainer({})}>
75
+ <View
76
+ style={[
77
+ uploadProgressStyles.progressBar({ intent }),
78
+ { width: `${upload.percentage}%` },
79
+ ]}
80
+ />
81
+ </View>
82
+ );
83
+ };
84
+
85
+ // Render details
86
+ const renderDetails = () => {
87
+ const details: string[] = [];
88
+
89
+ if (showFileSize) {
90
+ details.push(`${formatBytes(upload.bytesUploaded)} / ${formatBytes(upload.bytesTotal)}`);
91
+ }
92
+
93
+ if (showSpeed && upload.state === 'uploading' && upload.speed > 0) {
94
+ details.push(`${formatBytes(upload.speed)}/s`);
95
+ }
96
+
97
+ if (showETA && upload.state === 'uploading' && upload.estimatedTimeRemaining > 0) {
98
+ details.push(`${formatDuration(upload.estimatedTimeRemaining)} remaining`);
99
+ }
100
+
101
+ if (upload.currentChunk !== undefined && upload.totalChunks !== undefined) {
102
+ details.push(`Chunk ${upload.currentChunk}/${upload.totalChunks}`);
103
+ }
104
+
105
+ if (details.length === 0) {
106
+ return null;
107
+ }
108
+
109
+ return (
110
+ <View style={uploadProgressStyles.detailsRow({})}>
111
+ {details.map((detail, index) => (
112
+ <Text key={index} style={uploadProgressStyles.detail({})}>
113
+ {detail}
114
+ </Text>
115
+ ))}
116
+ </View>
117
+ );
118
+ };
119
+
120
+ // Render actions
121
+ const renderActions = () => {
122
+ const actions: React.ReactNode[] = [];
123
+
124
+ if (showCancel && (upload.state === 'uploading' || upload.state === 'pending')) {
125
+ actions.push(
126
+ <Pressable
127
+ key="cancel"
128
+ onPress={onCancel}
129
+ style={uploadProgressStyles.actionButton({})}
130
+ accessibilityRole="button"
131
+ accessibilityLabel="Cancel upload"
132
+ >
133
+ <Text style={uploadProgressStyles.actionIcon({})}>✗</Text>
134
+ </Pressable>
135
+ );
136
+ }
137
+
138
+ if (showRetry && upload.state === 'failed') {
139
+ actions.push(
140
+ <Pressable
141
+ key="retry"
142
+ onPress={onRetry}
143
+ style={uploadProgressStyles.actionButton({})}
144
+ accessibilityRole="button"
145
+ accessibilityLabel="Retry upload"
146
+ >
147
+ <Text style={uploadProgressStyles.actionIcon({})}>↻</Text>
148
+ </Pressable>
149
+ );
150
+ }
151
+
152
+ if (actions.length === 0) {
153
+ return null;
154
+ }
155
+
156
+ return (
157
+ <View style={uploadProgressStyles.actions({})}>
158
+ {actions}
159
+ </View>
160
+ );
161
+ };
162
+
163
+ // Render error message
164
+ const renderError = () => {
165
+ if (upload.state !== 'failed' || !upload.error) {
166
+ return null;
167
+ }
168
+
169
+ return (
170
+ <Text style={uploadProgressStyles.errorText({})}>
171
+ {upload.error.message}
172
+ </Text>
173
+ );
174
+ };
175
+
176
+ return (
177
+ <View style={[uploadProgressStyles.container({}), style]} testID={testID}>
178
+ {/* File info row */}
179
+ <View style={uploadProgressStyles.infoRow({})}>
180
+ {showFileName && (
181
+ <Text style={uploadProgressStyles.fileName({})} numberOfLines={1}>
182
+ {upload.file.name}
183
+ </Text>
184
+ )}
185
+ {renderStateIcon()}
186
+ {renderActions()}
187
+ </View>
188
+
189
+ {/* Progress bar */}
190
+ {renderProgressBar()}
191
+
192
+ {/* Details row */}
193
+ {renderDetails()}
194
+
195
+ {/* Error message */}
196
+ {renderError()}
197
+ </View>
198
+ );
199
+ };
200
+
201
+ UploadProgress.displayName = 'UploadProgress';
@@ -0,0 +1,8 @@
1
+ export { FilePickerButton } from './FilePickerButton.native';
2
+ export { DropZone } from './DropZone.native';
3
+ export { UploadProgress } from './UploadProgress.native';
4
+
5
+ // Styles
6
+ export { filePickerButtonStyles } from './FilePickerButton.styles';
7
+ export { dropZoneStyles } from './DropZone.styles';
8
+ export { uploadProgressStyles } from './UploadProgress.styles';
@@ -0,0 +1,6 @@
1
+ // Styles (platform-agnostic)
2
+ export { filePickerButtonStyles } from './FilePickerButton.styles';
3
+ export { dropZoneStyles } from './DropZone.styles';
4
+ export { uploadProgressStyles } from './UploadProgress.styles';
5
+
6
+ // Platform-specific components are exported from index.web.ts and index.native.ts
@@ -0,0 +1,8 @@
1
+ export { FilePickerButton } from './FilePickerButton.web';
2
+ export { DropZone } from './DropZone.web';
3
+ export { UploadProgress } from './UploadProgress.web';
4
+
5
+ // Styles
6
+ export { filePickerButtonStyles } from './FilePickerButton.styles';
7
+ export { dropZoneStyles } from './DropZone.styles';
8
+ export { uploadProgressStyles } from './UploadProgress.styles';