@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,155 +0,0 @@
1
- import { type DialogProps } from "@radix-ui/react-dialog";
2
- import { Command } from "cmdk";
3
- import { Search } from "lucide-react";
4
- import * as React from "react";
5
- import { cn } from "../../lib";
6
- import { DialogPopup, DialogRoot } from "../dialog";
7
-
8
- const AutoCompleteRoot = ({
9
- className,
10
- ...props
11
- }: React.ComponentProps<typeof Command>) => (
12
- <Command
13
- data-slot="auto-complete"
14
- className={cn(
15
- "flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground",
16
- className,
17
- )}
18
- {...props}
19
- />
20
- );
21
-
22
- interface AutoCompleteDialogProps extends DialogProps {}
23
-
24
- const AutoCompleteDialog = ({
25
- children,
26
- ...props
27
- }: AutoCompleteDialogProps) => {
28
- return (
29
- <DialogRoot {...props} data-slot="auto-complete-dialog">
30
- <DialogPopup
31
- className="p-0 overflow-hidden shadow-lg"
32
- data-slot="auto-complete-dialog-content"
33
- >
34
- <AutoCompleteRoot className="[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
35
- {children}
36
- </AutoCompleteRoot>
37
- </DialogPopup>
38
- </DialogRoot>
39
- );
40
- };
41
-
42
- const AutoCompleteInput = ({
43
- className,
44
- ...props
45
- }: React.ComponentProps<typeof Command.Input>) => (
46
- <div
47
- className="flex items-center px-3 border-b"
48
- data-slot="auto-complete-input-container"
49
- >
50
- <Search
51
- className="w-4 h-4 mr-2 opacity-50 shrink-0"
52
- data-slot="auto-complete-input-icon"
53
- />
54
- <Command.Input
55
- data-slot="auto-complete-input"
56
- className={cn(
57
- "flex h-11 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50",
58
- className,
59
- )}
60
- {...props}
61
- />
62
- </div>
63
- );
64
-
65
- const AutoCompleteList = ({
66
- className,
67
- ...props
68
- }: React.ComponentProps<typeof Command.List>) => (
69
- <Command.List
70
- data-slot="auto-complete-list"
71
- className={cn("max-h-[300px] overflow-y-auto overflow-x-hidden", className)}
72
- {...props}
73
- />
74
- );
75
-
76
- const AutoCompleteEmpty = (
77
- props: React.ComponentProps<typeof Command.Empty>,
78
- ) => (
79
- <Command.Empty
80
- className="py-6 text-sm text-center"
81
- data-slot="auto-complete-empty"
82
- {...props}
83
- />
84
- );
85
-
86
- const AutoCompleteGroup = ({
87
- className,
88
- ...props
89
- }: React.ComponentProps<typeof Command.Group>) => (
90
- <Command.Group
91
- data-slot="auto-complete-group"
92
- className={cn(
93
- "overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground",
94
- className,
95
- )}
96
- {...props}
97
- />
98
- );
99
-
100
- const AutoCompleteSeparator = ({
101
- className,
102
- ...props
103
- }: React.ComponentProps<typeof Command.Separator>) => (
104
- <Command.Separator
105
- data-slot="auto-complete-separator"
106
- className={cn("-mx-1 h-px bg-input", className)}
107
- {...props}
108
- />
109
- );
110
-
111
- const AutoCompleteItem = ({
112
- className,
113
- ...props
114
- }: React.ComponentProps<typeof Command.Item>) => {
115
- return (
116
- <Command.Item
117
- data-slot="auto-complete-item"
118
- className={cn(
119
- "aria-selected:bg-accent aria-selected:text-accent-foreground",
120
- "aria-disabled:pointer-events-none aria-disabled:opacity-50",
121
- "relative flex gap-2 cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none",
122
- className,
123
- )}
124
- {...props}
125
- />
126
- );
127
- };
128
-
129
- const AutoCompleteShortcut = ({
130
- className,
131
- ...props
132
- }: React.HTMLAttributes<HTMLSpanElement>) => {
133
- return (
134
- <span
135
- data-slot="auto-complete-shortcut"
136
- className={cn(
137
- "ml-auto text-xs tracking-widest text-muted-foreground",
138
- className,
139
- )}
140
- {...props}
141
- />
142
- );
143
- };
144
-
145
- export {
146
- AutoCompleteDialog,
147
- AutoCompleteEmpty,
148
- AutoCompleteGroup,
149
- AutoCompleteInput,
150
- AutoCompleteItem,
151
- AutoCompleteList,
152
- AutoCompleteRoot,
153
- AutoCompleteSeparator,
154
- AutoCompleteShortcut,
155
- };
@@ -1,21 +0,0 @@
1
- import { Meta, StoryObj } from "@storybook/react-vite";
2
- import { BackgroundImage } from "../../components";
3
-
4
- /**
5
- * Componente que facilita el manejo de imágenes de fondo.
6
- */
7
- const meta: Meta<typeof BackgroundImage> = {
8
- title: "Others/BackgroundImage",
9
- component: BackgroundImage,
10
- args: {
11
- src: "https://m.media-amazon.com/images/I/81PgpygKwdL._AC_UF894,1000_QL80_.jpg",
12
- children:
13
- "Lorem Ipsum es simplemente el texto de relleno de las imprentas y archivos de texto. Lorem Ipsum ha sido el texto de relleno estándar de las industrias desde el año 1500, cuando un impresor (N. del T. persona que se dedica a la imprenta)",
14
- className: "text-white w-[400px] h-[200px] rounded p-4",
15
- },
16
- };
17
-
18
- export default meta;
19
- type Story = StoryObj<typeof meta>;
20
-
21
- export const Default: Story = {};
@@ -1,29 +0,0 @@
1
- import { render, screen } from "@testing-library/react";
2
- import { describe, expect, it } from "vitest";
3
- import { BackgroundImage } from ".";
4
-
5
- describe("BackgroundImage component", () => {
6
- const imageSrc = "https://source.unsplash.com/random";
7
-
8
- it("should render the background image", () => {
9
- render(<BackgroundImage src={imageSrc} data-testid="container" />);
10
- const container = screen.getByTestId("container");
11
-
12
- expect(container).toBeInTheDocument();
13
- expect(container).toHaveStyle({
14
- backgroundImage: `url(${imageSrc})`,
15
- });
16
- });
17
-
18
- it("should render the children", () => {
19
- const text = "Lorem Ipsum";
20
- render(
21
- <BackgroundImage src={imageSrc}>
22
- <p>{text}</p>
23
- </BackgroundImage>,
24
- );
25
- const paragraph = screen.getByText(text);
26
-
27
- expect(paragraph).toBeInTheDocument();
28
- });
29
- });
@@ -1,23 +0,0 @@
1
- import { HTMLProps } from "react";
2
- import { cn } from "../../lib";
3
-
4
- interface Props extends HTMLProps<HTMLDivElement> {
5
- /**
6
- * La URL de la imagen
7
- */
8
- src: string;
9
- }
10
-
11
- export const BackgroundImage = ({ src, ...props }: Props) => {
12
- return (
13
- <div
14
- {...props}
15
- data-slot="background-image"
16
- className={cn("w-full block bg-cover bg-center", props.className)}
17
- style={{
18
- ...props.style,
19
- backgroundImage: `url(${src})`,
20
- }}
21
- />
22
- );
23
- };
@@ -1 +0,0 @@
1
- export * from "./background-image";
@@ -1,44 +0,0 @@
1
- import { cva } from "class-variance-authority";
2
-
3
- export const buttonVariants = cva(
4
- [
5
- "inline-flex items-center justify-center gap-2",
6
- "text-sm font-medium transition-all",
7
- "ring-offset-background",
8
- "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
9
- "disabled:pointer-events-none disabled:opacity-50",
10
- "hover:brightness-105",
11
- ],
12
- {
13
- variants: {
14
- variant: {
15
- default: "bg-primary text-primary-foreground",
16
- error: "bg-error text-error-foreground",
17
- success: "bg-success text-success-foreground",
18
- outline:
19
- "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
20
- secondary:
21
- "bg-secondary text-secondary-foreground hover:brightness-100 hover:bg-secondary/80",
22
- ghost:
23
- "hover:bg-accent hover:brightness-100 hover:text-accent-foreground",
24
- link: "text-primary underline-offset-4 hover:underline",
25
- },
26
- size: {
27
- default: "h-10 px-4 py-2",
28
- sm: "h-9 px-3",
29
- lg: "h-11 px-8",
30
- icon: "w-10 aspect-square",
31
- },
32
- shape: {
33
- rounded: "rounded-md",
34
- square: "rounded-none",
35
- circle: "rounded-full",
36
- },
37
- },
38
- defaultVariants: {
39
- variant: "default",
40
- size: "default",
41
- shape: "rounded",
42
- },
43
- },
44
- );
@@ -1,21 +0,0 @@
1
- import { LoadingIcon } from "./loading-icon";
2
-
3
- interface LoaderOverlayProps {
4
- loading: boolean;
5
- }
6
-
7
- export const LoaderOverlay = ({ loading }: LoaderOverlayProps) => (
8
- <span
9
- style={{
10
- position: "absolute",
11
- inset: 0,
12
- display: "flex",
13
- alignItems: "center",
14
- justifyContent: "center",
15
- background: "inherit",
16
- borderRadius: "inherit",
17
- }}
18
- >
19
- <LoadingIcon loading={loading} animate={false} />
20
- </span>
21
- );
@@ -1,47 +0,0 @@
1
- import { LoaderCircle, LucideProps } from "lucide-react";
2
- import { useRef } from "react";
3
- import { CSSTransition } from "react-transition-group";
4
- import { cn } from "../../../lib";
5
-
6
- interface LoadingIconProps extends LucideProps {
7
- loading?: boolean;
8
- animate?: boolean;
9
- }
10
-
11
- const LoadingIcon = ({
12
- loading,
13
- animate = true,
14
- ...props
15
- }: LoadingIconProps) => {
16
- const nodeRef = useRef(null);
17
-
18
- if (!animate) {
19
- return loading ? (
20
- <LoaderCircle
21
- {...props}
22
- data-testid="btn-loader"
23
- className={cn("animate-spin", props.className)}
24
- />
25
- ) : null;
26
- }
27
-
28
- return (
29
- <CSSTransition
30
- unmountOnExit
31
- mountOnEnter
32
- timeout={200}
33
- in={loading}
34
- classNames="btn-loader"
35
- nodeRef={nodeRef}
36
- >
37
- <LoaderCircle
38
- {...props}
39
- ref={nodeRef}
40
- data-testid="btn-loader"
41
- className={cn("animate-spin", props.className)}
42
- />
43
- </CSSTransition>
44
- );
45
- };
46
-
47
- export { LoadingIcon, type LoadingIconProps };
@@ -1,310 +0,0 @@
1
- import { Slot } from "@radix-ui/react-slot";
2
- import { useControllableState } from "@radix-ui/react-use-controllable-state";
3
- import { X } from "lucide-react";
4
- import React, {
5
- ComponentPropsWithoutRef,
6
- createContext,
7
- ReactNode,
8
- useContext,
9
- useRef,
10
- useState,
11
- } from "react";
12
- import { cn } from "../../lib";
13
- import { Button } from "../button";
14
- import { FileError, useDropzone } from "./use-dropzone";
15
-
16
- type UploadContextType = {
17
- files: File[];
18
- rejectedFiles: File[];
19
- handleFileAdd: (files: File[]) => void;
20
- handleFileRemove: (index: number, type: "accepted" | "rejected") => void;
21
- inputProps: React.ComponentPropsWithoutRef<"input">;
22
- isDragActive: boolean;
23
- dropzoneProps: {
24
- onDragOver: React.DragEventHandler<HTMLDivElement>;
25
- onDragEnter: React.DragEventHandler<HTMLDivElement>;
26
- onDragLeave: React.DragEventHandler<HTMLDivElement>;
27
- onDrop: React.DragEventHandler<HTMLDivElement>;
28
- };
29
- };
30
-
31
- const UploadContext = createContext<UploadContextType | null>(null);
32
-
33
- const useUploadContext = () => {
34
- const context = useContext(UploadContext);
35
- if (!context) {
36
- throw new Error(
37
- "useUploadContext must be used within a UploadContextProvider",
38
- );
39
- }
40
- return context;
41
- };
42
-
43
- type UploadRootProps = {
44
- onFileAdd?: (files: File[]) => void;
45
- onFileRemove?: (index: number, type: "accepted" | "rejected") => void;
46
- onChangeFiles?: (files: File[]) => void;
47
- files?: File[];
48
- maxFiles?: number;
49
- allowedExtensions?: string[];
50
- maxFileSize?: number;
51
- multiple?: true;
52
- className?: string;
53
- children?: ReactNode;
54
- onError?: (fileErrors: FileError[]) => void;
55
- };
56
-
57
- const UploadRoot = ({
58
- onFileAdd,
59
- onFileRemove,
60
- onChangeFiles,
61
- files: prop,
62
- maxFiles = 1,
63
- allowedExtensions = [],
64
- maxFileSize = 5 * 1024 * 1024, // 5MB
65
- multiple,
66
- children,
67
- onError,
68
- ...props
69
- }: UploadRootProps) => {
70
- const [files = [], setFiles] = useControllableState<File[]>({
71
- defaultProp: [],
72
- prop,
73
- onChange: onChangeFiles,
74
- });
75
- const [rejectedFiles, setRejectedFiles] = useState<File[]>([]);
76
- const { isDragActive, validateFiles, dropzoneProps } = useDropzone({
77
- onDrop: (acceptedFiles, rejectedFiles) => {
78
- setFiles([...files, ...acceptedFiles]);
79
- setRejectedFiles((prevRejectedFiles) => [
80
- ...prevRejectedFiles,
81
- ...rejectedFiles,
82
- ]);
83
- onFileAdd?.(acceptedFiles);
84
- },
85
- allowedExtensions,
86
- maxFileSize,
87
- maxFiles,
88
- onError,
89
- });
90
-
91
- const handleFileAdd = (newFiles: File[]) => {
92
- const [rejected = [], accepted = []] = validateFiles(newFiles);
93
-
94
- // Caso 1: Single file upload (maxFiles === 1 o !multiple)
95
- if (maxFiles === 1 || !multiple) {
96
- setFiles(accepted.slice(0, 1));
97
- setRejectedFiles([...rejected, ...accepted.slice(1)]);
98
- onFileAdd?.(accepted.slice(0, 1));
99
- return;
100
- }
101
-
102
- // Caso 2: Multiple files upload
103
- const availableSlots = maxFiles - files.length;
104
- if (availableSlots <= 0) {
105
- // No hay slots disponibles, todos van a rejected
106
- setRejectedFiles((prevRejected) => [...prevRejected, ...accepted]);
107
- onFileAdd?.([]);
108
- return;
109
- }
110
-
111
- // Dividir los archivos aceptados entre los que caben y los que no
112
- const acceptedFiles = accepted.slice(0, availableSlots);
113
- const overflowFiles = accepted.slice(availableSlots);
114
-
115
- // Actualizar estados
116
- setFiles([...files, ...acceptedFiles]);
117
- setRejectedFiles((prevRejected) => [
118
- ...prevRejected,
119
- ...rejected,
120
- ...overflowFiles,
121
- ]);
122
-
123
- onFileAdd?.(acceptedFiles);
124
- };
125
-
126
- const handleFileRemove = (index: number, type: "accepted" | "rejected") => {
127
- if (type === "accepted") {
128
- setFiles(files.filter((_, i) => i !== index));
129
- } else {
130
- setRejectedFiles((prevRejectedFiles) =>
131
- prevRejectedFiles.filter((_, i) => i !== index),
132
- );
133
- }
134
- onFileRemove?.(index, type);
135
- };
136
-
137
- return (
138
- <UploadContext.Provider
139
- value={{
140
- files,
141
- rejectedFiles,
142
- handleFileAdd,
143
- handleFileRemove,
144
- inputProps: {
145
- accept: allowedExtensions.join(","),
146
- multiple: multiple,
147
- },
148
- isDragActive,
149
- dropzoneProps,
150
- }}
151
- >
152
- <div {...props} data-slot="upload">
153
- {children}
154
- </div>
155
- </UploadContext.Provider>
156
- );
157
- };
158
-
159
- type UploadTriggerProps = {
160
- asChild?: boolean;
161
- } & ComponentPropsWithoutRef<"button">;
162
-
163
- const UploadTrigger = ({ asChild, ...props }: UploadTriggerProps) => {
164
- const { handleFileAdd, inputProps, dropzoneProps, isDragActive } =
165
- useUploadContext();
166
- const inputRef = useRef<HTMLInputElement>(null);
167
- const Comp = asChild ? Slot : "button";
168
-
169
- return (
170
- <>
171
- <Comp
172
- data-slot="upload-trigger"
173
- {...props}
174
- {...(dropzoneProps as any)}
175
- data-drag-active={isDragActive || false}
176
- onClick={(e) => {
177
- props.onClick?.(e);
178
- inputRef.current?.click();
179
- }}
180
- />
181
- <input
182
- type="file"
183
- className="hidden"
184
- data-slot="upload-input"
185
- {...inputProps}
186
- ref={inputRef}
187
- onChange={(e) => handleFileAdd(Array.from(e.target.files || []))}
188
- />
189
- </>
190
- );
191
- };
192
-
193
- type UploadContentProps = ComponentPropsWithoutRef<"div"> & {
194
- renderRejectedFile?: (file: File) => ReactNode;
195
- };
196
-
197
- const UploadContent = ({ className, ...props }: UploadContentProps) => {
198
- return (
199
- <div
200
- {...props}
201
- className={cn("mt-2 space-y-2", className)}
202
- data-slot="upload-content"
203
- />
204
- );
205
- };
206
-
207
- type UploadFilesListProps = {
208
- files: File[];
209
- handleFileRemove: (index: number, type: "accepted" | "rejected") => void;
210
- fileType: "accepted" | "rejected";
211
- children?: ReactNode;
212
- renderFile?: (file: File, removeItem: () => void) => ReactNode;
213
- };
214
-
215
- const UploadFilesList = ({
216
- files,
217
- handleFileRemove,
218
- fileType,
219
- children,
220
- renderFile,
221
- ...props
222
- }: UploadFilesListProps) => {
223
- if (files.length === 0) return null;
224
-
225
- return (
226
- <div className="mt-2 space-y-2" {...props} data-slot="upload-files-list">
227
- {children}
228
- {files.map((file, index) => {
229
- if (renderFile) {
230
- return renderFile(file, () => handleFileRemove(index, fileType));
231
- }
232
- return (
233
- <UploadItem
234
- key={file.name}
235
- file={file}
236
- onRemove={() => handleFileRemove(index, fileType)}
237
- />
238
- );
239
- })}
240
- </div>
241
- );
242
- };
243
-
244
- type UploadFilesProps = Omit<
245
- UploadFilesListProps,
246
- "fileType" | "handleFileRemove" | "files"
247
- >;
248
-
249
- export const UploadAcceptedFiles = (props: UploadFilesProps) => {
250
- const { files, handleFileRemove } = useUploadContext();
251
-
252
- return (
253
- <UploadFilesList
254
- data-slot="upload-accepted-files"
255
- files={files}
256
- handleFileRemove={handleFileRemove}
257
- fileType="accepted"
258
- {...props}
259
- />
260
- );
261
- };
262
-
263
- export const UploadRejectedFiles = (props: UploadFilesProps) => {
264
- const { rejectedFiles, handleFileRemove } = useUploadContext();
265
-
266
- return (
267
- <UploadFilesList
268
- data-slot="upload-rejected-files"
269
- files={rejectedFiles}
270
- handleFileRemove={handleFileRemove}
271
- fileType="rejected"
272
- {...props}
273
- />
274
- );
275
- };
276
-
277
- type UploadItemProps = ComponentPropsWithoutRef<"div"> & {
278
- file: File;
279
- onRemove: () => void;
280
- renderIcon?: (file: File) => ReactNode;
281
- };
282
-
283
- const UploadItem = ({
284
- file,
285
- onRemove,
286
- className,
287
- renderIcon,
288
- ...props
289
- }: UploadItemProps) => (
290
- <div
291
- className={cn("flex items-center gap-2 mb-2", className)}
292
- {...props}
293
- data-slot="upload-item"
294
- >
295
- {renderIcon?.(file)}
296
- {file.name}
297
- <Button
298
- variant="ghost"
299
- size="icon"
300
- type="button"
301
- className="h-6 w-6"
302
- onClick={onRemove}
303
- data-slot="upload-item-remove"
304
- >
305
- <X data-slot="upload-item-remove-icon" />
306
- </Button>
307
- </div>
308
- );
309
-
310
- export { UploadContent, UploadItem, UploadRoot, UploadTrigger };