@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 names in the drop zone */
7
- showFileNames?: boolean;
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
- onSelect?: FileTriggerProps['onSelect'];
30
- } & MarginPadding & WidthHeight & Omit<DropZoneProps, 'isDisabled' | 'children'> & Omit<FileTriggerProps, 'children'>;
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 Ellipsis from "./Ellipsis.js";
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', showFileNames = true, disabled, invalid, errorMessage, acceptedFileTypes, allowsMultiple, defaultCamera, acceptDirectory, onDrop, onSelect, ...rest })=>{
10
- const [displayFiles, setDisplayFiles] = useState('');
11
- const showFileTrigger = !!acceptedFileTypes || !!allowsMultiple || !!defaultCamera || !!acceptDirectory || !!onSelect;
12
- const handleFileDrop = (event)=>{
13
- let files = event.items.filter((file)=>'file' === file.kind);
14
- if (acceptedFileTypes && acceptedFileTypes.length > 0) files = files.filter((file)=>matchesFileTypes(file, acceptedFileTypes));
15
- if (!allowsMultiple && files.length > 1) files = files.slice(0, 1);
16
- const fileNames = files.map(({ name })=>name).join(', ');
17
- setDisplayFiles(fileNames);
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 files = Array.from(selected);
23
- const fileNames = files.map(({ name })=>name).join(', ');
24
- setDisplayFiles(fileNames);
25
- if (onSelect) onSelect(selected);
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
- return /*#__PURE__*/ jsxs(StyledDropZone, {
28
- isDisabled: disabled,
29
- invalid: invalid,
30
- onDrop: showFileTrigger ? handleFileDrop : onDrop,
31
- ...rest,
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__*/ jsx(StyledText, {
34
- slot: "label",
35
- children: label
36
- }),
37
- showFileTrigger && /*#__PURE__*/ jsx(SeparatorText, {
38
- children: "or"
39
- }),
40
- showFileTrigger && /*#__PURE__*/ jsx(FileTrigger, {
41
- acceptedFileTypes: acceptedFileTypes,
42
- allowsMultiple: allowsMultiple,
43
- defaultCamera: defaultCamera,
44
- acceptDirectory: acceptDirectory,
45
- onSelect: handleFileSelect
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
- displayFiles && showFileNames && /*#__PURE__*/ jsx(FilesContainer, {
48
- title: displayFiles,
49
- children: /*#__PURE__*/ jsx(Ellipsis, {
50
- children: displayFiles
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
- errorMessage && /*#__PURE__*/ jsx(ErrorMessageText, {
54
- children: errorMessage
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 SeparatorText = styled(Text)(({ theme })=>({
98
- fontSize: theme.font.size.md,
99
- color: theme.scale6,
100
- lineHeight: '14px'
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 FilesContainer = styled.div(({ theme })=>({
256
+ const FileItem = styled.div(({ theme })=>({
103
257
  display: 'flex',
104
258
  alignItems: 'center',
105
- justifyContent: 'center',
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
- color: theme.scale6,
108
- width: '100%'
268
+ lineHeight: '12px',
269
+ flex: 1,
270
+ overflow: 'hidden',
271
+ textOverflow: 'ellipsis',
272
+ whiteSpace: 'nowrap'
109
273
  }));
110
- const ErrorMessageText = styled(StyledText)(({ theme })=>({
111
- color: theme.color.error
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
- children: "Select files"
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';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wavv/ui",
3
- "version": "2.3.6",
3
+ "version": "2.3.7-alpha.2",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {