@wavv/ui 2.3.6 → 2.3.7-alpha.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.
|
@@ -1,16 +1,33 @@
|
|
|
1
1
|
import { type DropZoneProps, type FileTriggerProps } from 'react-aria-components';
|
|
2
2
|
import type { MarginPadding, WidthHeight } from './types';
|
|
3
|
+
/** Represents a file managed by the DropZone */
|
|
4
|
+
export type DropZoneFile = {
|
|
5
|
+
/** Unique identifier for the file */
|
|
6
|
+
id: string;
|
|
7
|
+
/** Name of the file */
|
|
8
|
+
name: string;
|
|
9
|
+
/** The File object */
|
|
10
|
+
file: File;
|
|
11
|
+
};
|
|
3
12
|
type Props = {
|
|
4
13
|
/** The label to display inside the drop zone */
|
|
5
14
|
label?: string;
|
|
6
|
-
/** Whether to show the file
|
|
7
|
-
|
|
15
|
+
/** Whether to show the file list below the drop zone with remove buttons */
|
|
16
|
+
showFileList?: boolean;
|
|
8
17
|
/** Whether the drop target is disabled. If true, the drop target will not accept any drops. */
|
|
9
18
|
disabled?: DropZoneProps['isDisabled'];
|
|
10
19
|
/** Whether the drop zone is in an invalid state */
|
|
11
20
|
invalid?: boolean;
|
|
12
21
|
/** Error message to display below the file names */
|
|
13
22
|
errorMessage?: string;
|
|
23
|
+
/** Array of supported file format names to display (e.g., ['PNG', 'JPG', 'PDF']) */
|
|
24
|
+
supportedFormats?: string[];
|
|
25
|
+
/** Maximum file size string to display (e.g., '25MB') */
|
|
26
|
+
maxFileSize?: string;
|
|
27
|
+
/** Callback when files are added. */
|
|
28
|
+
onFilesAdded?: (files: DropZoneFile[]) => void;
|
|
29
|
+
/** Callback when files are removed. */
|
|
30
|
+
onFilesRemoved?: (files: DropZoneFile[]) => void;
|
|
14
31
|
className?: DropZoneProps['className'];
|
|
15
32
|
style?: DropZoneProps['style'];
|
|
16
33
|
onDrop?: DropZoneProps['onDrop'];
|
|
@@ -26,7 +43,6 @@ type Props = {
|
|
|
26
43
|
allowsMultiple?: FileTriggerProps['allowsMultiple'];
|
|
27
44
|
defaultCamera?: FileTriggerProps['defaultCamera'];
|
|
28
45
|
acceptDirectory?: FileTriggerProps['acceptDirectory'];
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
declare const DropZone: ({ label, showFileNames, disabled, invalid, errorMessage, acceptedFileTypes, allowsMultiple, defaultCamera, acceptDirectory, onDrop, onSelect, ...rest }: Props) => import("react/jsx-runtime").JSX.Element;
|
|
46
|
+
} & MarginPadding & WidthHeight & Omit<DropZoneProps, 'isDisabled' | 'children'> & Omit<FileTriggerProps, 'children' | 'onSelect'>;
|
|
47
|
+
declare const DropZone: ({ label, showFileList, disabled, invalid, errorMessage, supportedFormats, maxFileSize, acceptedFileTypes, allowsMultiple, defaultCamera, acceptDirectory, onDrop, onFilesAdded, onFilesRemoved, width, margin, marginTop, marginRight, marginBottom, marginLeft, ...rest }: Props) => import("react/jsx-runtime").JSX.Element;
|
|
32
48
|
export default DropZone;
|
|
@@ -1,61 +1,205 @@
|
|
|
1
1
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
2
|
import styled from "@emotion/styled";
|
|
3
|
-
import { useState } from "react";
|
|
3
|
+
import { useId, useState } from "react";
|
|
4
4
|
import { DropZone, Text } from "react-aria-components";
|
|
5
5
|
import matchesFileTypes from "../utils/matchesFileTypes.js";
|
|
6
|
-
import
|
|
6
|
+
import Button from "./Button/index.js";
|
|
7
7
|
import FileTrigger from "./FileTrigger.js";
|
|
8
8
|
import { marginProps, paddingProps, widthHeightProps } from "./helpers/styledProps.js";
|
|
9
|
-
const DropZone_DropZone = ({ label = 'Drop here',
|
|
10
|
-
const [
|
|
11
|
-
const
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
9
|
+
const DropZone_DropZone = ({ label = 'Drop here', showFileList = true, disabled, invalid, errorMessage, supportedFormats, maxFileSize, acceptedFileTypes, allowsMultiple, defaultCamera, acceptDirectory, onDrop, onFilesAdded, onFilesRemoved, width, margin, marginTop, marginRight, marginBottom, marginLeft, ...rest })=>{
|
|
10
|
+
const [files, setFiles] = useState([]);
|
|
11
|
+
const [sizeValidationError, setSizeValidationError] = useState(null);
|
|
12
|
+
const [typeValidationError, setTypeValidationError] = useState(null);
|
|
13
|
+
const instanceId = useId();
|
|
14
|
+
const hasFileCallbacks = !!onFilesAdded || !!onFilesRemoved;
|
|
15
|
+
const shouldManageFiles = showFileList || hasFileCallbacks;
|
|
16
|
+
const showFileTrigger = !!acceptedFileTypes || !!allowsMultiple || !!defaultCamera || !!acceptDirectory || hasFileCallbacks;
|
|
17
|
+
const generateFileId = (name, index)=>`${instanceId}-${name}-${Date.now()}-${index}`;
|
|
18
|
+
const parseFileSize = (sizeString)=>{
|
|
19
|
+
const match = sizeString.trim().match(/^(\d+(?:\.\d+)?)\s*(KB|MB|GB|TB|B)$/i);
|
|
20
|
+
if (!match) return null;
|
|
21
|
+
const value = parseFloat(match[1]);
|
|
22
|
+
const unit = match[2].toUpperCase();
|
|
23
|
+
const multipliers = {
|
|
24
|
+
B: 1,
|
|
25
|
+
KB: 1024,
|
|
26
|
+
MB: 1048576,
|
|
27
|
+
GB: 1073741824,
|
|
28
|
+
TB: 1099511627776
|
|
29
|
+
};
|
|
30
|
+
return value * (multipliers[unit] || 1);
|
|
31
|
+
};
|
|
32
|
+
const validateFileSizes = (fileList)=>{
|
|
33
|
+
if (!maxFileSize) return {
|
|
34
|
+
valid: fileList,
|
|
35
|
+
oversized: []
|
|
36
|
+
};
|
|
37
|
+
const maxBytes = parseFileSize(maxFileSize);
|
|
38
|
+
if (null === maxBytes) return {
|
|
39
|
+
valid: fileList,
|
|
40
|
+
oversized: []
|
|
41
|
+
};
|
|
42
|
+
const valid = [];
|
|
43
|
+
const oversized = [];
|
|
44
|
+
for (const file of fileList)if (file.size <= maxBytes) valid.push(file);
|
|
45
|
+
else oversized.push(file);
|
|
46
|
+
return {
|
|
47
|
+
valid,
|
|
48
|
+
oversized
|
|
49
|
+
};
|
|
50
|
+
};
|
|
51
|
+
const addFiles = async (newFileList)=>{
|
|
52
|
+
const { valid, oversized } = validateFileSizes(newFileList);
|
|
53
|
+
if (oversized.length > 0) {
|
|
54
|
+
const oversizedNames = oversized.map((file)=>file.name).join(', ');
|
|
55
|
+
setSizeValidationError(`${oversizedNames} ${1 === oversized.length ? 'exceeds' : 'exceed'} maximum file size of ${maxFileSize}`);
|
|
56
|
+
} else setSizeValidationError(null);
|
|
57
|
+
if (0 === valid.length) return;
|
|
58
|
+
const newEntries = valid.map((file, index)=>({
|
|
59
|
+
id: generateFileId(file.name, index),
|
|
60
|
+
name: file.name,
|
|
61
|
+
file
|
|
62
|
+
}));
|
|
63
|
+
const updatedFiles = allowsMultiple ? [
|
|
64
|
+
...files,
|
|
65
|
+
...newEntries
|
|
66
|
+
] : newEntries;
|
|
67
|
+
setFiles(updatedFiles);
|
|
68
|
+
onFilesAdded?.(newEntries);
|
|
69
|
+
};
|
|
70
|
+
const handleFileDrop = async (event)=>{
|
|
71
|
+
let fileItems = event.items.filter((file)=>'file' === file.kind);
|
|
72
|
+
if (acceptedFileTypes && acceptedFileTypes.length > 0) {
|
|
73
|
+
const allFileItems = [
|
|
74
|
+
...fileItems
|
|
75
|
+
];
|
|
76
|
+
fileItems = fileItems.filter((file)=>matchesFileTypes(file, acceptedFileTypes));
|
|
77
|
+
const unsupportedItems = allFileItems.filter((file)=>!matchesFileTypes(file, acceptedFileTypes));
|
|
78
|
+
if (unsupportedItems.length > 0 && shouldManageFiles) {
|
|
79
|
+
const unsupportedNames = unsupportedItems.map((item)=>item.name || 'Unknown file').join(', ');
|
|
80
|
+
setTypeValidationError(`${unsupportedNames} ${1 === unsupportedItems.length ? 'is not a supported file type' : 'are not supported file types'}`);
|
|
81
|
+
} else setTypeValidationError(null);
|
|
82
|
+
} else setTypeValidationError(null);
|
|
83
|
+
if (!allowsMultiple && fileItems.length > 1) fileItems = fileItems.slice(0, 1);
|
|
84
|
+
if (shouldManageFiles) {
|
|
85
|
+
const filePromises = fileItems.map((item)=>{
|
|
86
|
+
if ('getFile' in item && 'function' == typeof item.getFile) return item.getFile();
|
|
87
|
+
return null;
|
|
88
|
+
});
|
|
89
|
+
const resolvedFiles = (await Promise.all(filePromises)).filter((f)=>null !== f);
|
|
90
|
+
await addFiles(resolvedFiles);
|
|
91
|
+
}
|
|
18
92
|
if (onDrop) onDrop(event);
|
|
19
93
|
};
|
|
20
|
-
const handleFileSelect = (selected)=>{
|
|
94
|
+
const handleFileSelect = async (selected)=>{
|
|
21
95
|
if (!selected) return;
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
96
|
+
const selectedFiles = Array.from(selected);
|
|
97
|
+
if (shouldManageFiles) await addFiles(selectedFiles);
|
|
98
|
+
};
|
|
99
|
+
const handleRemoveFile = (id)=>{
|
|
100
|
+
const removedFile = files.find((file)=>file.id === id);
|
|
101
|
+
const updatedFiles = files.filter((file)=>file.id !== id);
|
|
102
|
+
setFiles(updatedFiles);
|
|
103
|
+
if (removedFile) onFilesRemoved?.([
|
|
104
|
+
removedFile
|
|
105
|
+
]);
|
|
106
|
+
};
|
|
107
|
+
const containerProps = {
|
|
108
|
+
width,
|
|
109
|
+
margin,
|
|
110
|
+
marginTop,
|
|
111
|
+
marginRight,
|
|
112
|
+
marginBottom,
|
|
113
|
+
marginLeft
|
|
26
114
|
};
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
115
|
+
const showManagedFileList = showFileList && files.length > 0;
|
|
116
|
+
const showFileInfo = !!supportedFormats || !!maxFileSize;
|
|
117
|
+
const getErrorMessage = ()=>{
|
|
118
|
+
if (errorMessage) return errorMessage;
|
|
119
|
+
const errors = [];
|
|
120
|
+
if (typeValidationError) errors.push(typeValidationError);
|
|
121
|
+
if (sizeValidationError) errors.push(sizeValidationError);
|
|
122
|
+
return errors.length > 0 ? errors.join('. ') : null;
|
|
123
|
+
};
|
|
124
|
+
const displayError = getErrorMessage();
|
|
125
|
+
return /*#__PURE__*/ jsxs(Container, {
|
|
126
|
+
...containerProps,
|
|
32
127
|
children: [
|
|
33
|
-
/*#__PURE__*/
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
children:
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
128
|
+
/*#__PURE__*/ jsxs(StyledDropZone, {
|
|
129
|
+
isDisabled: disabled,
|
|
130
|
+
invalid: invalid,
|
|
131
|
+
onDrop: handleFileDrop,
|
|
132
|
+
...rest,
|
|
133
|
+
children: [
|
|
134
|
+
/*#__PURE__*/ jsxs(TriggerContainer, {
|
|
135
|
+
children: [
|
|
136
|
+
showFileTrigger && /*#__PURE__*/ jsx(FileTrigger, {
|
|
137
|
+
acceptedFileTypes: acceptedFileTypes,
|
|
138
|
+
allowsMultiple: allowsMultiple,
|
|
139
|
+
defaultCamera: defaultCamera,
|
|
140
|
+
acceptDirectory: acceptDirectory,
|
|
141
|
+
onSelect: handleFileSelect
|
|
142
|
+
}),
|
|
143
|
+
/*#__PURE__*/ jsxs(StyledText, {
|
|
144
|
+
slot: "label",
|
|
145
|
+
children: [
|
|
146
|
+
showFileTrigger ? 'or ' : '',
|
|
147
|
+
" ",
|
|
148
|
+
showFileTrigger ? label.toLowerCase() : label
|
|
149
|
+
]
|
|
150
|
+
})
|
|
151
|
+
]
|
|
152
|
+
}),
|
|
153
|
+
displayError && /*#__PURE__*/ jsx(ErrorMessageText, {
|
|
154
|
+
children: displayError
|
|
155
|
+
})
|
|
156
|
+
]
|
|
46
157
|
}),
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
158
|
+
showFileInfo && /*#__PURE__*/ jsxs(FileInfo, {
|
|
159
|
+
children: [
|
|
160
|
+
/*#__PURE__*/ jsx(FileInfoLeft, {
|
|
161
|
+
children: supportedFormats && /*#__PURE__*/ jsxs("div", {
|
|
162
|
+
children: [
|
|
163
|
+
"Supported formats: ",
|
|
164
|
+
supportedFormats.join(', ')
|
|
165
|
+
]
|
|
166
|
+
})
|
|
167
|
+
}),
|
|
168
|
+
/*#__PURE__*/ jsx(FileInfoRight, {
|
|
169
|
+
children: maxFileSize && /*#__PURE__*/ jsxs("div", {
|
|
170
|
+
children: [
|
|
171
|
+
"Maximum file size: ",
|
|
172
|
+
maxFileSize
|
|
173
|
+
]
|
|
174
|
+
})
|
|
175
|
+
})
|
|
176
|
+
]
|
|
52
177
|
}),
|
|
53
|
-
|
|
54
|
-
children:
|
|
178
|
+
showManagedFileList && /*#__PURE__*/ jsx(FileList, {
|
|
179
|
+
children: files.map((file)=>/*#__PURE__*/ jsxs(FileItem, {
|
|
180
|
+
children: [
|
|
181
|
+
/*#__PURE__*/ jsx(FileName, {
|
|
182
|
+
title: file.name,
|
|
183
|
+
children: file.name
|
|
184
|
+
}),
|
|
185
|
+
!disabled && /*#__PURE__*/ jsx(Button, {
|
|
186
|
+
tiny: true,
|
|
187
|
+
subtle: true,
|
|
188
|
+
secondary: true,
|
|
189
|
+
icon: "close",
|
|
190
|
+
onClick: ()=>handleRemoveFile(file.id)
|
|
191
|
+
})
|
|
192
|
+
]
|
|
193
|
+
}, file.id))
|
|
55
194
|
})
|
|
56
195
|
]
|
|
57
196
|
});
|
|
58
197
|
};
|
|
198
|
+
const Container = styled.div({
|
|
199
|
+
display: 'flex',
|
|
200
|
+
flexDirection: 'column',
|
|
201
|
+
gap: 4
|
|
202
|
+
}, widthHeightProps, marginProps);
|
|
59
203
|
const StyledDropZone = styled(DropZone)(({ theme, invalid })=>({
|
|
60
204
|
display: 'flex',
|
|
61
205
|
flexDirection: 'column',
|
|
@@ -88,27 +232,56 @@ const StyledDropZone = styled(DropZone)(({ theme, invalid })=>({
|
|
|
88
232
|
}
|
|
89
233
|
}
|
|
90
234
|
}), widthHeightProps, marginProps, paddingProps);
|
|
235
|
+
const TriggerContainer = styled.div({
|
|
236
|
+
display: 'flex',
|
|
237
|
+
alignItems: 'center',
|
|
238
|
+
justifyContent: 'center',
|
|
239
|
+
gap: 8
|
|
240
|
+
});
|
|
91
241
|
const StyledText = styled(Text)(({ theme })=>({
|
|
92
242
|
fontSize: theme.font.size.md,
|
|
93
243
|
fontWeight: theme.font.weight.medium,
|
|
94
244
|
lineHeight: '14px',
|
|
95
245
|
textAlign: 'center'
|
|
96
246
|
}));
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
247
|
+
const ErrorMessageText = styled(StyledText)(({ theme })=>({
|
|
248
|
+
color: theme.color.error
|
|
249
|
+
}));
|
|
250
|
+
const FileList = styled.div(({ theme })=>({
|
|
251
|
+
display: 'flex',
|
|
252
|
+
flexDirection: 'row',
|
|
253
|
+
flexWrap: 'wrap',
|
|
254
|
+
gap: theme.size.xs
|
|
101
255
|
}));
|
|
102
|
-
const
|
|
256
|
+
const FileItem = styled.div(({ theme })=>({
|
|
103
257
|
display: 'flex',
|
|
104
258
|
alignItems: 'center',
|
|
105
|
-
justifyContent: '
|
|
259
|
+
justifyContent: 'space-between',
|
|
260
|
+
gap: theme.size.xs,
|
|
261
|
+
padding: '4px 8px',
|
|
262
|
+
paddingRight: 4,
|
|
263
|
+
backgroundColor: theme.scale0,
|
|
264
|
+
borderRadius: 4
|
|
265
|
+
}));
|
|
266
|
+
const FileName = styled.div(({ theme })=>({
|
|
106
267
|
fontSize: theme.font.size.sm,
|
|
107
|
-
|
|
108
|
-
|
|
268
|
+
lineHeight: '12px',
|
|
269
|
+
flex: 1,
|
|
270
|
+
overflow: 'hidden',
|
|
271
|
+
textOverflow: 'ellipsis',
|
|
272
|
+
whiteSpace: 'nowrap'
|
|
109
273
|
}));
|
|
110
|
-
const
|
|
111
|
-
|
|
274
|
+
const FileInfo = styled.div(({ theme })=>({
|
|
275
|
+
display: 'flex',
|
|
276
|
+
alignItems: 'center',
|
|
277
|
+
justifyContent: 'space-between',
|
|
278
|
+
gap: 16,
|
|
279
|
+
fontSize: theme.font.size.sm,
|
|
280
|
+
color: theme.scale6
|
|
112
281
|
}));
|
|
282
|
+
const FileInfoLeft = styled.div({});
|
|
283
|
+
const FileInfoRight = styled.div({
|
|
284
|
+
textAlign: 'right'
|
|
285
|
+
});
|
|
113
286
|
const components_DropZone = DropZone_DropZone;
|
|
114
287
|
export { components_DropZone as default };
|
|
@@ -4,9 +4,9 @@ import Button from "./Button/index.js";
|
|
|
4
4
|
const FileTrigger_FileTrigger = (props)=>/*#__PURE__*/ jsx(FileTrigger, {
|
|
5
5
|
...props,
|
|
6
6
|
children: /*#__PURE__*/ jsx(Button, {
|
|
7
|
-
small: true,
|
|
8
7
|
outline: true,
|
|
9
|
-
|
|
8
|
+
icon: "upload",
|
|
9
|
+
children: "Upload"
|
|
10
10
|
})
|
|
11
11
|
});
|
|
12
12
|
const components_FileTrigger = FileTrigger_FileTrigger;
|
package/build/index.d.ts
CHANGED
|
@@ -76,6 +76,7 @@ export type { ThemeOption } from './theme';
|
|
|
76
76
|
export type { EditorValue } from './components/Editor';
|
|
77
77
|
export type { Selection } from 'react-aria-components';
|
|
78
78
|
export type { DropEvent, DropItem } from 'react-aria';
|
|
79
|
+
export type { DropZoneFile } from './components/DropZone';
|
|
79
80
|
export type { DotType } from './components/Dot';
|
|
80
81
|
export { default as useConfirm } from './hooks/useConfirm';
|
|
81
82
|
export { default as useElementObserver } from './hooks/useElementObserver';
|