doom-design-system 0.3.3 → 0.3.4
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 +10 -6
- package/dist/components/Badge/Badge.d.ts +2 -2
- package/dist/components/Badge/Badge.js +4 -4
- package/dist/components/Badge/Badge.module.css +6 -0
- package/dist/components/Button/Button.d.ts +3 -3
- package/dist/components/Button/Button.js +6 -6
- package/dist/components/Button/Button.module.css +30 -6
- package/dist/components/Drawer/Drawer.js +1 -1
- package/dist/components/Drawer/Drawer.module.css +0 -22
- package/dist/components/FileUpload/FileUpload.d.ts +32 -0
- package/dist/components/FileUpload/FileUpload.js +238 -0
- package/dist/components/FileUpload/FileUpload.module.css +335 -0
- package/dist/components/FileUpload/index.d.ts +2 -0
- package/dist/components/FileUpload/index.js +1 -0
- package/dist/components/Layout/Layout.d.ts +12 -8
- package/dist/components/Layout/Layout.js +13 -11
- package/dist/components/Layout/Layout.module.css +29 -0
- package/dist/components/Link/Link.module.css +12 -5
- package/dist/components/Modal/Modal.js +1 -1
- package/dist/components/Modal/Modal.module.css +0 -36
- package/dist/components/Sheet/Sheet.js +1 -1
- package/dist/components/Sheet/Sheet.module.css +0 -22
- package/dist/components/Slat/Slat.d.ts +14 -0
- package/dist/components/Slat/Slat.js +27 -0
- package/dist/components/Slat/Slat.module.css +101 -0
- package/dist/components/Slat/index.d.ts +2 -0
- package/dist/components/Slat/index.js +1 -0
- package/dist/index.d.ts +40 -38
- package/dist/index.js +40 -38
- package/dist/styles/globals.css +238 -0
- package/dist/styles/themes/definitions.d.ts +3 -3
- package/dist/styles/themes/definitions.js +3 -3
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# Doom Design System
|
|
2
2
|
|
|
3
|
+
[](https://github.com/ornelasEduardo/doom/actions/workflows/ci.yml)
|
|
4
|
+
[](https://www.npmjs.com/package/doom-design-system)
|
|
5
|
+
[](https://opensource.org/licenses/MIT)
|
|
6
|
+
|
|
3
7
|
A modern, premium, neubrutalist and comic book inspired design system built with React and SASS Modules.
|
|
4
8
|
|
|
5
9
|
## Features
|
|
@@ -34,11 +38,11 @@ npm install lucide-react
|
|
|
34
38
|
Wrap your application with the `DesignSystemProvider` to ensure all styles and themes are applied correctly. It injects the necessary global CSS and theme variables.
|
|
35
39
|
|
|
36
40
|
```tsx
|
|
37
|
-
import { DesignSystemProvider } from
|
|
38
|
-
import { Montserrat } from
|
|
41
|
+
import { DesignSystemProvider } from "doom-design-system";
|
|
42
|
+
import { Montserrat } from "next/font/google";
|
|
39
43
|
|
|
40
44
|
// Optional: Use a custom google font
|
|
41
|
-
const montserrat = Montserrat({ subsets: [
|
|
45
|
+
const montserrat = Montserrat({ subsets: ["latin"] });
|
|
42
46
|
|
|
43
47
|
export default function RootLayout({ children }) {
|
|
44
48
|
return (
|
|
@@ -56,7 +60,7 @@ export default function RootLayout({ children }) {
|
|
|
56
60
|
Import and use components in your application. They are now fully tree-shakeable and lightweight.
|
|
57
61
|
|
|
58
62
|
```tsx
|
|
59
|
-
import { Button, Card, Text, Link } from
|
|
63
|
+
import { Button, Card, Text, Link } from "doom-design-system";
|
|
60
64
|
|
|
61
65
|
function MyComponent() {
|
|
62
66
|
return (
|
|
@@ -65,7 +69,7 @@ function MyComponent() {
|
|
|
65
69
|
<p>
|
|
66
70
|
Check out this <Link href="#">awesome link</Link>.
|
|
67
71
|
</p>
|
|
68
|
-
<Button variant="primary" onClick={() => alert(
|
|
72
|
+
<Button variant="primary" onClick={() => alert("Boom!")}>
|
|
69
73
|
Click Me
|
|
70
74
|
</Button>
|
|
71
75
|
</Card>
|
|
@@ -78,7 +82,7 @@ function MyComponent() {
|
|
|
78
82
|
The Design System uses CSS Variables for theming. You can control the theme using the `DesignSystemProvider`.
|
|
79
83
|
|
|
80
84
|
```tsx
|
|
81
|
-
<DesignSystemProvider initialTheme="doom"
|
|
85
|
+
<DesignSystemProvider initialTheme="doom">
|
|
82
86
|
{/* The entire app will be themed automatically */}
|
|
83
87
|
</DesignSystemProvider>
|
|
84
88
|
```
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import React from
|
|
2
|
-
type BadgeVariant =
|
|
1
|
+
import React from "react";
|
|
2
|
+
type BadgeVariant = "primary" | "success" | "warning" | "error" | "secondary" | "outline";
|
|
3
3
|
interface BadgeProps extends React.HTMLAttributes<HTMLSpanElement> {
|
|
4
4
|
variant?: BadgeVariant;
|
|
5
5
|
children: React.ReactNode;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
"use client";
|
|
2
2
|
var __rest = (this && this.__rest) || function (s, e) {
|
|
3
3
|
var t = {};
|
|
4
4
|
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
|
|
@@ -11,9 +11,9 @@ var __rest = (this && this.__rest) || function (s, e) {
|
|
|
11
11
|
return t;
|
|
12
12
|
};
|
|
13
13
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
14
|
-
import clsx from
|
|
15
|
-
import styles from
|
|
14
|
+
import clsx from "clsx";
|
|
15
|
+
import styles from "./Badge.module.css";
|
|
16
16
|
export function Badge(_a) {
|
|
17
|
-
var { variant =
|
|
17
|
+
var { variant = "primary", children, className } = _a, props = __rest(_a, ["variant", "children", "className"]);
|
|
18
18
|
return (_jsx("span", Object.assign({ className: clsx(styles.badge, styles[variant], className) }, props, { children: children })));
|
|
19
19
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import React from
|
|
2
|
-
type ButtonVariant =
|
|
3
|
-
type ButtonSize =
|
|
1
|
+
import React from "react";
|
|
2
|
+
type ButtonVariant = "primary" | "secondary" | "ghost" | "outline" | "success" | "danger";
|
|
3
|
+
type ButtonSize = "sm" | "md" | "lg";
|
|
4
4
|
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
|
5
5
|
variant?: ButtonVariant;
|
|
6
6
|
size?: ButtonSize;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
"use client";
|
|
2
2
|
var __rest = (this && this.__rest) || function (s, e) {
|
|
3
3
|
var t = {};
|
|
4
4
|
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
|
|
@@ -11,11 +11,11 @@ var __rest = (this && this.__rest) || function (s, e) {
|
|
|
11
11
|
return t;
|
|
12
12
|
};
|
|
13
13
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
14
|
-
import clsx from
|
|
15
|
-
import { Spinner } from
|
|
16
|
-
import styles from
|
|
14
|
+
import clsx from "clsx";
|
|
15
|
+
import { Spinner } from "../Spinner/index.js";
|
|
16
|
+
import styles from "./Button.module.css";
|
|
17
17
|
export function Button(_a) {
|
|
18
|
-
var { children, variant =
|
|
18
|
+
var { children, variant = "primary", size = "md", loading = false, className, disabled } = _a, props = __rest(_a, ["children", "variant", "size", "loading", "className", "disabled"]);
|
|
19
19
|
const buttonClass = clsx(styles.button, styles[variant], styles[size], loading && styles.loading, className);
|
|
20
|
-
return (_jsxs("button", Object.assign({ className: buttonClass, disabled: disabled || loading }, props, { children: [loading &&
|
|
20
|
+
return (_jsxs("button", Object.assign({ className: buttonClass, disabled: disabled || loading }, props, { children: [loading && _jsx(Spinner, { size: "sm", className: styles.spinnerIcon }), children] })));
|
|
21
21
|
}
|
|
@@ -34,7 +34,14 @@
|
|
|
34
34
|
}
|
|
35
35
|
.button:disabled {
|
|
36
36
|
opacity: 0.6;
|
|
37
|
-
cursor: not-allowed;
|
|
37
|
+
cursor: not-allowed !important;
|
|
38
|
+
background-image: repeating-linear-gradient(45deg, transparent, transparent 10px, rgba(0, 0, 0, 0.05) 10px, rgba(0, 0, 0, 0.05) 20px) !important;
|
|
39
|
+
}
|
|
40
|
+
.button:disabled:hover {
|
|
41
|
+
transform: none !important;
|
|
42
|
+
filter: none !important;
|
|
43
|
+
}
|
|
44
|
+
.button:disabled {
|
|
38
45
|
transform: none;
|
|
39
46
|
box-shadow: var(--shadow-hard);
|
|
40
47
|
}
|
|
@@ -47,7 +54,7 @@
|
|
|
47
54
|
color: var(--primary-foreground);
|
|
48
55
|
border-color: var(--border-strong);
|
|
49
56
|
}
|
|
50
|
-
.primary:hover {
|
|
57
|
+
.primary:hover:not(:disabled) {
|
|
51
58
|
filter: brightness(1.1);
|
|
52
59
|
}
|
|
53
60
|
|
|
@@ -58,7 +65,7 @@
|
|
|
58
65
|
color: var(--secondary-foreground);
|
|
59
66
|
border-color: var(--border-strong);
|
|
60
67
|
}
|
|
61
|
-
.secondary:hover {
|
|
68
|
+
.secondary:hover:not(:disabled) {
|
|
62
69
|
filter: brightness(1.1);
|
|
63
70
|
}
|
|
64
71
|
|
|
@@ -69,12 +76,24 @@
|
|
|
69
76
|
color: var(--success-foreground);
|
|
70
77
|
border-color: var(--border-strong);
|
|
71
78
|
}
|
|
72
|
-
.success:hover {
|
|
79
|
+
.success:hover:not(:disabled) {
|
|
80
|
+
filter: brightness(1.1);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
.danger {
|
|
84
|
+
--btn-focus-border: var(--card-border);
|
|
85
|
+
--btn-focus-shadow: #000000;
|
|
86
|
+
background-color: var(--error);
|
|
87
|
+
color: var(--error-foreground);
|
|
88
|
+
border-color: var(--border-strong);
|
|
89
|
+
}
|
|
90
|
+
.danger:hover:not(:disabled) {
|
|
73
91
|
filter: brightness(1.1);
|
|
74
92
|
}
|
|
75
93
|
|
|
76
94
|
.outline {
|
|
77
95
|
background-color: transparent;
|
|
96
|
+
border-color: currentColor;
|
|
78
97
|
}
|
|
79
98
|
|
|
80
99
|
.ghost {
|
|
@@ -82,7 +101,7 @@
|
|
|
82
101
|
border-color: transparent;
|
|
83
102
|
box-shadow: none;
|
|
84
103
|
}
|
|
85
|
-
.ghost:hover, .ghost:focus-visible {
|
|
104
|
+
.ghost:hover:not(:disabled), .ghost:focus-visible:not(:disabled) {
|
|
86
105
|
background-color: color-mix(in srgb, var(--primary), transparent 90%);
|
|
87
106
|
color: var(--primary);
|
|
88
107
|
transform: none;
|
|
@@ -90,6 +109,9 @@
|
|
|
90
109
|
border-color: transparent;
|
|
91
110
|
outline: none;
|
|
92
111
|
}
|
|
112
|
+
.ghost:disabled {
|
|
113
|
+
box-shadow: none;
|
|
114
|
+
}
|
|
93
115
|
.ghost:active {
|
|
94
116
|
transform: scale(0.95);
|
|
95
117
|
transition: none;
|
|
@@ -97,8 +119,10 @@
|
|
|
97
119
|
|
|
98
120
|
/* Sizes */
|
|
99
121
|
.sm {
|
|
100
|
-
padding: 0.25rem
|
|
122
|
+
padding: 0.25rem;
|
|
101
123
|
font-size: var(--text-sm);
|
|
124
|
+
min-width: 24px;
|
|
125
|
+
justify-content: center;
|
|
102
126
|
}
|
|
103
127
|
|
|
104
128
|
.md {
|
|
@@ -33,5 +33,5 @@ export function Drawer({ isOpen, onClose, title, side = "right", children, foote
|
|
|
33
33
|
}, [isOpen, onClose]);
|
|
34
34
|
if (!mounted)
|
|
35
35
|
return null;
|
|
36
|
-
return createPortal(_jsxs(_Fragment, { children: [_jsx("div", { className: clsx(styles.overlay, isOpen && styles.isOpen), onClick: onClose, "aria-hidden": "true" }), _jsxs("div", { className: clsx(styles.panel, styles[side], styles[variant], isOpen && styles.isOpen, className), role: "dialog", "aria-modal": "true", "aria-labelledby": title ? titleId : undefined, "aria-label": !title ? "Drawer" : undefined, children: [_jsxs("div", { className: styles.header, children: [title && (_jsx("h2", { id: titleId, className: styles.title, children: title })), _jsx(Button, { variant: "
|
|
36
|
+
return createPortal(_jsxs(_Fragment, { children: [_jsx("div", { className: clsx(styles.overlay, isOpen && styles.isOpen), onClick: onClose, "aria-hidden": "true" }), _jsxs("div", { className: clsx(styles.panel, styles[side], styles[variant], isOpen && styles.isOpen, className), role: "dialog", "aria-modal": "true", "aria-labelledby": title ? titleId : undefined, "aria-label": !title ? "Drawer" : undefined, children: [_jsxs("div", { className: styles.header, children: [title && (_jsx("h2", { id: titleId, className: styles.title, children: title })), _jsx(Button, { variant: "danger", size: "sm", onClick: onClose, "aria-label": "Close drawer", children: _jsx(X, { size: 24 }) })] }), _jsx("div", { className: styles.content, children: children }), footer && _jsx("div", { className: styles.footer, children: footer })] })] }), document.body);
|
|
37
37
|
}
|
|
@@ -76,11 +76,6 @@
|
|
|
76
76
|
background-color: transparent !important;
|
|
77
77
|
color: var(--solid-fg);
|
|
78
78
|
}
|
|
79
|
-
.panel.solid .header button {
|
|
80
|
-
background-color: var(--error) !important;
|
|
81
|
-
color: var(--error-foreground) !important;
|
|
82
|
-
border-color: var(--border-strong) !important;
|
|
83
|
-
}
|
|
84
79
|
.panel.solid .content {
|
|
85
80
|
background-color: transparent !important;
|
|
86
81
|
}
|
|
@@ -97,23 +92,6 @@
|
|
|
97
92
|
justify-content: space-between;
|
|
98
93
|
align-items: center;
|
|
99
94
|
flex-shrink: 0;
|
|
100
|
-
/* Match Modal's Close Button Styling */
|
|
101
|
-
}
|
|
102
|
-
.header button {
|
|
103
|
-
background-color: var(--error) !important;
|
|
104
|
-
color: var(--error-foreground) !important;
|
|
105
|
-
border: var(--border-width) solid var(--border-strong) !important;
|
|
106
|
-
box-shadow: var(--shadow-sm) !important;
|
|
107
|
-
}
|
|
108
|
-
.header button:hover {
|
|
109
|
-
background-color: var(--error) !important;
|
|
110
|
-
filter: brightness(1.1);
|
|
111
|
-
transform: translate(-2px, -2px);
|
|
112
|
-
box-shadow: var(--shadow-sm-hover) !important;
|
|
113
|
-
}
|
|
114
|
-
.header button:active {
|
|
115
|
-
transform: translate(2px, 2px);
|
|
116
|
-
box-shadow: var(--shadow-sm-checked) !important;
|
|
117
95
|
}
|
|
118
96
|
|
|
119
97
|
.title {
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
export interface FileUploadProps {
|
|
3
|
+
/** Label for the file upload */
|
|
4
|
+
label?: string;
|
|
5
|
+
/** Helper text */
|
|
6
|
+
helperText?: string;
|
|
7
|
+
/** Accepted file types (e.g., "image/*", ".pdf,.doc") */
|
|
8
|
+
accept?: string;
|
|
9
|
+
/** Maximum file size in bytes */
|
|
10
|
+
maxSize?: number;
|
|
11
|
+
/** Allow multiple files */
|
|
12
|
+
multiple?: boolean;
|
|
13
|
+
/** Initial files to show */
|
|
14
|
+
defaultFiles?: File[];
|
|
15
|
+
/** Disabled state */
|
|
16
|
+
disabled?: boolean;
|
|
17
|
+
/** Error state */
|
|
18
|
+
error?: boolean;
|
|
19
|
+
/** Error message */
|
|
20
|
+
errorMessage?: string;
|
|
21
|
+
/** Required field */
|
|
22
|
+
required?: boolean;
|
|
23
|
+
/** Show image previews for supported file types */
|
|
24
|
+
showPreview?: boolean;
|
|
25
|
+
/** Callback when files are selected */
|
|
26
|
+
onChange?: (files: File[]) => void;
|
|
27
|
+
/** Custom class name */
|
|
28
|
+
className?: string;
|
|
29
|
+
/** Force active/void state for debugging */
|
|
30
|
+
forceActive?: boolean;
|
|
31
|
+
}
|
|
32
|
+
export declare const FileUpload: React.FC<FileUploadProps>;
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useRef, useState, useCallback, useMemo, useEffect, } from "react";
|
|
4
|
+
import styles from "./FileUpload.module.css";
|
|
5
|
+
import { Button } from "../Button/Button.js";
|
|
6
|
+
import { Text } from "../Text/Text.js";
|
|
7
|
+
import { Stack, Flex, Switcher } from "../Layout/Layout.js";
|
|
8
|
+
import { Badge } from "../Badge/Badge.js";
|
|
9
|
+
import { Slat } from "../Slat/Slat.js";
|
|
10
|
+
import { Upload, File as FileIcon, FileImage, FileVideo, FileAudio, FileCode, FileArchive, FileText, FileSpreadsheet, Asterisk, X, } from "lucide-react";
|
|
11
|
+
import { Skeleton } from "../Skeleton/Skeleton.js";
|
|
12
|
+
import clsx from "clsx";
|
|
13
|
+
var Status;
|
|
14
|
+
(function (Status) {
|
|
15
|
+
Status["Disabled"] = "disabled";
|
|
16
|
+
Status["Error"] = "error";
|
|
17
|
+
Status["Void"] = "void";
|
|
18
|
+
Status["HasFiles"] = "hasFiles";
|
|
19
|
+
Status["Idle"] = "idle";
|
|
20
|
+
})(Status || (Status = {}));
|
|
21
|
+
export const FileUpload = ({ label, helperText, accept, maxSize, multiple = false, defaultFiles = [], disabled = false, error = false, errorMessage, required = false, showPreview = false, onChange, className = "", forceActive = false, }) => {
|
|
22
|
+
const [files, setFiles] = useState(defaultFiles);
|
|
23
|
+
const [isDragging, setIsDragging] = useState(false);
|
|
24
|
+
const [isActive, setIsActive] = useState(false);
|
|
25
|
+
const [uploadError, setUploadError] = useState("");
|
|
26
|
+
const [previews, setPreviews] = useState({});
|
|
27
|
+
const inputRef = useRef(null);
|
|
28
|
+
const previewsRef = useRef({});
|
|
29
|
+
// Keep ref in sync for unmount cleanup
|
|
30
|
+
useEffect(() => {
|
|
31
|
+
previewsRef.current = previews;
|
|
32
|
+
}, [previews]);
|
|
33
|
+
// Clean up object URLs on unmount
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
return () => {
|
|
36
|
+
Object.values(previewsRef.current).forEach((url) => URL.revokeObjectURL(url));
|
|
37
|
+
};
|
|
38
|
+
}, []);
|
|
39
|
+
const getFileIcon = (fileName) => {
|
|
40
|
+
var _a;
|
|
41
|
+
const ext = (_a = fileName.split(".").pop()) === null || _a === void 0 ? void 0 : _a.toLowerCase();
|
|
42
|
+
const size = 20;
|
|
43
|
+
if (["jpg", "jpeg", "png", "gif", "svg", "webp"].includes(ext || ""))
|
|
44
|
+
return _jsx(FileImage, { size: size });
|
|
45
|
+
if (["mp4", "mov", "avi", "mkv", "webm", "flv"].includes(ext || ""))
|
|
46
|
+
return _jsx(FileVideo, { size: size });
|
|
47
|
+
if (["mp3", "wav", "ogg", "m4a", "flac"].includes(ext || ""))
|
|
48
|
+
return _jsx(FileAudio, { size: size });
|
|
49
|
+
if ([
|
|
50
|
+
"js",
|
|
51
|
+
"ts",
|
|
52
|
+
"jsx",
|
|
53
|
+
"tsx",
|
|
54
|
+
"html",
|
|
55
|
+
"css",
|
|
56
|
+
"json",
|
|
57
|
+
"py",
|
|
58
|
+
"java",
|
|
59
|
+
"cpp",
|
|
60
|
+
"c",
|
|
61
|
+
"go",
|
|
62
|
+
].includes(ext || ""))
|
|
63
|
+
return _jsx(FileCode, { size: size });
|
|
64
|
+
if (["zip", "rar", "7z", "tar", "gz"].includes(ext || ""))
|
|
65
|
+
return _jsx(FileArchive, { size: size });
|
|
66
|
+
if (["pdf"].includes(ext || ""))
|
|
67
|
+
return _jsx(FileText, { size: size });
|
|
68
|
+
if (["xls", "xlsx", "csv", "ods"].includes(ext || ""))
|
|
69
|
+
return _jsx(FileSpreadsheet, { size: size });
|
|
70
|
+
return _jsx(FileIcon, { size: size });
|
|
71
|
+
};
|
|
72
|
+
const formatFileSize = (bytes) => {
|
|
73
|
+
if (bytes === 0)
|
|
74
|
+
return "0 Bytes";
|
|
75
|
+
const k = 1024;
|
|
76
|
+
const sizes = ["Bytes", "KB", "MB", "GB"];
|
|
77
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
78
|
+
return Math.round((bytes / Math.pow(k, i)) * 100) / 100 + " " + sizes[i];
|
|
79
|
+
};
|
|
80
|
+
const generatePreview = useCallback((file) => {
|
|
81
|
+
if (showPreview && file.type.startsWith("image/")) {
|
|
82
|
+
const url = URL.createObjectURL(file);
|
|
83
|
+
setPreviews((prev) => (Object.assign(Object.assign({}, prev), { [file.name]: url })));
|
|
84
|
+
}
|
|
85
|
+
}, [showPreview]);
|
|
86
|
+
const validateFiles = (fileList) => {
|
|
87
|
+
if (!fileList)
|
|
88
|
+
return [];
|
|
89
|
+
const validFiles = [];
|
|
90
|
+
setUploadError("");
|
|
91
|
+
Array.from(fileList).forEach((file) => {
|
|
92
|
+
if (maxSize && file.size > maxSize) {
|
|
93
|
+
setUploadError(`File "${file.name}" exceeds maximum size of ${formatFileSize(maxSize)}`);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
validFiles.push(file);
|
|
97
|
+
generatePreview(file);
|
|
98
|
+
});
|
|
99
|
+
return validFiles;
|
|
100
|
+
};
|
|
101
|
+
const handleFileChange = (event) => {
|
|
102
|
+
const validFiles = validateFiles(event.target.files);
|
|
103
|
+
if (validFiles.length > 0) {
|
|
104
|
+
const newFiles = multiple ? [...files, ...validFiles] : validFiles;
|
|
105
|
+
setFiles(newFiles);
|
|
106
|
+
onChange === null || onChange === void 0 ? void 0 : onChange(newFiles);
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
const handleDragHandlers = {
|
|
110
|
+
onDragEnter: (e) => {
|
|
111
|
+
e.preventDefault();
|
|
112
|
+
e.stopPropagation();
|
|
113
|
+
if (!disabled)
|
|
114
|
+
setIsDragging(true);
|
|
115
|
+
},
|
|
116
|
+
onDragLeave: (e) => {
|
|
117
|
+
e.preventDefault();
|
|
118
|
+
e.stopPropagation();
|
|
119
|
+
// Check if we're moving to a child element
|
|
120
|
+
if (e.currentTarget.contains(e.relatedTarget)) {
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
setIsDragging(false);
|
|
124
|
+
},
|
|
125
|
+
onDragOver: (e) => {
|
|
126
|
+
e.preventDefault();
|
|
127
|
+
e.stopPropagation();
|
|
128
|
+
},
|
|
129
|
+
onDrop: (e) => {
|
|
130
|
+
e.preventDefault();
|
|
131
|
+
e.stopPropagation();
|
|
132
|
+
setIsDragging(false);
|
|
133
|
+
if (disabled)
|
|
134
|
+
return;
|
|
135
|
+
const validFiles = validateFiles(e.dataTransfer.files);
|
|
136
|
+
if (validFiles.length > 0) {
|
|
137
|
+
const newFiles = multiple ? [...files, ...validFiles] : validFiles;
|
|
138
|
+
setFiles(newFiles);
|
|
139
|
+
onChange === null || onChange === void 0 ? void 0 : onChange(newFiles);
|
|
140
|
+
}
|
|
141
|
+
},
|
|
142
|
+
};
|
|
143
|
+
const handleRemoveFile = (index) => {
|
|
144
|
+
const fileToRemove = files[index];
|
|
145
|
+
if (fileToRemove) {
|
|
146
|
+
const key = fileToRemove.name;
|
|
147
|
+
if (previews[key]) {
|
|
148
|
+
URL.revokeObjectURL(previews[key]);
|
|
149
|
+
const newPreviews = Object.assign({}, previews);
|
|
150
|
+
delete newPreviews[key];
|
|
151
|
+
setPreviews(newPreviews);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
const newFiles = files.filter((_, i) => i !== index);
|
|
155
|
+
setFiles(newFiles);
|
|
156
|
+
onChange === null || onChange === void 0 ? void 0 : onChange(newFiles);
|
|
157
|
+
if (inputRef.current)
|
|
158
|
+
inputRef.current.value = "";
|
|
159
|
+
};
|
|
160
|
+
const handleClick = () => {
|
|
161
|
+
var _a;
|
|
162
|
+
if (!disabled) {
|
|
163
|
+
setIsActive(true);
|
|
164
|
+
(_a = inputRef.current) === null || _a === void 0 ? void 0 : _a.click();
|
|
165
|
+
const handleFocus = () => {
|
|
166
|
+
setIsActive(false);
|
|
167
|
+
window.removeEventListener("focus", handleFocus);
|
|
168
|
+
};
|
|
169
|
+
window.addEventListener("focus", handleFocus);
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
const displayError = error && errorMessage ? errorMessage : uploadError;
|
|
173
|
+
const hasFiles = files.length > 0;
|
|
174
|
+
const isVoid = (isDragging || isActive || forceActive) && !hasFiles;
|
|
175
|
+
const status = useMemo(() => {
|
|
176
|
+
if (disabled)
|
|
177
|
+
return Status.Disabled;
|
|
178
|
+
if (displayError)
|
|
179
|
+
return Status.Error;
|
|
180
|
+
if (isVoid)
|
|
181
|
+
return Status.Void;
|
|
182
|
+
if (hasFiles)
|
|
183
|
+
return Status.HasFiles;
|
|
184
|
+
return Status.Idle;
|
|
185
|
+
}, [disabled, displayError, isVoid, hasFiles]);
|
|
186
|
+
return (_jsxs("div", Object.assign({ className: clsx(styles.window, className, {
|
|
187
|
+
[styles.void]: isVoid,
|
|
188
|
+
[styles.disabled]: disabled,
|
|
189
|
+
}) }, handleDragHandlers, { children: [_jsxs(Switcher, { threshold: "xxs", className: clsx(styles.header, {
|
|
190
|
+
[styles.errorHeader]: status === Status.Error,
|
|
191
|
+
[styles.disabledHeader]: disabled,
|
|
192
|
+
}), justify: "space-between", align: "center", gap: "var(--spacing-sm)", children: [_jsx("div", { className: styles.headerContent, children: _jsxs(Text, { className: styles.headerTitle, id: "upload-title", children: [label || "Upload Files", required && (_jsx("span", { className: styles.requiredMark, children: _jsx(Asterisk, { size: 14, "aria-label": "required" }) }))] }) }), _jsx(Badge, { variant: status === Status.Error
|
|
193
|
+
? "error"
|
|
194
|
+
: status === Status.HasFiles
|
|
195
|
+
? "success"
|
|
196
|
+
: isVoid
|
|
197
|
+
? "primary"
|
|
198
|
+
: "secondary", role: "status", "aria-live": "polite", children: status === Status.Error
|
|
199
|
+
? "Error"
|
|
200
|
+
: status === Status.HasFiles
|
|
201
|
+
? `${files.length} selected`
|
|
202
|
+
: isVoid
|
|
203
|
+
? "Waiting..."
|
|
204
|
+
: disabled
|
|
205
|
+
? "Disabled"
|
|
206
|
+
: "Ready" })] }), _jsxs("div", { className: clsx(styles.body, {
|
|
207
|
+
[styles.hasFiles]: hasFiles,
|
|
208
|
+
[styles.dragging]: isDragging || forceActive,
|
|
209
|
+
[styles.disabled]: disabled,
|
|
210
|
+
[styles.error]: !!displayError,
|
|
211
|
+
}), children: [_jsx("div", { className: styles.starField, "aria-hidden": "true" }), _jsx("input", { ref: inputRef, type: "file", accept: accept, multiple: multiple, onChange: handleFileChange, disabled: disabled, className: styles.hiddenInput, "aria-label": "File upload input", "aria-required": required, "aria-invalid": !!displayError, "aria-describedby": clsx({
|
|
212
|
+
"upload-helper": !!helperText,
|
|
213
|
+
"upload-error": !!displayError,
|
|
214
|
+
}) }), !hasFiles ? (_jsx("div", { className: styles.emptyState, onClick: handleClick, onKeyDown: (e) => {
|
|
215
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
216
|
+
e.preventDefault();
|
|
217
|
+
handleClick();
|
|
218
|
+
}
|
|
219
|
+
}, role: "button", tabIndex: disabled ? -1 : 0, children: _jsxs(Stack, { gap: "var(--spacing-md)", align: "center", children: [_jsx("div", { className: styles.voidIconWrapper, "aria-hidden": "true", children: _jsx(Upload, { className: styles.icon, size: 48, strokeWidth: 2 }) }), _jsxs(Stack, { gap: "var(--spacing-xs)", align: "center", children: [_jsx(Text, { weight: "bold", className: styles.voidText, children: isDragging || isActive || forceActive
|
|
220
|
+
? "Drop files now"
|
|
221
|
+
: "Drag and drop files" }), _jsx(Text, { variant: "small", className: styles.secondary, children: "or click to browse" })] }), helperText && (_jsx(Text, { variant: "small", className: styles.helperText, id: "upload-helper", children: helperText })), displayError && (_jsx(Text, { variant: "small", className: styles.errorText, id: "upload-error", children: displayError }))] }) })) : (_jsxs(Flex, { direction: "column", justify: "space-between", gap: "var(--spacing-md)", className: styles.fileList, children: [_jsx(Stack, { gap: "var(--spacing-sm)", children: files.map((file, index) => {
|
|
222
|
+
const previewUrl = previews[file.name];
|
|
223
|
+
return (_jsx(Slat, { label: file.name, secondaryLabel: formatFileSize(file.size), prependContent: previewUrl ? (_jsx(PreviewImage, { src: previewUrl, alt: file.name })) : (getFileIcon(file.name)), appendContent: _jsx(Button, { type: "button", variant: "danger", size: "sm", onClick: (e) => {
|
|
224
|
+
e.stopPropagation();
|
|
225
|
+
handleRemoveFile(index);
|
|
226
|
+
}, className: styles.removeButton, "aria-label": `Remove ${file.name}`, children: _jsx(X, { size: 16, "aria-hidden": "true" }) }) }, `${file.name}-${index}`));
|
|
227
|
+
}) }), multiple && (_jsx(Button, { variant: "secondary", onClick: (e) => {
|
|
228
|
+
e.preventDefault();
|
|
229
|
+
e.stopPropagation();
|
|
230
|
+
handleClick();
|
|
231
|
+
}, children: "UPLOAD MORE FILES" }))] }))] })] })));
|
|
232
|
+
};
|
|
233
|
+
const PreviewImage = ({ src, alt }) => {
|
|
234
|
+
const [loaded, setLoaded] = useState(false);
|
|
235
|
+
return (_jsxs("div", { className: styles.previewWrapper, children: [!loaded && (_jsx(Skeleton, { width: "100%", height: "100%", className: styles.previewSkeleton })), _jsx("img", { src: src, alt: alt
|
|
236
|
+
.replace(/\.(jpg|jpeg|png|gif|webp|svg)$/i, "")
|
|
237
|
+
.replace(/[-_]/g, " ") || "File preview", className: clsx(styles.previewImg, { [styles.loaded]: loaded }), onLoad: () => setLoaded(true), style: loaded ? {} : { position: "absolute", opacity: 0 } })] }));
|
|
238
|
+
};
|