@wavv/ui 2.3.6 → 2.3.7-alpha.1

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,186 @@
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 instanceId = useId();
13
+ const hasFileCallbacks = !!onFilesAdded || !!onFilesRemoved;
14
+ const shouldManageFiles = showFileList || hasFileCallbacks;
15
+ const showFileTrigger = !!acceptedFileTypes || !!allowsMultiple || !!defaultCamera || !!acceptDirectory || hasFileCallbacks;
16
+ const generateFileId = (name, index)=>`${instanceId}-${name}-${Date.now()}-${index}`;
17
+ const parseFileSize = (sizeString)=>{
18
+ const match = sizeString.trim().match(/^(\d+(?:\.\d+)?)\s*(KB|MB|GB|TB|B)$/i);
19
+ if (!match) return null;
20
+ const value = parseFloat(match[1]);
21
+ const unit = match[2].toUpperCase();
22
+ const multipliers = {
23
+ B: 1,
24
+ KB: 1024,
25
+ MB: 1048576,
26
+ GB: 1073741824,
27
+ TB: 1099511627776
28
+ };
29
+ return value * (multipliers[unit] || 1);
30
+ };
31
+ const validateFileSizes = (fileList)=>{
32
+ if (!maxFileSize) return {
33
+ valid: fileList,
34
+ oversized: []
35
+ };
36
+ const maxBytes = parseFileSize(maxFileSize);
37
+ if (null === maxBytes) return {
38
+ valid: fileList,
39
+ oversized: []
40
+ };
41
+ const valid = [];
42
+ const oversized = [];
43
+ for (const file of fileList)if (file.size <= maxBytes) valid.push(file);
44
+ else oversized.push(file);
45
+ return {
46
+ valid,
47
+ oversized
48
+ };
49
+ };
50
+ const addFiles = async (newFileList)=>{
51
+ const { valid, oversized } = validateFileSizes(newFileList);
52
+ if (oversized.length > 0) {
53
+ const oversizedNames = oversized.map((file)=>file.name).join(', ');
54
+ setSizeValidationError(`Error: ${oversizedNames} ${1 === oversized.length ? 'exceeds' : 'exceed'} maximum file size of ${maxFileSize}`);
55
+ } else setSizeValidationError(null);
56
+ if (0 === valid.length) return;
57
+ const newEntries = valid.map((file, index)=>({
58
+ id: generateFileId(file.name, index),
59
+ name: file.name,
60
+ file
61
+ }));
62
+ const updatedFiles = allowsMultiple ? [
63
+ ...files,
64
+ ...newEntries
65
+ ] : newEntries;
66
+ setFiles(updatedFiles);
67
+ onFilesAdded?.(newEntries);
68
+ };
69
+ const handleFileDrop = async (event)=>{
70
+ let fileItems = event.items.filter((file)=>'file' === file.kind);
71
+ if (acceptedFileTypes && acceptedFileTypes.length > 0) fileItems = fileItems.filter((file)=>matchesFileTypes(file, acceptedFileTypes));
72
+ if (!allowsMultiple && fileItems.length > 1) fileItems = fileItems.slice(0, 1);
73
+ if (shouldManageFiles) {
74
+ const filePromises = fileItems.map((item)=>{
75
+ if ('getFile' in item && 'function' == typeof item.getFile) return item.getFile();
76
+ return null;
77
+ });
78
+ const resolvedFiles = (await Promise.all(filePromises)).filter((f)=>null !== f);
79
+ await addFiles(resolvedFiles);
80
+ }
18
81
  if (onDrop) onDrop(event);
19
82
  };
20
- const handleFileSelect = (selected)=>{
83
+ const handleFileSelect = async (selected)=>{
21
84
  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);
85
+ const selectedFiles = Array.from(selected);
86
+ if (shouldManageFiles) await addFiles(selectedFiles);
87
+ };
88
+ const handleRemoveFile = (id)=>{
89
+ const removedFile = files.find((file)=>file.id === id);
90
+ const updatedFiles = files.filter((file)=>file.id !== id);
91
+ setFiles(updatedFiles);
92
+ if (removedFile) onFilesRemoved?.([
93
+ removedFile
94
+ ]);
95
+ };
96
+ const containerProps = {
97
+ width,
98
+ margin,
99
+ marginTop,
100
+ marginRight,
101
+ marginBottom,
102
+ marginLeft
26
103
  };
27
- return /*#__PURE__*/ jsxs(StyledDropZone, {
28
- isDisabled: disabled,
29
- invalid: invalid,
30
- onDrop: showFileTrigger ? handleFileDrop : onDrop,
31
- ...rest,
104
+ const showManagedFileList = showFileList && files.length > 0;
105
+ const showFileInfo = !!supportedFormats || !!maxFileSize;
106
+ return /*#__PURE__*/ jsxs(Container, {
107
+ ...containerProps,
32
108
  children: [
33
- /*#__PURE__*/ jsx(StyledText, {
34
- slot: "label",
35
- children: label
109
+ /*#__PURE__*/ jsxs(StyledDropZone, {
110
+ isDisabled: disabled,
111
+ invalid: invalid,
112
+ onDrop: handleFileDrop,
113
+ ...rest,
114
+ children: [
115
+ /*#__PURE__*/ jsxs(TriggerContainer, {
116
+ children: [
117
+ showFileTrigger && /*#__PURE__*/ jsx(FileTrigger, {
118
+ acceptedFileTypes: acceptedFileTypes,
119
+ allowsMultiple: allowsMultiple,
120
+ defaultCamera: defaultCamera,
121
+ acceptDirectory: acceptDirectory,
122
+ onSelect: handleFileSelect
123
+ }),
124
+ /*#__PURE__*/ jsxs(StyledText, {
125
+ slot: "label",
126
+ children: [
127
+ showFileTrigger ? 'or ' : '',
128
+ " ",
129
+ showFileTrigger ? label.toLowerCase() : label
130
+ ]
131
+ })
132
+ ]
133
+ }),
134
+ (errorMessage || sizeValidationError) && /*#__PURE__*/ jsx(ErrorMessageText, {
135
+ children: errorMessage || sizeValidationError
136
+ })
137
+ ]
36
138
  }),
37
- showFileTrigger && /*#__PURE__*/ jsx(SeparatorText, {
38
- children: "or"
139
+ showFileInfo && /*#__PURE__*/ jsxs(FileInfo, {
140
+ children: [
141
+ /*#__PURE__*/ jsx(FileInfoLeft, {
142
+ children: supportedFormats && /*#__PURE__*/ jsxs("div", {
143
+ children: [
144
+ "Supported formats: ",
145
+ supportedFormats.join(', ')
146
+ ]
147
+ })
148
+ }),
149
+ /*#__PURE__*/ jsx(FileInfoRight, {
150
+ children: maxFileSize && /*#__PURE__*/ jsxs("div", {
151
+ children: [
152
+ "Maximum file size: ",
153
+ maxFileSize
154
+ ]
155
+ })
156
+ })
157
+ ]
39
158
  }),
40
- showFileTrigger && /*#__PURE__*/ jsx(FileTrigger, {
41
- acceptedFileTypes: acceptedFileTypes,
42
- allowsMultiple: allowsMultiple,
43
- defaultCamera: defaultCamera,
44
- acceptDirectory: acceptDirectory,
45
- onSelect: handleFileSelect
46
- }),
47
- displayFiles && showFileNames && /*#__PURE__*/ jsx(FilesContainer, {
48
- title: displayFiles,
49
- children: /*#__PURE__*/ jsx(Ellipsis, {
50
- children: displayFiles
51
- })
52
- }),
53
- errorMessage && /*#__PURE__*/ jsx(ErrorMessageText, {
54
- children: errorMessage
159
+ showManagedFileList && /*#__PURE__*/ jsx(FileList, {
160
+ children: files.map((file)=>/*#__PURE__*/ jsxs(FileItem, {
161
+ children: [
162
+ /*#__PURE__*/ jsx(FileName, {
163
+ title: file.name,
164
+ children: file.name
165
+ }),
166
+ !disabled && /*#__PURE__*/ jsx(Button, {
167
+ icon: "close",
168
+ size: "small",
169
+ subtle: true,
170
+ secondary: true,
171
+ onClick: ()=>handleRemoveFile(file.id)
172
+ })
173
+ ]
174
+ }, file.id))
55
175
  })
56
176
  ]
57
177
  });
58
178
  };
179
+ const Container = styled.div({
180
+ display: 'flex',
181
+ flexDirection: 'column',
182
+ gap: 4
183
+ }, widthHeightProps, marginProps);
59
184
  const StyledDropZone = styled(DropZone)(({ theme, invalid })=>({
60
185
  display: 'flex',
61
186
  flexDirection: 'column',
@@ -88,27 +213,55 @@ const StyledDropZone = styled(DropZone)(({ theme, invalid })=>({
88
213
  }
89
214
  }
90
215
  }), widthHeightProps, marginProps, paddingProps);
216
+ const TriggerContainer = styled.div({
217
+ display: 'flex',
218
+ alignItems: 'center',
219
+ justifyContent: 'center',
220
+ gap: 8
221
+ });
91
222
  const StyledText = styled(Text)(({ theme })=>({
92
223
  fontSize: theme.font.size.md,
93
224
  fontWeight: theme.font.weight.medium,
94
225
  lineHeight: '14px',
95
226
  textAlign: 'center'
96
227
  }));
97
- const SeparatorText = styled(Text)(({ theme })=>({
98
- fontSize: theme.font.size.md,
99
- color: theme.scale6,
100
- lineHeight: '14px'
228
+ const ErrorMessageText = styled(StyledText)(({ theme })=>({
229
+ color: theme.color.error
101
230
  }));
102
- const FilesContainer = styled.div(({ theme })=>({
231
+ const FileList = styled.div(({ theme })=>({
232
+ display: 'flex',
233
+ flexDirection: 'column',
234
+ gap: theme.size.xs
235
+ }));
236
+ const FileItem = styled.div(({ theme })=>({
103
237
  display: 'flex',
104
238
  alignItems: 'center',
105
- justifyContent: 'center',
239
+ justifyContent: 'space-between',
240
+ gap: theme.size.xs,
241
+ padding: '4px 8px',
242
+ paddingRight: 4,
243
+ backgroundColor: theme.scale0,
244
+ borderRadius: 4
245
+ }));
246
+ const FileName = styled.div(({ theme })=>({
106
247
  fontSize: theme.font.size.sm,
107
- color: theme.scale6,
108
- width: '100%'
248
+ lineHeight: '14px',
249
+ flex: 1,
250
+ overflow: 'hidden',
251
+ textOverflow: 'ellipsis',
252
+ whiteSpace: 'nowrap'
109
253
  }));
110
- const ErrorMessageText = styled(StyledText)(({ theme })=>({
111
- color: theme.color.error
254
+ const FileInfo = styled.div(({ theme })=>({
255
+ display: 'flex',
256
+ alignItems: 'center',
257
+ justifyContent: 'space-between',
258
+ gap: 16,
259
+ fontSize: theme.font.size.sm,
260
+ color: theme.scale6
112
261
  }));
262
+ const FileInfoLeft = styled.div({});
263
+ const FileInfoRight = styled.div({
264
+ textAlign: 'right'
265
+ });
113
266
  const components_DropZone = DropZone_DropZone;
114
267
  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.1",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {