@boxcustodia/library 2.0.0-alpha.12 → 2.0.0-alpha.14

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 (174) hide show
  1. package/dist/index.cjs.js +1 -138
  2. package/dist/index.d.ts +1087 -720
  3. package/dist/index.es.js +7011 -56097
  4. package/dist/theme.css +1 -1
  5. package/package.json +34 -26
  6. package/src/__doc__/Examples.tsx +1 -1
  7. package/src/__doc__/Intro.mdx +3 -3
  8. package/src/__doc__/Tabs.mdx +112 -0
  9. package/src/__doc__/V2.mdx +1246 -0
  10. package/src/components/accordion/accordion.stories.tsx +143 -0
  11. package/src/components/accordion/accordion.tsx +135 -0
  12. package/src/components/accordion/index.ts +1 -0
  13. package/src/components/alert/alert.stories.tsx +24 -4
  14. package/src/components/alert/alert.tsx +17 -9
  15. package/src/components/alert-dialog/alert-dialog.stories.tsx +24 -0
  16. package/src/components/alert-dialog/alert-dialog.test.tsx +1 -1
  17. package/src/components/alert-dialog/alert-dialog.tsx +58 -10
  18. package/src/components/auto-complete/auto-complete.stories.tsx +616 -200
  19. package/src/components/auto-complete/auto-complete.tsx +420 -68
  20. package/src/components/auto-complete/index.ts +0 -1
  21. package/src/components/avatar/avatar.stories.tsx +162 -21
  22. package/src/components/avatar/avatar.tsx +79 -20
  23. package/src/components/button/button.stories.tsx +219 -294
  24. package/src/components/button/button.test.tsx +10 -17
  25. package/src/components/button/button.tsx +78 -19
  26. package/src/components/button/components/base-button.tsx +30 -53
  27. package/src/components/button/index.ts +0 -1
  28. package/src/components/calendar/calendar.stories.tsx +1 -1
  29. package/src/components/calendar/calendar.tsx +4 -4
  30. package/src/components/card/card.stories.tsx +141 -69
  31. package/src/components/card/card.tsx +155 -54
  32. package/src/components/center/center.stories.tsx +22 -39
  33. package/src/components/checkbox/checkbox.stories.tsx +25 -5
  34. package/src/components/checkbox/checkbox.tsx +76 -15
  35. package/src/components/checkbox-group/checkbox-group.stories.tsx +116 -28
  36. package/src/components/checkbox-group/checkbox-group.tsx +84 -3
  37. package/src/components/combobox/combobox.stories.tsx +33 -23
  38. package/src/components/combobox/combobox.tsx +99 -77
  39. package/src/components/date-picker/date-input.stories.tsx +14 -6
  40. package/src/components/date-picker/date-input.tsx +2 -2
  41. package/src/components/date-picker/date-picker.model.ts +13 -4
  42. package/src/components/date-picker/date-picker.stories.tsx +38 -12
  43. package/src/components/date-picker/date-picker.tsx +28 -14
  44. package/src/components/dialog/dialog.stories.tsx +18 -0
  45. package/src/components/dialog/dialog.test.tsx +1 -1
  46. package/src/components/dialog/dialog.tsx +51 -20
  47. package/src/components/divider/divider.stories.tsx +126 -51
  48. package/src/components/divider/divider.tsx +16 -16
  49. package/src/components/dropzone/dropzone.stories.tsx +71 -90
  50. package/src/components/dropzone/dropzone.tsx +383 -105
  51. package/src/components/dropzone/index.ts +0 -1
  52. package/src/components/empty/empty.stories.tsx +165 -0
  53. package/src/components/empty/empty.tsx +156 -0
  54. package/src/components/empty/index.ts +1 -0
  55. package/src/components/field/field.stories.tsx +227 -4
  56. package/src/components/field/field.tsx +77 -42
  57. package/src/components/form/form.stories.tsx +320 -197
  58. package/src/components/form/form.tsx +3 -23
  59. package/src/components/index.ts +2 -6
  60. package/src/components/input/input.stories.tsx +5 -5
  61. package/src/components/input/input.tsx +4 -4
  62. package/src/components/kbd/kbd.stories.tsx +1 -0
  63. package/src/components/label/label.stories.tsx +16 -0
  64. package/src/components/label/label.tsx +13 -2
  65. package/src/components/loader/loader.stories.tsx +7 -5
  66. package/src/components/loader/loader.tsx +8 -3
  67. package/src/components/menu/menu-primitives.tsx +207 -196
  68. package/src/components/menu/menu.stories.tsx +276 -146
  69. package/src/components/menu/menu.tsx +146 -54
  70. package/src/components/number-input/number-input.stories.tsx +27 -4
  71. package/src/components/number-input/number-input.test.tsx +2 -2
  72. package/src/components/number-input/number-input.tsx +31 -33
  73. package/src/components/otp/index.ts +1 -0
  74. package/src/components/otp/otp.stories.tsx +209 -0
  75. package/src/components/otp/otp.tsx +100 -0
  76. package/src/components/pagination/index.ts +1 -0
  77. package/src/components/pagination/pagination.model.ts +2 -0
  78. package/src/components/pagination/pagination.stories.tsx +154 -59
  79. package/src/components/pagination/pagination.test.tsx +122 -57
  80. package/src/components/pagination/pagination.tsx +575 -77
  81. package/src/components/password/password.stories.tsx +18 -3
  82. package/src/components/password/password.tsx +29 -9
  83. package/src/components/popover/popover.stories.tsx +26 -5
  84. package/src/components/popover/popover.tsx +15 -23
  85. package/src/components/progress/progress.stories.tsx +1 -0
  86. package/src/components/radio-group/index.ts +1 -0
  87. package/src/components/radio-group/radio-group.stories.tsx +251 -0
  88. package/src/components/radio-group/radio-group.tsx +212 -0
  89. package/src/components/scroll-area/scroll-area.stories.tsx +1 -0
  90. package/src/components/select/select.stories.tsx +118 -19
  91. package/src/components/select/select.tsx +67 -62
  92. package/src/components/skeleton/skeleton.stories.tsx +1 -0
  93. package/src/components/stack/stack.stories.tsx +179 -89
  94. package/src/components/stack/stack.tsx +2 -2
  95. package/src/components/stepper/index.ts +1 -1
  96. package/src/components/stepper/stepper.stories.tsx +767 -83
  97. package/src/components/stepper/stepper.test.tsx +18 -18
  98. package/src/components/stepper/stepper.tsx +554 -0
  99. package/src/components/switch/switch.stories.tsx +15 -1
  100. package/src/components/switch/switch.tsx +17 -4
  101. package/src/components/table/index.ts +0 -2
  102. package/src/components/table/table.stories.tsx +131 -18
  103. package/src/components/table/table.test.tsx +1 -1
  104. package/src/components/table/table.tsx +183 -77
  105. package/src/components/tabs/tabs.stories.tsx +373 -155
  106. package/src/components/tabs/tabs.test.tsx +12 -12
  107. package/src/components/tabs/tabs.tsx +72 -149
  108. package/src/components/tag/index.ts +0 -1
  109. package/src/components/tag/tag.stories.tsx +155 -120
  110. package/src/components/tag/tag.tsx +47 -95
  111. package/src/components/textarea/textarea.stories.tsx +8 -22
  112. package/src/components/textarea/textarea.tsx +17 -79
  113. package/src/components/timeline/timeline.stories.tsx +323 -42
  114. package/src/components/timeline/timeline.tsx +359 -132
  115. package/src/components/toast/toast.stories.tsx +1 -0
  116. package/src/components/tooltip/tooltip.tsx +11 -9
  117. package/src/components/tree/index.ts +0 -1
  118. package/src/components/tree/tree.stories.tsx +365 -408
  119. package/src/components/tree/tree.test.tsx +163 -0
  120. package/src/components/tree/tree.tsx +212 -36
  121. package/src/hooks/useAsync/__doc__/useAsync.stories.tsx +5 -5
  122. package/src/hooks/useClipboard/__doc__/useClipboard.stories.tsx +1 -3
  123. package/src/hooks/useDebounceCallback/__doc__/useDebouncedCallback.stories.tsx +6 -6
  124. package/src/hooks/useDocumentTitle/__doc__/useDocumentTitle.stories.tsx +1 -1
  125. package/src/hooks/useEventListener/__test__/useEventListener.test.tsx +1 -1
  126. package/src/hooks/useLocalStorage/__doc__/useLocalStorage.stories.tsx +1 -1
  127. package/src/hooks/usePagination/usePagination.tsx +36 -24
  128. package/src/styles/theme.css +1 -1
  129. package/src/utils/form.tsx +67 -37
  130. package/src/utils/index.ts +1 -1
  131. package/src/__doc__/Migration.mdx +0 -475
  132. package/src/components/auto-complete/auto-complete-primitives.tsx +0 -155
  133. package/src/components/background-image/background-image.stories.tsx +0 -21
  134. package/src/components/background-image/background-image.test.tsx +0 -29
  135. package/src/components/background-image/background-image.tsx +0 -23
  136. package/src/components/background-image/index.ts +0 -1
  137. package/src/components/button/button.variants.ts +0 -44
  138. package/src/components/button/components/loader-overlay.tsx +0 -21
  139. package/src/components/button/components/loading-icon.tsx +0 -47
  140. package/src/components/dropzone/upload-primitives.tsx +0 -310
  141. package/src/components/dropzone/use-dropzone.ts +0 -122
  142. package/src/components/empty-state/empty-state.stories.tsx +0 -56
  143. package/src/components/empty-state/empty-state.tsx +0 -39
  144. package/src/components/empty-state/index.ts +0 -1
  145. package/src/components/heading/heading.stories.tsx +0 -74
  146. package/src/components/heading/heading.tsx +0 -28
  147. package/src/components/heading/heading.variants.ts +0 -27
  148. package/src/components/heading/index.ts +0 -1
  149. package/src/components/kbd/kbd.variants.ts +0 -26
  150. package/src/components/menu/util/render-menu-item.tsx +0 -54
  151. package/src/components/multi-select/hooks/use-multi-select.ts +0 -66
  152. package/src/components/multi-select/index.ts +0 -1
  153. package/src/components/multi-select/multi-select.stories.tsx +0 -294
  154. package/src/components/multi-select/multi-select.tsx +0 -300
  155. package/src/components/multi-select/multi-select.variants.ts +0 -22
  156. package/src/components/pagination/components/pagination-option.tsx +0 -27
  157. package/src/components/show/index.ts +0 -1
  158. package/src/components/show/show.stories.tsx +0 -197
  159. package/src/components/show/show.test.tsx +0 -41
  160. package/src/components/show/show.tsx +0 -16
  161. package/src/components/stepper/Stepper.tsx +0 -190
  162. package/src/components/stepper/context/stepper-context.tsx +0 -11
  163. package/src/components/table/table-primitives.tsx +0 -122
  164. package/src/components/table/table.model.ts +0 -20
  165. package/src/components/table-pagination/index.ts +0 -2
  166. package/src/components/table-pagination/table-pagination.model.ts +0 -2
  167. package/src/components/table-pagination/table-pagination.stories.tsx +0 -23
  168. package/src/components/table-pagination/table-pagination.test.tsx +0 -32
  169. package/src/components/table-pagination/table-pagination.tsx +0 -108
  170. package/src/components/tabs/context/tabs-context.tsx +0 -14
  171. package/src/components/tag/tag.variants.ts +0 -31
  172. package/src/components/timeline/timeline-status.ts +0 -5
  173. package/src/components/tree/hooks/use-controllable-tree-state.ts +0 -80
  174. package/src/components/tree/tree-primitives.tsx +0 -126
@@ -1,91 +1,363 @@
1
- import { LucideIcon, UploadCloud } from "lucide-react";
2
- import { ComponentPropsWithoutRef, useMemo } from "react";
3
- import { cn } from "../../lib";
4
- import { FileTypeValue } from "./file-types";
1
+ import { useRender } from "@base-ui/react/use-render";
2
+ import { LucideIcon, UploadCloud, X } from "lucide-react";
5
3
  import {
6
- UploadAcceptedFiles,
7
- UploadContent,
8
- UploadRejectedFiles,
9
- UploadRoot,
10
- UploadTrigger,
11
- } from "./upload-primitives";
12
- import { FileError } from "./use-dropzone";
4
+ type ComponentPropsWithoutRef,
5
+ createContext,
6
+ type ReactNode,
7
+ useContext,
8
+ useState,
9
+ } from "react";
10
+ import type { FileRejection } from "react-dropzone";
11
+ import { useDropzone as useRDZ } from "react-dropzone";
12
+ import { cn } from "../../lib";
13
+ import { Button } from "../button";
14
+ import type { FileTypeValue } from "./file-types";
15
+
16
+ // --- Types ---
17
+
18
+ export type FileErrorCode =
19
+ | "INVALID_EXTENSION"
20
+ | "FILE_TOO_LARGE"
21
+ | "MAX_FILES_EXCEEDED";
22
+
23
+ export type FileError = {
24
+ file: File;
25
+ errorMessage: string;
26
+ errorCode: FileErrorCode;
27
+ };
28
+
29
+ // --- Helpers ---
30
+
31
+ const RDZ_CODE_MAP: Record<string, FileErrorCode> = {
32
+ "file-too-large": "FILE_TOO_LARGE",
33
+ "too-many-files": "MAX_FILES_EXCEEDED",
34
+ "file-invalid-type": "INVALID_EXTENSION",
35
+ };
36
+
37
+ function mapRejections(rejections: FileRejection[]): [File[], FileError[]] {
38
+ const files: File[] = [];
39
+ const errors: FileError[] = [];
40
+ for (const { file, errors: rdzErrors } of rejections) {
41
+ files.push(file);
42
+ for (const err of rdzErrors) {
43
+ errors.push({
44
+ file,
45
+ errorMessage: err.message,
46
+ errorCode: RDZ_CODE_MAP[err.code] ?? "INVALID_EXTENSION",
47
+ });
48
+ }
49
+ }
50
+ return [files, errors];
51
+ }
52
+
53
+ function mimeToLabel(mime: string): string {
54
+ const sub = mime.split("/")[1] ?? mime;
55
+ const withoutPlus = sub.split("+")[0];
56
+ const parts = withoutPlus.split(".");
57
+ return parts[parts.length - 1] ?? withoutPlus;
58
+ }
59
+
60
+ // --- Context ---
61
+
62
+ type DropzoneCtx = {
63
+ acceptedFiles: File[];
64
+ rejectedFiles: File[];
65
+ removeAccepted: (index: number) => void;
66
+ removeRejected: (index: number) => void;
67
+ isDragActive: boolean;
68
+ getRootProps: ReturnType<typeof useRDZ>["getRootProps"];
69
+ getInputProps: ReturnType<typeof useRDZ>["getInputProps"];
70
+ open: () => void;
71
+ };
72
+
73
+ const DropzoneContext = createContext<DropzoneCtx | null>(null);
74
+
75
+ function useDropzoneCtx(): DropzoneCtx {
76
+ const ctx = useContext(DropzoneContext);
77
+ if (!ctx) throw new Error("useDropzoneCtx must be used within DropzoneRoot");
78
+ return ctx;
79
+ }
80
+
81
+ // --- DropzoneRoot ---
82
+
83
+ export type DropzoneRootProps = {
84
+ onFileAdd?: (files: File[]) => void;
85
+ onFileRemove?: (index: number, type: "accepted" | "rejected") => void;
86
+ onFilesChange?: (files: File[]) => void;
87
+ files?: File[];
88
+ maxFiles?: number;
89
+ allowedExtensions?: FileTypeValue[];
90
+ maxFileSize?: number;
91
+ multiple?: boolean;
92
+ onError?: (errors: FileError[]) => void;
93
+ children?: ReactNode;
94
+ } & Omit<ComponentPropsWithoutRef<"div">, "onDrop" | "onError">;
95
+
96
+ export function DropzoneRoot({
97
+ onFileAdd,
98
+ onFileRemove,
99
+ onFilesChange,
100
+ files: controlledFiles,
101
+ maxFiles = 1,
102
+ allowedExtensions = [],
103
+ maxFileSize = 5 * 1024 * 1024,
104
+ multiple,
105
+ onError,
106
+ children,
107
+ className,
108
+ ...props
109
+ }: DropzoneRootProps) {
110
+ const isControlled = controlledFiles !== undefined;
111
+ const [internalFiles, setInternalFiles] = useState<File[]>([]);
112
+ const [rejectedFiles, setRejectedFiles] = useState<File[]>([]);
113
+ const acceptedFiles = isControlled ? controlledFiles : internalFiles;
114
+
115
+ const updateFiles = (next: File[]) => {
116
+ if (!isControlled) setInternalFiles(next);
117
+ onFilesChange?.(next);
118
+ };
119
+
120
+ const accept =
121
+ allowedExtensions.length > 0
122
+ ? Object.fromEntries(allowedExtensions.map((mime) => [mime, []]))
123
+ : undefined;
124
+
125
+ const { getRootProps, getInputProps, isDragActive, open } = useRDZ({
126
+ onDrop(newAccepted, newRejections) {
127
+ const [newRejected, fileErrors] = mapRejections(newRejections);
128
+ updateFiles([...acceptedFiles, ...newAccepted]);
129
+ setRejectedFiles((prev) => [...prev, ...newRejected]);
130
+ onFileAdd?.(newAccepted);
131
+ if (fileErrors.length > 0) onError?.(fileErrors);
132
+ },
133
+ accept,
134
+ maxFiles,
135
+ maxSize: maxFileSize,
136
+ multiple: multiple ?? maxFiles > 1,
137
+ noClick: true,
138
+ });
139
+
140
+ const removeAccepted = (index: number) => {
141
+ updateFiles(acceptedFiles.filter((_, i) => i !== index));
142
+ onFileRemove?.(index, "accepted");
143
+ };
144
+
145
+ const removeRejected = (index: number) => {
146
+ setRejectedFiles((prev) => prev.filter((_, i) => i !== index));
147
+ onFileRemove?.(index, "rejected");
148
+ };
149
+
150
+ return (
151
+ <DropzoneContext.Provider
152
+ value={{
153
+ acceptedFiles,
154
+ rejectedFiles,
155
+ removeAccepted,
156
+ removeRejected,
157
+ isDragActive,
158
+ getRootProps,
159
+ getInputProps,
160
+ open,
161
+ }}
162
+ >
163
+ <div className={cn("w-full", className)} data-slot="dropzone" {...props}>
164
+ {children}
165
+ </div>
166
+ </DropzoneContext.Provider>
167
+ );
168
+ }
169
+
170
+ // --- DropzoneTrigger ---
171
+
172
+ export type DropzoneTriggerProps = useRender.ComponentProps<"div">;
173
+
174
+ export function DropzoneTrigger({
175
+ render,
176
+ className,
177
+ onClick,
178
+ children,
179
+ ...props
180
+ }: DropzoneTriggerProps) {
181
+ const { getRootProps, getInputProps, isDragActive, open } = useDropzoneCtx();
182
+
183
+ const element = useRender({
184
+ defaultTagName: "div",
185
+ render,
186
+ props: {
187
+ ...(getRootProps() as ComponentPropsWithoutRef<"div">),
188
+ ...props,
189
+ className,
190
+ "data-slot": "dropzone-trigger",
191
+ "data-drag-active": isDragActive || undefined,
192
+ onClick: (event: React.MouseEvent<HTMLDivElement>) => {
193
+ open();
194
+ onClick?.(event);
195
+ },
196
+ children: render ? undefined : children,
197
+ },
198
+ });
199
+
200
+ return (
201
+ <>
202
+ {element}
203
+ <input
204
+ {...getInputProps()}
205
+ className="hidden"
206
+ data-slot="dropzone-input"
207
+ />
208
+ </>
209
+ );
210
+ }
211
+
212
+ // --- DropzoneContent ---
213
+
214
+ export function DropzoneContent({
215
+ className,
216
+ ...props
217
+ }: ComponentPropsWithoutRef<"div">) {
218
+ return (
219
+ <div
220
+ className={cn("mt-2 space-y-2", className)}
221
+ data-slot="dropzone-content"
222
+ {...props}
223
+ />
224
+ );
225
+ }
226
+
227
+ // --- DropzoneItem ---
228
+
229
+ export type DropzoneItemProps = ComponentPropsWithoutRef<"div"> & {
230
+ file: File;
231
+ onRemove: () => void;
232
+ renderIcon?: (file: File) => ReactNode;
233
+ };
234
+
235
+ export function DropzoneItem({
236
+ file,
237
+ onRemove,
238
+ className,
239
+ renderIcon,
240
+ ...props
241
+ }: DropzoneItemProps) {
242
+ return (
243
+ <div
244
+ className={cn("flex items-center gap-2 mb-2", className)}
245
+ data-slot="dropzone-item"
246
+ {...props}
247
+ >
248
+ {renderIcon?.(file)}
249
+ {file.name}
250
+ <Button
251
+ variant="ghost"
252
+ size="icon"
253
+ type="button"
254
+ className="h-6 w-6"
255
+ onClick={onRemove}
256
+ data-slot="dropzone-item-remove"
257
+ >
258
+ <X />
259
+ </Button>
260
+ </div>
261
+ );
262
+ }
263
+
264
+ // --- DropzoneFilesList (internal) ---
265
+
266
+ type DropzoneFilesListProps = {
267
+ files: File[];
268
+ onRemove: (index: number) => void;
269
+ renderFile?: (file: File, remove: () => void) => ReactNode;
270
+ };
271
+
272
+ function DropzoneFilesList({
273
+ files,
274
+ onRemove,
275
+ renderFile,
276
+ }: DropzoneFilesListProps) {
277
+ if (files.length === 0) return null;
278
+ return (
279
+ <div className="mt-2 space-y-2">
280
+ {files.map((file, index) =>
281
+ renderFile ? (
282
+ // biome-ignore lint/suspicious/noArrayIndexKey: file name not unique
283
+ <div key={index}>{renderFile(file, () => onRemove(index))}</div>
284
+ ) : (
285
+ <DropzoneItem
286
+ // biome-ignore lint/suspicious/noArrayIndexKey: file name not unique
287
+ key={index}
288
+ file={file}
289
+ onRemove={() => onRemove(index)}
290
+ />
291
+ ),
292
+ )}
293
+ </div>
294
+ );
295
+ }
296
+
297
+ // --- DropzoneAcceptedFiles / DropzoneRejectedFiles ---
298
+
299
+ export type DropzoneFilesProps = {
300
+ renderFile?: (file: File, remove: () => void) => ReactNode;
301
+ };
302
+
303
+ export function DropzoneAcceptedFiles(props: DropzoneFilesProps) {
304
+ const { acceptedFiles, removeAccepted } = useDropzoneCtx();
305
+ return (
306
+ <DropzoneFilesList
307
+ files={acceptedFiles}
308
+ onRemove={removeAccepted}
309
+ {...props}
310
+ />
311
+ );
312
+ }
313
+
314
+ export function DropzoneRejectedFiles(props: DropzoneFilesProps) {
315
+ const { rejectedFiles, removeRejected } = useDropzoneCtx();
316
+ return (
317
+ <DropzoneFilesList
318
+ files={rejectedFiles}
319
+ onRemove={removeRejected}
320
+ {...props}
321
+ />
322
+ );
323
+ }
324
+
325
+ // --- Dropzone (composite) ---
13
326
 
14
327
  export type DropzoneProps = {
15
- /**
16
- * Función que se ejecuta cuando se agregan archivos
17
- */
18
328
  onFileAdd?: (files: File[]) => void;
19
- /**
20
- * Función que se ejecuta cuando se elimina un archivo
21
- */
22
329
  onFileRemove?: (index: number, type: "accepted" | "rejected") => void;
23
- /**
24
- * Evento que captura cambios en lista de archivos
25
- * @default () => {}
26
- */
27
- onChangeFiles?: (files: File[]) => void;
28
- /**
29
- * Lista de archivos
30
- * @default []
31
- */
330
+ onFilesChange?: (files: File[]) => void;
32
331
  files?: File[];
33
- /**
34
- * Número máximo de archivos permitidos
35
- * @default 1
36
- */
37
332
  maxFiles?: number;
38
- /**
39
- * Lista de extensiones permitidas (e.j. ["image/jpeg", "image/png"])
40
- * @default []
41
- */
42
333
  allowedExtensions?: FileTypeValue[];
43
- /**
44
- * Tamaño máximo de archivo en bytes
45
- * @default 5MB
46
- */
47
334
  maxFileSize?: number;
48
- /**
49
- * Permite seleccionar múltiples archivos
50
- */
51
335
  multiple?: boolean;
52
- /**
53
- * Función para renderizar cada archivo aceptado
54
- */
55
- renderAcceptedFile?: (file: File, removeItem: () => void) => React.ReactNode;
56
- /**
57
- * Función para renderizar cada archivo rechazado
58
- */
59
- renderRejectedFile?: (file: File, removeItem: () => void) => React.ReactNode;
60
- /**
61
- * Mostrar archivos rechazados
62
- * @default false
63
- */
336
+ renderAcceptedFile?: (file: File, removeItem: () => void) => ReactNode;
337
+ renderRejectedFile?: (file: File, removeItem: () => void) => ReactNode;
64
338
  showRejected?: boolean;
65
- /**
66
- * Clase CSS para el contenedor
67
- */
68
339
  className?: string;
69
- /**
70
- * Icono personalizado para el dropzone
71
- * @default UploadCloud
72
- */
73
340
  icon?: LucideIcon;
74
- /**
75
- * Tamaño del icono
76
- * @default 40
77
- */
78
341
  iconSize?: number;
79
- /**
80
- * Función que se ejecuta cuando se rechazan archivos
81
- */
82
342
  onError?: (fileErrors: FileError[]) => void;
343
+ label?: string;
344
+ /** Styles applied to each internal slot. */
345
+ classNames?: {
346
+ /** Dashed drop area that contains the icon and label. */
347
+ trigger?: string;
348
+ /** Upload icon rendered inside the trigger. */
349
+ icon?: string;
350
+ /** Text container below the icon (label + meta lines). */
351
+ label?: string;
352
+ /** Wrapper around the accepted/rejected file lists. */
353
+ content?: string;
354
+ };
83
355
  } & Omit<ComponentPropsWithoutRef<"div">, "onDrop" | "onError">;
84
356
 
85
- export const Dropzone = ({
357
+ export function Dropzone({
86
358
  onFileAdd,
87
359
  onFileRemove,
88
- onChangeFiles = () => {},
360
+ onFilesChange,
89
361
  files,
90
362
  maxFiles = 1,
91
363
  allowedExtensions = [],
@@ -95,60 +367,66 @@ export const Dropzone = ({
95
367
  renderRejectedFile,
96
368
  showRejected = false,
97
369
  className,
370
+ classNames,
98
371
  icon: Icon = UploadCloud,
99
372
  iconSize = 40,
100
373
  onError,
374
+ label = "Arrastra y suelta archivos aquí o haz clic para seleccionar",
101
375
  ...props
102
- }: DropzoneProps) => {
103
- const allowedLabels = useMemo(() => {
104
- return allowedExtensions.map((ext) => ext.split("/")?.[1]).join(", ");
105
- }, [allowedExtensions]);
376
+ }: DropzoneProps) {
377
+ const allowedLabels = allowedExtensions.map(mimeToLabel).join(", ");
106
378
 
107
379
  return (
108
- <UploadRoot
380
+ <DropzoneRoot
109
381
  onFileAdd={onFileAdd}
110
382
  onFileRemove={onFileRemove}
111
383
  files={files}
112
- onChangeFiles={onChangeFiles}
384
+ onFilesChange={onFilesChange}
113
385
  maxFiles={maxFiles}
114
386
  allowedExtensions={allowedExtensions}
115
387
  maxFileSize={maxFileSize}
116
- multiple={multiple ? true : undefined}
388
+ multiple={multiple}
117
389
  className={cn("w-full", className)}
118
390
  onError={onError}
119
391
  {...props}
120
392
  >
121
- <UploadTrigger asChild>
122
- <div
123
- className={cn(
124
- "w-full min-h-[150px] border-2 border-dashed rounded-lg border-muted p-4",
125
- "hover:bg-muted/50 data-[drag-active=true]:border-primary data-[drag-active=true]:bg-primary/10",
126
- "transition-colors flex flex-col items-center justify-center gap-4",
127
- "pointer-events-auto",
128
- )}
129
- >
130
- <Icon
131
- className="text-muted-foreground pointer-events-none"
132
- style={{ width: iconSize, height: iconSize }}
133
- />
134
- <div className="text-center text-muted-foreground pointer-events-none">
135
- <p>Arrastra y suelta archivos aquí o haz clic para seleccionar</p>
136
- {allowedExtensions.length > 0 && (
137
- <p className="text-sm">Tipos permitidos: {allowedLabels}</p>
393
+ <DropzoneTrigger
394
+ render={
395
+ <div
396
+ className={cn(
397
+ "w-full min-h-[150px] border-2 border-dashed rounded-lg border-muted p-4",
398
+ "hover:bg-muted/50 data-[drag-active]:border-primary data-[drag-active]:bg-primary/10",
399
+ "transition-colors flex flex-col items-center justify-center gap-4 cursor-pointer",
400
+ classNames?.trigger,
138
401
  )}
139
- <p className="text-sm">
140
- Tamaño máximo: {Math.floor(maxFileSize / 1024 / 1024)}MB
141
- </p>
402
+ >
403
+ <Icon
404
+ className={cn("text-muted-foreground", classNames?.icon)}
405
+ style={{ width: iconSize, height: iconSize }}
406
+ />
407
+ <div
408
+ className={cn(
409
+ "text-center text-muted-foreground",
410
+ classNames?.label,
411
+ )}
412
+ >
413
+ <p>{label}</p>
414
+ {allowedExtensions.length > 0 && (
415
+ <p className="text-sm">Tipos permitidos: {allowedLabels}</p>
416
+ )}
417
+ <p className="text-sm">
418
+ Tamaño máximo: {Math.floor(maxFileSize / 1024 / 1024)}MB
419
+ </p>
420
+ </div>
142
421
  </div>
143
- </div>
144
- </UploadTrigger>
145
-
146
- <UploadContent>
147
- <UploadAcceptedFiles renderFile={renderAcceptedFile} />
422
+ }
423
+ />
424
+ <DropzoneContent className={classNames?.content}>
425
+ <DropzoneAcceptedFiles renderFile={renderAcceptedFile} />
148
426
  {showRejected && (
149
- <UploadRejectedFiles renderFile={renderRejectedFile} />
427
+ <DropzoneRejectedFiles renderFile={renderRejectedFile} />
150
428
  )}
151
- </UploadContent>
152
- </UploadRoot>
429
+ </DropzoneContent>
430
+ </DropzoneRoot>
153
431
  );
154
- };
432
+ }
@@ -1,3 +1,2 @@
1
1
  export * from "./dropzone";
2
2
  export * from "./file-types";
3
- export * from "./upload-primitives";
@@ -0,0 +1,165 @@
1
+ import type { Meta, StoryObj } from "@storybook/react-vite";
2
+ import { MessageSquareMore, SearchX, UploadCloud } from "lucide-react";
3
+ import {
4
+ Button,
5
+ Empty,
6
+ EmptyContent,
7
+ EmptyDescription,
8
+ EmptyHeader,
9
+ EmptyMedia,
10
+ EmptyRoot,
11
+ EmptyTitle,
12
+ Stack,
13
+ } from "../../components";
14
+
15
+ /**
16
+ * Composite para estados vacíos: sin resultados, sin datos, sin permisos, etc.
17
+ * `Empty` es el composite principal — acepta `icon`, `title`, `description` y
18
+ * `action`. `iconVariant="icon"` muestra el ícono dentro de un chip muted
19
+ * (`size-8`); el default muestra el ícono suelto a `size-12`.
20
+ * `action` se renderiza dentro de `EmptyContent`; `children` escapa al root
21
+ * directamente, útil para layouts que no encajan en el wrapper centrado.
22
+ * Para composición manual usá los primitives: `EmptyRoot`, `EmptyHeader`,
23
+ * `EmptyMedia`, `EmptyTitle`, `EmptyDescription`, `EmptyContent`.
24
+ * Para estilar partes internas usá `classNames`.
25
+ */
26
+ const meta: Meta<typeof Empty> = {
27
+ title: "Components/Empty",
28
+ component: Empty,
29
+ args: {
30
+ title: "No hay datos",
31
+ },
32
+ argTypes: {
33
+ classNames: { control: false },
34
+ },
35
+ tags: ["beta"],
36
+ };
37
+
38
+ export default meta;
39
+
40
+ type Story = StoryObj<typeof Empty>;
41
+
42
+ export const Default: Story = {};
43
+
44
+ /**
45
+ * `className` estila el root. `classNames` expone los slots `header`, `media`,
46
+ * `title`, `description` y `content`.
47
+ */
48
+ export const WithClassNames: Story = {
49
+ args: {
50
+ icon: <SearchX />,
51
+ iconVariant: "icon",
52
+ title: "Sin resultados",
53
+ description: "Probá con otros filtros.",
54
+ action: <Button size="sm">Limpiar</Button>,
55
+ classNames: {
56
+ title: "text-base",
57
+ description: "italic",
58
+ media: "bg-primary/10 text-primary",
59
+ },
60
+ },
61
+ };
62
+
63
+ export const WithDescription: Story = {
64
+ args: {
65
+ description: "Intentá buscar con otros filtros o revisá más tarde.",
66
+ },
67
+ };
68
+
69
+ /**
70
+ * `iconVariant="icon"` envuelve el ícono en un chip `size-8` con fondo muted.
71
+ * Ideal para estados vacíos compactos (tablas, paneles). El default muestra
72
+ * el ícono suelto a `size-12` para páginas completas o secciones hero.
73
+ */
74
+ export const IconVariant: Story = {
75
+ args: {
76
+ icon: <SearchX />,
77
+ iconVariant: "icon",
78
+ title: "Sin resultados",
79
+ description: "No encontramos coincidencias para tu búsqueda.",
80
+ },
81
+ };
82
+
83
+ export const WithAction: Story = {
84
+ args: {
85
+ icon: <MessageSquareMore />,
86
+ title: "No tenés mensajes",
87
+ description: "Cuando recibas uno aparecerá acá.",
88
+ action: (
89
+ <Stack>
90
+ <Button variant="outline">Importar</Button>
91
+ <Button>Crear nuevo</Button>
92
+ </Stack>
93
+ ),
94
+ },
95
+ };
96
+
97
+ /**
98
+ * `icon={null}` suprime el media. Útil para estados vacíos en tablas o
99
+ * contextos donde el ícono añade ruido visual innecesario.
100
+ */
101
+ export const NoIcon: Story = {
102
+ args: {
103
+ icon: null,
104
+ title: "Tabla vacía",
105
+ description: "Agregá una fila para empezar.",
106
+ action: <Button size="sm">Agregar fila</Button>,
107
+ },
108
+ };
109
+
110
+ /**
111
+ * `children` se renderiza fuera de `EmptyContent`, directamente en `EmptyRoot`.
112
+ * Permite añadir elementos que no deben quedar dentro del wrapper centrado —
113
+ * por ejemplo un link de texto o un componente con ancho propio.
114
+ */
115
+ export const WithChildren: Story = {
116
+ render: () => (
117
+ <Empty
118
+ icon={<UploadCloud />}
119
+ title="Sin archivos"
120
+ description="Subí tu primer archivo para empezar."
121
+ action={<Button>Subir archivo</Button>}
122
+ >
123
+ <p className="text-xs text-muted-foreground">
124
+ Formatos soportados: PDF, PNG, JPG — máx. 10 MB
125
+ </p>
126
+ </Empty>
127
+ ),
128
+ };
129
+
130
+ /**
131
+ * Composición manual con primitives. Usala cuando necesitás reordenar partes,
132
+ * agregar slots extra, o construir layouts que el composite no cubre.
133
+ *
134
+ * ```tsx
135
+ * <EmptyRoot>
136
+ * <EmptyHeader>
137
+ * <EmptyMedia variant="icon"><Icon /></EmptyMedia>
138
+ * <EmptyTitle>Título</EmptyTitle>
139
+ * <EmptyDescription>Descripción</EmptyDescription>
140
+ * </EmptyHeader>
141
+ * <EmptyContent>
142
+ * <Button>Acción</Button>
143
+ * </EmptyContent>
144
+ * </EmptyRoot>
145
+ * ```
146
+ */
147
+ export const Primitive: Story = {
148
+ render: () => (
149
+ <EmptyRoot>
150
+ <EmptyHeader>
151
+ <EmptyMedia variant="icon">
152
+ <SearchX />
153
+ </EmptyMedia>
154
+ <EmptyTitle>Sin resultados</EmptyTitle>
155
+ <EmptyDescription>
156
+ No encontramos nada que coincida. Probá con{" "}
157
+ <a href="#">otros filtros</a>.
158
+ </EmptyDescription>
159
+ </EmptyHeader>
160
+ <EmptyContent>
161
+ <Button variant="outline">Limpiar filtros</Button>
162
+ </EmptyContent>
163
+ </EmptyRoot>
164
+ ),
165
+ };