@pol-studios/ui 1.0.0

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.
@@ -0,0 +1,256 @@
1
+ "use client";
2
+
3
+ // src/file/dropzone.tsx
4
+ import { CheckCircle, File, Loader2, Upload, X } from "lucide-react";
5
+ import {
6
+ createContext,
7
+ useCallback,
8
+ useContext
9
+ } from "react";
10
+
11
+ // src/primitives/button.tsx
12
+ import * as React from "react";
13
+ import { Slot } from "@radix-ui/react-slot";
14
+ import { cva } from "class-variance-authority";
15
+ import { cn } from "@pol-studios/utils";
16
+ import { jsx } from "react/jsx-runtime";
17
+ var buttonVariants = cva(
18
+ "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
19
+ {
20
+ variants: {
21
+ variant: {
22
+ default: "bg-primary text-primary-foreground hover:bg-primary/90",
23
+ destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
24
+ outline: "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
25
+ secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
26
+ ghost: "hover:bg-accent hover:text-accent-foreground",
27
+ link: "text-primary underline-offset-4 hover:underline"
28
+ },
29
+ size: {
30
+ default: "h-10 px-4 py-2",
31
+ sm: "h-9 rounded-md px-3",
32
+ lg: "h-11 rounded-md px-8",
33
+ icon: "h-10 w-10"
34
+ }
35
+ },
36
+ defaultVariants: {
37
+ variant: "default",
38
+ size: "default"
39
+ }
40
+ }
41
+ );
42
+ var Button = React.forwardRef(
43
+ ({ className, variant, size, asChild = false, ...props }, ref) => {
44
+ const Comp = asChild ? Slot : "button";
45
+ return /* @__PURE__ */ jsx(
46
+ Comp,
47
+ {
48
+ className: cn(buttonVariants({ variant, size, className })),
49
+ ref,
50
+ ...props
51
+ }
52
+ );
53
+ }
54
+ );
55
+ Button.displayName = "Button";
56
+
57
+ // src/file/dropzone.tsx
58
+ import { cn as cn2 } from "@pol-studios/utils";
59
+ import { Fragment, jsx as jsx2, jsxs } from "react/jsx-runtime";
60
+ var formatBytes = (bytes, decimals = 2, size) => {
61
+ const k = 1e3;
62
+ const dm = decimals < 0 ? 0 : decimals;
63
+ const sizes = ["bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
64
+ if (bytes === 0 || bytes === void 0)
65
+ return size !== void 0 ? `0 ${size}` : "0 bytes";
66
+ const i = size !== void 0 ? sizes.indexOf(size) : Math.floor(Math.log(bytes) / Math.log(k));
67
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + " " + sizes[i];
68
+ };
69
+ var DropzoneContext = createContext(
70
+ void 0
71
+ );
72
+ var Dropzone = ({
73
+ className,
74
+ children,
75
+ getRootProps,
76
+ getInputProps,
77
+ ...restProps
78
+ }) => {
79
+ const isSuccess = restProps.isSuccess;
80
+ const isActive = restProps.isDragActive;
81
+ const isInvalid = restProps.isDragActive && restProps.isDragReject || restProps.errors.length > 0 && !restProps.isSuccess || restProps.files.some((file) => file.errors.length !== 0);
82
+ return /* @__PURE__ */ jsx2(DropzoneContext.Provider, { value: { ...restProps }, children: /* @__PURE__ */ jsxs(
83
+ "div",
84
+ {
85
+ ...getRootProps({
86
+ className: cn2(
87
+ "border-2 border-gray-300 rounded-lg p-6 text-center bg-card transition-colors duration-300 text-foreground",
88
+ className,
89
+ isSuccess ? "border-solid" : "border-dashed",
90
+ isActive && "border-primary bg-primary/10",
91
+ isInvalid && "border-destructive bg-destructive/10"
92
+ )
93
+ }),
94
+ children: [
95
+ /* @__PURE__ */ jsx2("input", { ...getInputProps() }),
96
+ children
97
+ ]
98
+ }
99
+ ) });
100
+ };
101
+ var DropzoneContent = ({ className }) => {
102
+ const {
103
+ files,
104
+ setFiles,
105
+ onUpload,
106
+ loading,
107
+ successes,
108
+ errors,
109
+ maxFileSize,
110
+ maxFiles,
111
+ isSuccess
112
+ } = useDropzoneContext();
113
+ const exceedMaxFiles = files.length > maxFiles;
114
+ const handleRemoveFile = useCallback(
115
+ (fileName) => {
116
+ setFiles(files.filter((file) => file.name !== fileName));
117
+ },
118
+ [files, setFiles]
119
+ );
120
+ if (isSuccess) {
121
+ return /* @__PURE__ */ jsxs(
122
+ "div",
123
+ {
124
+ className: cn2(
125
+ "flex flex-row items-center gap-x-2 justify-center",
126
+ className
127
+ ),
128
+ children: [
129
+ /* @__PURE__ */ jsx2(CheckCircle, { size: 16, className: "text-primary" }),
130
+ /* @__PURE__ */ jsxs("p", { className: "text-primary text-sm", children: [
131
+ "Successfully uploaded ",
132
+ files.length,
133
+ " file",
134
+ files.length > 1 ? "s" : ""
135
+ ] })
136
+ ]
137
+ }
138
+ );
139
+ }
140
+ return /* @__PURE__ */ jsxs("div", { className: cn2("flex flex-col", className), children: [
141
+ files.map((file, idx) => {
142
+ const fileError = errors.find((e) => e.name === file.name);
143
+ const isSuccessfullyUploaded = !!successes.find((e) => e === file.name);
144
+ return /* @__PURE__ */ jsxs(
145
+ "div",
146
+ {
147
+ className: "flex items-center gap-x-4 border-b py-2 first:mt-4 last:mb-4 ",
148
+ children: [
149
+ file.type.startsWith("image/") ? /* @__PURE__ */ jsx2("div", { className: "h-10 w-10 rounded border overflow-hidden shrink-0 bg-muted flex items-center justify-center", children: /* @__PURE__ */ jsx2(
150
+ "img",
151
+ {
152
+ src: file.preview,
153
+ alt: file.name,
154
+ className: "object-cover"
155
+ }
156
+ ) }) : /* @__PURE__ */ jsx2("div", { className: "h-10 w-10 rounded border bg-muted flex items-center justify-center", children: /* @__PURE__ */ jsx2(File, { size: 18 }) }),
157
+ /* @__PURE__ */ jsxs("div", { className: "shrink grow flex flex-col items-start truncate", children: [
158
+ /* @__PURE__ */ jsx2("p", { title: file.name, className: "text-sm truncate max-w-full", children: file.name }),
159
+ file.errors.length > 0 ? /* @__PURE__ */ jsx2("p", { className: "text-xs text-destructive", children: file.errors.map(
160
+ (e) => e.message.startsWith("File is larger than") ? `File is larger than ${formatBytes(maxFileSize, 2)} (Size: ${formatBytes(file.size, 2)})` : e.message
161
+ ).join(", ") }) : loading && !isSuccessfullyUploaded ? /* @__PURE__ */ jsx2("p", { className: "text-xs text-muted-foreground", children: "Uploading file..." }) : !!fileError ? /* @__PURE__ */ jsxs("p", { className: "text-xs text-destructive", children: [
162
+ "Failed to upload: ",
163
+ fileError.message
164
+ ] }) : isSuccessfullyUploaded ? /* @__PURE__ */ jsx2("p", { className: "text-xs text-primary", children: "Successfully uploaded file" }) : /* @__PURE__ */ jsx2("p", { className: "text-xs text-muted-foreground", children: formatBytes(file.size, 2) })
165
+ ] }),
166
+ !loading && !isSuccessfullyUploaded && /* @__PURE__ */ jsx2(
167
+ Button,
168
+ {
169
+ size: "icon",
170
+ variant: "link",
171
+ className: "shrink-0 justify-self-end text-muted-foreground hover:text-foreground",
172
+ onClick: () => handleRemoveFile(file.name),
173
+ children: /* @__PURE__ */ jsx2(X, {})
174
+ }
175
+ )
176
+ ]
177
+ },
178
+ `${file.name}-${idx}`
179
+ );
180
+ }),
181
+ exceedMaxFiles && /* @__PURE__ */ jsxs("p", { className: "text-sm text-left mt-2 text-destructive", children: [
182
+ "You may upload only up to ",
183
+ maxFiles,
184
+ " files, please remove",
185
+ " ",
186
+ files.length - maxFiles,
187
+ " file",
188
+ files.length - maxFiles > 1 ? "s" : "",
189
+ "."
190
+ ] }),
191
+ files.length > 0 && !exceedMaxFiles && /* @__PURE__ */ jsx2("div", { className: "mt-2", children: /* @__PURE__ */ jsx2(
192
+ Button,
193
+ {
194
+ variant: "outline",
195
+ onClick: onUpload,
196
+ disabled: files.some((file) => file.errors.length !== 0) || loading,
197
+ children: loading ? /* @__PURE__ */ jsxs(Fragment, { children: [
198
+ /* @__PURE__ */ jsx2(Loader2, { className: "mr-2 h-4 w-4 animate-spin" }),
199
+ "Uploading..."
200
+ ] }) : /* @__PURE__ */ jsx2(Fragment, { children: "Upload files" })
201
+ }
202
+ ) })
203
+ ] });
204
+ };
205
+ var DropzoneEmptyState = ({ className }) => {
206
+ const { maxFiles, maxFileSize, inputRef, isSuccess } = useDropzoneContext();
207
+ if (isSuccess) {
208
+ return null;
209
+ }
210
+ return /* @__PURE__ */ jsxs("div", { className: cn2("flex flex-col items-center gap-y-2", className), children: [
211
+ /* @__PURE__ */ jsx2(Upload, { size: 20, className: "text-muted-foreground" }),
212
+ /* @__PURE__ */ jsxs("p", { className: "text-sm", children: [
213
+ "Upload",
214
+ !!maxFiles && maxFiles > 1 ? ` ${maxFiles}` : "",
215
+ " file",
216
+ !maxFiles || maxFiles > 1 ? "s" : ""
217
+ ] }),
218
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-col items-center gap-y-1", children: [
219
+ /* @__PURE__ */ jsxs("p", { className: "text-xs text-muted-foreground", children: [
220
+ "Drag and drop or",
221
+ " ",
222
+ /* @__PURE__ */ jsxs(
223
+ "a",
224
+ {
225
+ onClick: () => inputRef.current?.click(),
226
+ className: "underline cursor-pointer transition hover:text-foreground",
227
+ children: [
228
+ "select ",
229
+ maxFiles === 1 ? `file` : "files"
230
+ ]
231
+ }
232
+ ),
233
+ " ",
234
+ "to upload"
235
+ ] }),
236
+ maxFileSize !== Number.POSITIVE_INFINITY && /* @__PURE__ */ jsxs("p", { className: "text-xs text-muted-foreground", children: [
237
+ "Maximum file size: ",
238
+ formatBytes(maxFileSize, 2)
239
+ ] })
240
+ ] })
241
+ ] });
242
+ };
243
+ var useDropzoneContext = () => {
244
+ const context = useContext(DropzoneContext);
245
+ if (!context) {
246
+ throw new Error("useDropzoneContext must be used within a Dropzone");
247
+ }
248
+ return context;
249
+ };
250
+ export {
251
+ Dropzone,
252
+ DropzoneContent,
253
+ DropzoneEmptyState,
254
+ formatBytes,
255
+ useDropzoneContext
256
+ };