@ttoss/components 2.4.3 → 2.4.5

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/README.md CHANGED
@@ -51,17 +51,60 @@ import { Drawer } from '@ttoss/components/Drawer';
51
51
 
52
52
  ### FileUploader
53
53
 
54
- Drag-and-drop file upload with progress tracking. [📖 Docs](https://storybook.ttoss.dev/?path=/docs/components-fileuploader--docs)
54
+ Controlled file uploader with drag-and-drop support. Displays uploaded files with previews, clickable links, and remove functionality. [📖 Docs](https://storybook.ttoss.dev/?path=/docs/components-fileuploader--docs)
55
55
 
56
56
  ```tsx
57
57
  import { FileUploader } from '@ttoss/components/FileUploader';
58
+ import { useState } from 'react';
59
+
60
+ const [files, setFiles] = useState([
61
+ {
62
+ id: 'file-1',
63
+ name: 'document.pdf',
64
+ url: 'https://example.com/files/document.pdf',
65
+ },
66
+ {
67
+ id: 'file-2',
68
+ name: 'image.jpg',
69
+ imageUrl: 'https://example.com/images/thumb.jpg', // Optional preview
70
+ url: 'https://example.com/files/image.jpg',
71
+ },
72
+ ]);
58
73
 
59
74
  <FileUploader
60
- onUpload={async (file) => ({ url: 'file-url', id: 'file-id' })}
61
- onUploadComplete={(file, result) => console.log('Uploaded:', result)}
75
+ // Required: Upload handler
76
+ onUpload={async (file, onProgress) => {
77
+ // Your upload logic here
78
+ onProgress?.(50); // Report progress
79
+ const result = await uploadToServer(file);
80
+ return { url: result.url, id: result.id, name: result.name };
81
+ }}
82
+ // Controlled files list
83
+ files={files}
84
+ // Callbacks
85
+ onUploadComplete={(file, result) => {
86
+ setFiles([...files, { id: result.id, name: file.name, url: result.url }]);
87
+ }}
88
+ onRemove={(file, index) => {
89
+ setFiles(files.filter((_, i) => i !== index));
90
+ }}
91
+ // Optional: Validation
92
+ accept="image/*,.pdf"
93
+ maxSize={10 * 1024 * 1024} // 10MB
94
+ maxFiles={5}
62
95
  />;
63
96
  ```
64
97
 
98
+ **Key Features:**
99
+
100
+ - **Controlled component**: Pass `files` prop to display uploaded files
101
+ - **Clickable file names**: Names are links that open the file URL
102
+ - **Image previews**: Show thumbnails when `imageUrl` is provided
103
+ - **Remove functionality**: Each file has a remove button
104
+ - **Upload callbacks**: `onUploadStart`, `onUploadProgress`, `onUploadComplete`, `onUploadError`
105
+ - **Validation**: File type, size, and quantity limits
106
+ - **Drag-and-drop**: Native drag-and-drop support
107
+
65
108
  ### InstallPwa
66
109
 
67
110
  PWA installation prompt component. [📖 Docs](https://storybook.ttoss.dev/?path=/docs/components-installpwa--docs)
@@ -3,7 +3,7 @@ import * as React from 'react';
3
3
 
4
4
  type UploadResult = {
5
5
  url: string;
6
- id: string;
6
+ id: string | number;
7
7
  };
8
8
  type FileUploadState = {
9
9
  file: File;
@@ -18,7 +18,13 @@ type OnUploadProgress = (file: File, progress: number) => void;
18
18
  type OnUploadComplete = (file: File, result: UploadResult) => void;
19
19
  type OnUploadError = (file: File, error: Error) => void;
20
20
  type OnFilesChange = (files: FileUploadState[]) => void;
21
- type OnRemoveFile = (file: FileUploadState, index: number) => void;
21
+ type OnRemove = (file: UploadedFile, index: number) => void;
22
+ type UploadedFile = {
23
+ id: string | number;
24
+ name: string;
25
+ imageUrl?: string;
26
+ url: string;
27
+ };
22
28
  type FileUploaderProps = {
23
29
  onUpload: OnUpload;
24
30
  onUploadStart?: OnUploadStart;
@@ -26,7 +32,7 @@ type FileUploaderProps = {
26
32
  onUploadComplete?: OnUploadComplete;
27
33
  onUploadError?: OnUploadError;
28
34
  onFilesChange?: OnFilesChange;
29
- onRemoveFile?: OnRemoveFile;
35
+ onRemove?: OnRemove;
30
36
  accept?: string;
31
37
  multiple?: boolean;
32
38
  maxSize?: number;
@@ -39,10 +45,11 @@ type FileUploaderProps = {
39
45
  children?: React.ReactNode;
40
46
  showFileList?: boolean;
41
47
  FileListComponent?: (props: {
42
- files: FileUploadState[];
43
- onRemoveFile: (index: number) => void;
48
+ files: UploadedFile[];
49
+ onRemove: (index: number) => void;
44
50
  }) => React.ReactNode;
51
+ files?: UploadedFile[];
45
52
  };
46
- declare const FileUploader: ({ onUpload, onUploadStart, onUploadProgress, onUploadComplete, onUploadError, onFilesChange, onRemoveFile, accept, multiple, maxSize, maxFiles, disabled, autoUpload, retryAttempts, placeholder, error, children, showFileList, FileListComponent, }: FileUploaderProps) => react_jsx_runtime.JSX.Element;
53
+ declare const FileUploader: ({ onUpload, onUploadStart, onUploadProgress, onUploadComplete, onUploadError, onFilesChange, onRemove, accept, multiple, maxSize, maxFiles, disabled, autoUpload, retryAttempts, placeholder, error, children, showFileList, FileListComponent, files: uploadedFiles, }: FileUploaderProps) => react_jsx_runtime.JSX.Element;
47
54
 
48
- export { type FileUploadState, FileUploader, type FileUploaderProps, type OnFilesChange, type OnRemoveFile, type OnUpload, type OnUploadComplete, type OnUploadError, type OnUploadProgress, type OnUploadStart, type UploadResult };
55
+ export { type FileUploadState, FileUploader, type FileUploaderProps, type OnFilesChange, type OnRemove, type OnUpload, type OnUploadComplete, type OnUploadError, type OnUploadProgress, type OnUploadStart, type UploadResult, type UploadedFile };
@@ -12,7 +12,7 @@ var FileUploader = /* @__PURE__ */__name(({
12
12
  onUploadComplete,
13
13
  onUploadError,
14
14
  onFilesChange,
15
- onRemoveFile,
15
+ onRemove,
16
16
  accept,
17
17
  multiple = true,
18
18
  maxSize = 10 * 1024 * 1024,
@@ -24,7 +24,8 @@ var FileUploader = /* @__PURE__ */__name(({
24
24
  error,
25
25
  children,
26
26
  showFileList = true,
27
- FileListComponent
27
+ FileListComponent,
28
+ files: uploadedFiles = []
28
29
  }) => {
29
30
  const {
30
31
  intl
@@ -43,6 +44,9 @@ var FileUploader = /* @__PURE__ */__name(({
43
44
  const fileArray = Array.from(newFiles);
44
45
  const validFiles = [];
45
46
  const currentFileCount = files.length;
47
+ if (currentFileCount >= maxFiles) {
48
+ return [];
49
+ }
46
50
  for (const file of fileArray) {
47
51
  if (maxSize && file.size > maxSize) {
48
52
  continue;
@@ -127,7 +131,7 @@ var FileUploader = /* @__PURE__ */__name(({
127
131
  const handleDrop = /* @__PURE__ */__name(event => {
128
132
  event.preventDefault();
129
133
  setIsDragOver(false);
130
- if (disabled) return;
134
+ if (disabled || files.length >= maxFiles) return;
131
135
  const droppedFiles = event.dataTransfer.files;
132
136
  if (droppedFiles) {
133
137
  const validFiles = validateFiles(droppedFiles);
@@ -136,7 +140,7 @@ var FileUploader = /* @__PURE__ */__name(({
136
140
  }, "handleDrop");
137
141
  const handleDragOver = /* @__PURE__ */__name(event => {
138
142
  event.preventDefault();
139
- if (!disabled) {
143
+ if (!disabled && files.length < maxFiles) {
140
144
  setIsDragOver(true);
141
145
  }
142
146
  }, "handleDragOver");
@@ -145,48 +149,38 @@ var FileUploader = /* @__PURE__ */__name(({
145
149
  setIsDragOver(false);
146
150
  }, "handleDragLeave");
147
151
  const handleClick = /* @__PURE__ */__name(() => {
148
- if (!disabled && fileInputRef.current) {
152
+ if (!disabled && files.length < maxFiles && fileInputRef.current) {
149
153
  fileInputRef.current.click();
150
154
  }
151
155
  }, "handleClick");
152
156
  const handleRemoveFile = React.useCallback(index => {
153
- const fileToRemove = files[index];
154
- setFiles(prevFiles => {
155
- const newFiles = prevFiles.filter((_, i) => {
156
- return i !== index;
157
- });
158
- onFilesChange?.(newFiles);
159
- return newFiles;
160
- });
161
- onRemoveFile?.(fileToRemove, index);
162
- }, [files, onFilesChange, onRemoveFile]);
163
- const retryUpload = React.useCallback(index => {
164
- const fileState = files[index];
165
- if (fileState.status === "error") {
166
- uploadFile(fileState);
167
- }
168
- }, [files, uploadFile]);
157
+ const fileToRemove = uploadedFiles[index];
158
+ onRemove?.(fileToRemove, index);
159
+ }, [uploadedFiles, onRemove]);
169
160
  const isUploading = files.some(f => {
170
161
  return f.status === "uploading";
171
162
  });
163
+ const isMaxFilesReached = files.length >= maxFiles;
172
164
  const fileListNode = React.useMemo(() => {
173
- if (!showFileList || files.length === 0) {
165
+ if (!showFileList || files.length === 0 && uploadedFiles.length === 0) {
174
166
  return null;
175
167
  }
176
168
  if (FileListComponent) {
177
169
  return /* @__PURE__ */React.createElement(FileListComponent, {
178
- files,
179
- onRemoveFile: handleRemoveFile
170
+ files: uploadedFiles,
171
+ onRemove: handleRemoveFile
180
172
  });
181
173
  }
182
174
  return /* @__PURE__ */React.createElement(Stack, {
183
175
  sx: {
184
- gap: 1
176
+ gap: 1,
177
+ width: "100%"
185
178
  }
186
- }, files.map((fileState, index) => {
179
+ }, uploadedFiles.map((file, index) => {
187
180
  return /* @__PURE__ */React.createElement(Flex, {
188
- key: index,
181
+ key: file.id,
189
182
  sx: {
183
+ width: "full",
190
184
  alignItems: "center",
191
185
  justifyContent: "space-between",
192
186
  p: 2,
@@ -200,47 +194,42 @@ var FileUploader = /* @__PURE__ */__name(({
200
194
  gap: 2,
201
195
  flex: 1
202
196
  }
203
- }, /* @__PURE__ */React.createElement(Text, {
197
+ }, file.imageUrl && /* @__PURE__ */React.createElement(Box, {
204
198
  sx: {
205
- fontSize: "lg"
199
+ width: 32,
200
+ height: 32
201
+ }
202
+ }, /* @__PURE__ */React.createElement("img", {
203
+ src: file.imageUrl,
204
+ alt: file.name,
205
+ style: {
206
+ width: "100%",
207
+ height: "100%",
208
+ objectFit: "cover",
209
+ borderRadius: 4
206
210
  }
207
- }, fileState.status === "completed" ? "\u2713" : fileState.status === "error" ? "\u2717" : fileState.status === "uploading" ? "\u21BB" : "\u{1F4C4}"), /* @__PURE__ */React.createElement(Box, {
211
+ })), /* @__PURE__ */React.createElement(Box, {
208
212
  sx: {
209
213
  flex: 1
210
214
  }
215
+ }, /* @__PURE__ */React.createElement("a", {
216
+ href: file.url,
217
+ target: "_blank",
218
+ rel: "noopener noreferrer",
219
+ style: {
220
+ textDecoration: "none",
221
+ color: "inherit"
222
+ }
211
223
  }, /* @__PURE__ */React.createElement(Text, {
212
224
  variant: "body",
213
225
  sx: {
214
- fontWeight: "medium"
215
- }
216
- }, fileState.file.name), fileState.status === "uploading" && fileState.progress && /* @__PURE__ */React.createElement(Text, {
217
- variant: "caption",
218
- sx: {
219
- color: "primary.default"
220
- }
221
- }, fileState.progress.toFixed(0), "%"), fileState.status === "error" && /* @__PURE__ */React.createElement(Text, {
222
- variant: "caption",
223
- sx: {
224
- color: "error.default"
225
- }
226
- }, "Failed")), /* @__PURE__ */React.createElement(Text, {
227
- variant: "caption",
228
- sx: {
229
- color: "text.muted"
230
- }
231
- }, formatFileSize(fileState.file.size))), /* @__PURE__ */React.createElement(Flex, {
232
- sx: {
233
- gap: 1
234
- }
235
- }, fileState.status === "error" && /* @__PURE__ */React.createElement(Button, {
236
- variant: "destructive",
237
- onClick: /* @__PURE__ */__name(() => {
238
- return retryUpload(index);
239
- }, "onClick"),
240
- sx: {
241
- fontSize: "xs"
226
+ fontWeight: "medium",
227
+ "&:hover": {
228
+ textDecoration: "underline",
229
+ color: "primary.default"
230
+ }
242
231
  }
243
- }, "Retry"), /* @__PURE__ */React.createElement(Button, {
232
+ }, file.name)))), /* @__PURE__ */React.createElement(Button, {
244
233
  variant: "destructive",
245
234
  onClick: /* @__PURE__ */__name(() => {
246
235
  return handleRemoveFile(index);
@@ -252,12 +241,88 @@ var FileUploader = /* @__PURE__ */__name(({
252
241
  color: "error.default"
253
242
  }
254
243
  }
255
- }, "Remove")));
244
+ }, intl.formatMessage({
245
+ id: "G/yZLu",
246
+ defaultMessage: [{
247
+ "type": 0,
248
+ "value": "Remove"
249
+ }]
250
+ })));
256
251
  }));
257
- }, [FileListComponent, files, handleRemoveFile, retryUpload, showFileList]);
252
+ }, [files.length, uploadedFiles, handleRemoveFile, intl, showFileList, FileListComponent]);
253
+ const placeholderTexts = React.useMemo(() => {
254
+ const texts = [];
255
+ if (isUploading) {
256
+ texts.push(intl.formatMessage({
257
+ id: "JEsxDw",
258
+ defaultMessage: [{
259
+ "type": 0,
260
+ "value": "Uploading..."
261
+ }]
262
+ }));
263
+ } else if (isMaxFilesReached) {
264
+ texts.push(intl.formatMessage({
265
+ id: "eRShvB",
266
+ defaultMessage: [{
267
+ "type": 0,
268
+ "value": "Maximum files reached"
269
+ }]
270
+ }));
271
+ } else {
272
+ texts.push(placeholder || intl.formatMessage({
273
+ id: "gy0Ynb",
274
+ defaultMessage: [{
275
+ "type": 0,
276
+ "value": "Click or drag files here"
277
+ }]
278
+ }));
279
+ }
280
+ if (!isUploading && !isMaxFilesReached) {
281
+ if (accept) texts.push(accept);
282
+ if (maxSize) texts.push(`Max ${formatFileSize(maxSize)}`);
283
+ if (multiple && maxFiles) texts.push(intl.formatMessage({
284
+ id: "fOOwej",
285
+ defaultMessage: [{
286
+ "type": 6,
287
+ "value": "max_files",
288
+ "options": {
289
+ "one": {
290
+ "value": [{
291
+ "type": 0,
292
+ "value": "Up to "
293
+ }, {
294
+ "type": 7
295
+ }, {
296
+ "type": 0,
297
+ "value": " file"
298
+ }]
299
+ },
300
+ "other": {
301
+ "value": [{
302
+ "type": 0,
303
+ "value": "Up to "
304
+ }, {
305
+ "type": 7
306
+ }, {
307
+ "type": 0,
308
+ "value": " files"
309
+ }]
310
+ }
311
+ },
312
+ "offset": 0,
313
+ "pluralType": "cardinal"
314
+ }]
315
+ }, {
316
+ max_files: maxFiles
317
+ }));
318
+ }
319
+ return texts.filter(Boolean).join(" \u2022 ");
320
+ }, [isUploading, isMaxFilesReached, intl, placeholder, accept, maxSize, multiple, maxFiles]);
258
321
  return /* @__PURE__ */React.createElement(Stack, {
259
322
  sx: {
260
- gap: 3
323
+ gap: 3,
324
+ justifyContent: "stretch",
325
+ width: "100%"
261
326
  }
262
327
  }, /* @__PURE__ */React.createElement(Box, {
263
328
  onDrop: handleDrop,
@@ -265,18 +330,19 @@ var FileUploader = /* @__PURE__ */__name(({
265
330
  onDragLeave: handleDragLeave,
266
331
  onClick: handleClick,
267
332
  sx: {
333
+ width: "100%",
268
334
  border: "2px dashed",
269
335
  borderColor: error ? "error.default" : isDragOver ? "primary.default" : "display.border.muted.default",
270
336
  borderRadius: "xl",
271
337
  padding: 6,
272
338
  textAlign: "center",
273
- cursor: disabled || isUploading ? "not-allowed" : "pointer",
339
+ cursor: disabled || isUploading || isMaxFilesReached ? "not-allowed" : "pointer",
274
340
  backgroundColor: isDragOver ? "primary.muted" : "display.background.secondary.default",
275
341
  transition: "all 0.2s ease",
276
- opacity: disabled ? 0.6 : 1,
342
+ opacity: disabled || isMaxFilesReached ? 0.6 : 1,
277
343
  "&:hover": {
278
- borderColor: !disabled && !isUploading && !error ? "primary.default" : void 0,
279
- backgroundColor: !disabled && !isUploading ? "primary.muted" : void 0
344
+ borderColor: !disabled && !isUploading && !error && !isMaxFilesReached ? "primary.default" : void 0,
345
+ backgroundColor: !disabled && !isUploading && !isMaxFilesReached ? "primary.muted" : void 0
280
346
  }
281
347
  }
282
348
  }, /* @__PURE__ */React.createElement("input", {
@@ -285,7 +351,7 @@ var FileUploader = /* @__PURE__ */__name(({
285
351
  accept,
286
352
  multiple,
287
353
  onChange: handleFileChange,
288
- disabled: disabled || isUploading,
354
+ disabled: disabled || isUploading || isMaxFilesReached,
289
355
  style: {
290
356
  display: "none"
291
357
  }
@@ -298,9 +364,15 @@ var FileUploader = /* @__PURE__ */__name(({
298
364
  }
299
365
  }, /* @__PURE__ */React.createElement(Text, {
300
366
  sx: {
301
- fontSize: "3xl"
367
+ fontSize: "2xl"
302
368
  }
303
- }, "\u{1F4C1}"), /* @__PURE__ */React.createElement(Box, {
369
+ }, intl.formatMessage({
370
+ id: "XKyo5X",
371
+ defaultMessage: [{
372
+ "type": 0,
373
+ "value": "File Upload"
374
+ }]
375
+ })), /* @__PURE__ */React.createElement(Box, {
304
376
  sx: {
305
377
  textAlign: "center"
306
378
  }
@@ -310,24 +382,7 @@ var FileUploader = /* @__PURE__ */__name(({
310
382
  color: "text.default",
311
383
  mb: 1
312
384
  }
313
- }, isUploading ? intl.formatMessage({
314
- id: "JEsxDw",
315
- defaultMessage: [{
316
- "type": 0,
317
- "value": "Uploading..."
318
- }]
319
- }) : placeholder || intl.formatMessage({
320
- id: "gy0Ynb",
321
- defaultMessage: [{
322
- "type": 0,
323
- "value": "Click or drag files here"
324
- }]
325
- })), /* @__PURE__ */React.createElement(Text, {
326
- variant: "caption",
327
- sx: {
328
- color: "text.muted"
329
- }
330
- }, [accept && accept, maxSize && `Max ${formatFileSize(maxSize)}`, multiple && maxFiles && `Up to ${maxFiles} files`].filter(Boolean).join(" \u2022 "))), !isUploading && /* @__PURE__ */React.createElement(Button, {
385
+ }, placeholderTexts)), !isUploading && !isMaxFilesReached && /* @__PURE__ */React.createElement(Button, {
331
386
  variant: "secondary",
332
387
  disabled
333
388
  }, intl.formatMessage({
@@ -1,5 +1,4 @@
1
1
  /** Powered by @ttoss/config. https://ttoss.dev/docs/modules/packages/config/ */
2
- import * as React from 'react';
3
2
  import { __name } from "./chunk-V4MHYKRI.js";
4
3
 
5
4
  // ../../node_modules/.pnpm/@iconify-icon+react@2.3.0_react@19.1.1/node_modules/@iconify-icon/react/dist/iconify.mjs
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ttoss/components",
3
- "version": "2.4.3",
3
+ "version": "2.4.5",
4
4
  "description": "React components for ttoss ecosystem.",
5
5
  "license": "MIT",
6
6
  "author": "ttoss",
@@ -101,9 +101,9 @@
101
101
  },
102
102
  "peerDependencies": {
103
103
  "react": ">=16.8.0",
104
- "@ttoss/react-i18n": "^2.0.17",
105
- "@ttoss/ui": "^5.10.1",
106
- "@ttoss/react-hooks": "^2.1.3"
104
+ "@ttoss/react-i18n": "^2.0.18",
105
+ "@ttoss/react-hooks": "^2.1.4",
106
+ "@ttoss/ui": "^5.10.2"
107
107
  },
108
108
  "devDependencies": {
109
109
  "@types/jest": "^30.0.0",
@@ -112,13 +112,13 @@
112
112
  "react": "^19.1.0",
113
113
  "tsup": "^8.5.0",
114
114
  "tsx": "^4.19.2",
115
- "@ttoss/config": "^1.35.7",
116
- "@ttoss/react-i18n": "^2.0.17",
117
- "@ttoss/i18n-cli": "^0.7.33",
118
- "@ttoss/react-icons": "^0.4.16",
119
- "@ttoss/test-utils": "^2.1.27",
120
- "@ttoss/ui": "^5.10.1",
121
- "@ttoss/react-hooks": "^2.1.3"
115
+ "@ttoss/i18n-cli": "^0.7.34",
116
+ "@ttoss/config": "^1.35.8",
117
+ "@ttoss/react-i18n": "^2.0.18",
118
+ "@ttoss/ui": "^5.10.2",
119
+ "@ttoss/react-icons": "^0.4.17",
120
+ "@ttoss/test-utils": "^2.1.28",
121
+ "@ttoss/react-hooks": "^2.1.4"
122
122
  },
123
123
  "keywords": [
124
124
  "React",