doom-design-system 0.3.3 → 0.4.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.
Files changed (42) hide show
  1. package/README.md +10 -6
  2. package/dist/components/Avatar/Avatar.js +2 -1
  3. package/dist/components/Badge/Badge.d.ts +5 -3
  4. package/dist/components/Badge/Badge.js +5 -5
  5. package/dist/components/Badge/Badge.module.css +20 -2
  6. package/dist/components/Button/Button.d.ts +3 -3
  7. package/dist/components/Button/Button.js +6 -6
  8. package/dist/components/Button/Button.module.css +30 -6
  9. package/dist/components/Drawer/Drawer.js +1 -1
  10. package/dist/components/Drawer/Drawer.module.css +0 -22
  11. package/dist/components/FileUpload/FileUpload.d.ts +32 -0
  12. package/dist/components/FileUpload/FileUpload.js +237 -0
  13. package/dist/components/FileUpload/FileUpload.module.css +330 -0
  14. package/dist/components/FileUpload/index.d.ts +2 -0
  15. package/dist/components/FileUpload/index.js +1 -0
  16. package/dist/components/Image/Image.d.ts +8 -0
  17. package/dist/components/Image/Image.js +60 -0
  18. package/dist/components/Image/Image.module.css +36 -0
  19. package/dist/components/Image/index.d.ts +1 -0
  20. package/dist/components/Image/index.js +1 -0
  21. package/dist/components/Layout/Layout.d.ts +12 -8
  22. package/dist/components/Layout/Layout.js +13 -11
  23. package/dist/components/Layout/Layout.module.css +29 -0
  24. package/dist/components/Link/Link.d.ts +6 -3
  25. package/dist/components/Link/Link.js +27 -6
  26. package/dist/components/Link/Link.module.css +22 -6
  27. package/dist/components/Modal/Modal.js +1 -1
  28. package/dist/components/Modal/Modal.module.css +0 -36
  29. package/dist/components/Sheet/Sheet.js +1 -1
  30. package/dist/components/Sheet/Sheet.module.css +0 -22
  31. package/dist/components/Slat/Slat.d.ts +14 -0
  32. package/dist/components/Slat/Slat.js +27 -0
  33. package/dist/components/Slat/Slat.module.css +101 -0
  34. package/dist/components/Slat/index.d.ts +2 -0
  35. package/dist/components/Slat/index.js +1 -0
  36. package/dist/index.d.ts +41 -38
  37. package/dist/index.js +41 -38
  38. package/dist/styles/globals.css +238 -0
  39. package/dist/styles/themes/definitions.d.ts +3 -3
  40. package/dist/styles/themes/definitions.js +3 -3
  41. package/dist/tsconfig.build.tsbuildinfo +1 -1
  42. package/package.json +1 -1
package/README.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # Doom Design System
2
2
 
3
+ [![Doom CI](https://github.com/ornelasEduardo/doom/actions/workflows/ci.yml/badge.svg)](https://github.com/ornelasEduardo/doom/actions/workflows/ci.yml)
4
+ [![npm version](https://img.shields.io/npm/v/doom-design-system.svg)](https://www.npmjs.com/package/doom-design-system)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](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 'doom-design-system';
38
- import { Montserrat } from 'next/font/google';
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: ['latin'] });
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 'doom-design-system';
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('Boom!')}>
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
  ```
@@ -3,7 +3,8 @@ import { jsx as _jsx } from "react/jsx-runtime";
3
3
  import { useState } from "react";
4
4
  import clsx from "clsx";
5
5
  import styles from "./Avatar.module.css";
6
+ import { Image } from "../Image/Image.js";
6
7
  export function Avatar({ src, alt = "Avatar", fallback, size = "md", shape = "square", className, }) {
7
8
  const [hasError, setHasError] = useState(false);
8
- return (_jsx("div", { className: clsx(styles.avatar, styles[size], styles[shape], className), children: src && !hasError ? (_jsx("img", { src: src, alt: alt, className: styles.image, onError: () => setHasError(true) })) : (_jsx("span", { className: clsx(styles.fallback, styles[size]), children: typeof fallback === "string" ? fallback.slice(0, 2) : fallback })) }));
9
+ return (_jsx("div", { className: clsx(styles.avatar, styles[size], styles[shape], className), children: src && !hasError ? (_jsx(Image, { src: src, alt: alt, className: styles.image, onError: () => setHasError(true), fit: "cover", rounded: false })) : (_jsx("span", { className: clsx(styles.fallback, styles[size]), children: typeof fallback === "string" ? fallback.slice(0, 2) : fallback })) }));
9
10
  }
@@ -1,8 +1,10 @@
1
- import React from 'react';
2
- type BadgeVariant = 'primary' | 'success' | 'warning' | 'error' | 'secondary';
1
+ import React from "react";
2
+ type BadgeVariant = "primary" | "success" | "warning" | "error" | "secondary" | "outline";
3
+ type BadgeSize = "sm" | "md" | "lg";
3
4
  interface BadgeProps extends React.HTMLAttributes<HTMLSpanElement> {
4
5
  variant?: BadgeVariant;
6
+ size?: BadgeSize;
5
7
  children: React.ReactNode;
6
8
  }
7
- export declare function Badge({ variant, children, className, ...props }: BadgeProps): import("react/jsx-runtime").JSX.Element;
9
+ export declare function Badge({ variant, size, children, className, ...props }: BadgeProps): import("react/jsx-runtime").JSX.Element;
8
10
  export {};
@@ -1,4 +1,4 @@
1
- 'use client';
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 'clsx';
15
- import styles from './Badge.module.css';
14
+ import clsx from "clsx";
15
+ import styles from "./Badge.module.css";
16
16
  export function Badge(_a) {
17
- var { variant = 'primary', children, className } = _a, props = __rest(_a, ["variant", "children", "className"]);
18
- return (_jsx("span", Object.assign({ className: clsx(styles.badge, styles[variant], className) }, props, { children: children })));
17
+ var { variant = "primary", size = "md", children, className } = _a, props = __rest(_a, ["variant", "size", "children", "className"]);
18
+ return (_jsx("span", Object.assign({ className: clsx(styles.badge, styles[variant], styles[size], className) }, props, { children: children })));
19
19
  }
@@ -1,13 +1,25 @@
1
1
  .badge {
2
2
  display: inline-flex;
3
3
  align-items: center;
4
- padding: 0.25rem 0.75rem;
4
+ justify-content: center;
5
5
  border-radius: var(--radius-pill);
6
- font-size: 0.75rem;
7
6
  font-weight: 700;
8
7
  text-transform: uppercase;
9
8
  border: 2px solid var(--card-border);
10
9
  box-shadow: 2px 2px 0px 0px var(--card-border);
10
+ line-height: 1;
11
+ }
12
+ .badge.sm {
13
+ padding: 0.2rem 0.6rem;
14
+ font-size: 0.7rem;
15
+ }
16
+ .badge.md {
17
+ padding: 0.25rem 0.75rem;
18
+ font-size: 0.75rem;
19
+ }
20
+ .badge.lg {
21
+ padding: 0.375rem 1rem;
22
+ font-size: 0.875rem;
11
23
  }
12
24
  .badge.primary {
13
25
  background-color: var(--primary);
@@ -29,3 +41,9 @@
29
41
  background-color: var(--secondary);
30
42
  color: var(--secondary-foreground);
31
43
  }
44
+ .badge.outline {
45
+ background-color: transparent;
46
+ color: var(--muted-foreground);
47
+ border-color: var(--border-strong);
48
+ box-shadow: none;
49
+ }
@@ -1,6 +1,6 @@
1
- import React from 'react';
2
- type ButtonVariant = 'primary' | 'secondary' | 'ghost' | 'outline' | 'success';
3
- type ButtonSize = 'sm' | 'md' | 'lg';
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
- 'use client';
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 'clsx';
15
- import { Spinner } from '../Spinner/index.js';
16
- import styles from './Button.module.css';
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 = 'primary', size = 'md', loading = false, className, disabled } = _a, props = __rest(_a, ["children", "variant", "size", "loading", "className", "disabled"]);
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 && (_jsx(Spinner, { size: "sm", className: styles.spinnerIcon })), children] })));
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 0.5rem;
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: "ghost", 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);
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,237 @@
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 { Image } from "../Image/Image.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
+ return (_jsx("div", { className: styles.previewWrapper, children: _jsx(Image, { src: src, alt: alt
235
+ .replace(/\.(jpg|jpeg|png|gif|webp|svg)$/i, "")
236
+ .replace(/[-_]/g, " ") || "File preview", className: styles.previewImg, fit: "cover", rounded: false }) }));
237
+ };