@plexui/ui 0.7.77 → 0.7.78
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/dist/es/components/FileUpload/FileUpload.js +112 -0
- package/dist/es/components/FileUpload/FileUpload.js.map +1 -0
- package/dist/es/components/FileUpload/FileUpload.module.css +125 -0
- package/dist/es/components/FileUpload/index.js +2 -0
- package/dist/es/components/FileUpload/index.js.map +1 -0
- package/dist/es/styles/variables-components.css +18 -0
- package/dist/types/components/FileUpload/FileUpload.d.ts +20 -0
- package/dist/types/components/FileUpload/index.d.ts +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import clsx from "clsx";
|
|
4
|
+
import { useCallback, useId, useRef, useState } from "react";
|
|
5
|
+
import { FileDocument, ImageSquare, FileBlank, UploadDocuments, X, } from "../Icon";
|
|
6
|
+
import { Button } from "../Button";
|
|
7
|
+
import { FieldError } from "../FieldError";
|
|
8
|
+
import s from "./FileUpload.module.css";
|
|
9
|
+
function getFileIcon(file) {
|
|
10
|
+
const type = file.type;
|
|
11
|
+
const name = file.name.toLowerCase();
|
|
12
|
+
if (type === "application/pdf" ||
|
|
13
|
+
name.endsWith(".pdf") ||
|
|
14
|
+
name.endsWith(".doc") ||
|
|
15
|
+
name.endsWith(".docx") ||
|
|
16
|
+
name.endsWith(".txt") ||
|
|
17
|
+
name.endsWith(".rtf")) {
|
|
18
|
+
return _jsx(FileDocument, {});
|
|
19
|
+
}
|
|
20
|
+
if (type.startsWith("image/") || /\.(png|jpe?g|gif|svg|webp|bmp|ico)$/.test(name)) {
|
|
21
|
+
return _jsx(ImageSquare, {});
|
|
22
|
+
}
|
|
23
|
+
return _jsx(FileBlank, {});
|
|
24
|
+
}
|
|
25
|
+
function formatFileSize(bytes) {
|
|
26
|
+
if (bytes === 0)
|
|
27
|
+
return "0 B";
|
|
28
|
+
if (bytes < 1024)
|
|
29
|
+
return `${bytes} B`;
|
|
30
|
+
if (bytes < 1024 * 1024)
|
|
31
|
+
return `${(bytes / 1024).toFixed(0)} KB`;
|
|
32
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
33
|
+
}
|
|
34
|
+
function splitFileName(name) {
|
|
35
|
+
const lastDot = name.lastIndexOf(".");
|
|
36
|
+
if (lastDot <= 0)
|
|
37
|
+
return { stem: name, ext: "" };
|
|
38
|
+
return {
|
|
39
|
+
stem: name.slice(0, lastDot),
|
|
40
|
+
ext: name.slice(lastDot),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
function matchesAccept(file, accept) {
|
|
44
|
+
const acceptTypes = accept.split(",").map((t) => t.trim().toLowerCase());
|
|
45
|
+
const fileName = file.name.toLowerCase();
|
|
46
|
+
const fileType = file.type.toLowerCase();
|
|
47
|
+
return acceptTypes.some((acceptType) => {
|
|
48
|
+
if (acceptType.startsWith(".")) {
|
|
49
|
+
return fileName.endsWith(acceptType);
|
|
50
|
+
}
|
|
51
|
+
if (acceptType.endsWith("/*")) {
|
|
52
|
+
const prefix = acceptType.slice(0, -1);
|
|
53
|
+
return fileType.startsWith(prefix);
|
|
54
|
+
}
|
|
55
|
+
return fileType === acceptType;
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
export const FileUpload = (props) => {
|
|
59
|
+
const { variant = "compact", label, description, accept, multiple = false, maxFiles, maxSize, value: controlledValue, onValueChange, onFileReject, errorMessage, "invalid": invalidProp, disabled = false, className, id: idProp, "aria-describedby": ariaDescribedByProp, } = props;
|
|
60
|
+
const [internalFiles, setInternalFiles] = useState([]);
|
|
61
|
+
const inputRef = useRef(null);
|
|
62
|
+
const generatedId = useId();
|
|
63
|
+
const inputId = idProp || `file-upload-${generatedId}`;
|
|
64
|
+
const errorId = `${inputId}-error`;
|
|
65
|
+
const files = controlledValue ?? internalFiles;
|
|
66
|
+
const isControlled = controlledValue !== undefined;
|
|
67
|
+
const invalid = invalidProp ?? !!errorMessage;
|
|
68
|
+
const ariaDescribedBy = [ariaDescribedByProp, errorMessage ? errorId : undefined].filter(Boolean).join(" ") ||
|
|
69
|
+
undefined;
|
|
70
|
+
const updateFiles = useCallback((next) => {
|
|
71
|
+
onValueChange?.(next);
|
|
72
|
+
if (!isControlled) {
|
|
73
|
+
setInternalFiles(next);
|
|
74
|
+
}
|
|
75
|
+
}, [onValueChange, isControlled]);
|
|
76
|
+
const handleFileSelect = useCallback((e) => {
|
|
77
|
+
const selected = Array.from(e.target.files || []);
|
|
78
|
+
const accepted = [];
|
|
79
|
+
for (const file of selected) {
|
|
80
|
+
if (accept && !matchesAccept(file, accept)) {
|
|
81
|
+
onFileReject?.(file, `File type not accepted: ${file.name}`);
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
if (maxSize && file.size > maxSize) {
|
|
85
|
+
onFileReject?.(file, `File too large: ${file.name} (${formatFileSize(file.size)})`);
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
if (maxFiles && files.length + accepted.length >= maxFiles) {
|
|
89
|
+
onFileReject?.(file, `Maximum ${maxFiles} files allowed`);
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
accepted.push(file);
|
|
93
|
+
}
|
|
94
|
+
if (accepted.length > 0) {
|
|
95
|
+
updateFiles([...files, ...accepted]);
|
|
96
|
+
}
|
|
97
|
+
e.target.value = "";
|
|
98
|
+
}, [accept, maxSize, maxFiles, files, updateFiles, onFileReject]);
|
|
99
|
+
const removeFile = useCallback((index) => {
|
|
100
|
+
const next = files.filter((_, i) => i !== index);
|
|
101
|
+
updateFiles(next);
|
|
102
|
+
}, [files, updateFiles]);
|
|
103
|
+
const openPicker = useCallback(() => {
|
|
104
|
+
inputRef.current?.click();
|
|
105
|
+
}, []);
|
|
106
|
+
const atLimit = !!maxFiles && files.length >= maxFiles;
|
|
107
|
+
return (_jsxs("div", { className: clsx(s.Root, className), children: [variant === "area" ? (_jsxs("button", { type: "button", className: s.AreaTrigger, "data-invalid": invalid ? "" : undefined, onClick: openPicker, disabled: disabled || atLimit, children: [_jsx(UploadDocuments, { className: s.AreaIcon }), _jsx("span", { className: s.AreaLabel, children: label || "Tap to upload files" }), description && _jsx("span", { className: s.AreaDescription, children: description })] })) : (_jsx(Button, { color: "secondary", variant: "soft", size: "3xl", pill: true, block: true, disabled: disabled || atLimit, onClick: openPicker, className: "h-[3.25rem]", children: label || "Upload file" })), files.length > 0 && (_jsx("ul", { className: s.FileList, "data-variant": variant, "aria-label": "Attached files", children: files.map((file, index) => {
|
|
108
|
+
const { stem, ext } = splitFileName(file.name);
|
|
109
|
+
return (_jsxs("li", { className: s.FileItem, children: [_jsx("div", { className: s.FileIcon, children: getFileIcon(file) }), _jsxs("div", { className: s.FileInfo, children: [_jsxs("div", { className: s.FileNameRow, children: [_jsx("span", { className: s.FileNameStem, children: stem }), ext && _jsx("span", { className: s.FileNameExt, children: ext })] }), _jsx("span", { className: s.FileSize, children: formatFileSize(file.size) })] }), _jsx("button", { type: "button", className: s.FileRemove, onClick: () => removeFile(index), "aria-label": `Remove ${file.name}`, disabled: disabled, children: _jsx(X, {}) })] }, `${file.name}-${file.size}-${file.lastModified}`));
|
|
110
|
+
}) })), _jsx("input", { ref: inputRef, type: "file", className: s.HiddenInput, accept: accept, multiple: multiple, onChange: handleFileSelect, disabled: disabled, id: inputId, "aria-describedby": ariaDescribedBy, tabIndex: -1 }), errorMessage && (_jsx(FieldError, { id: errorId, className: s.ErrorMessage, children: errorMessage }))] }));
|
|
111
|
+
};
|
|
112
|
+
//# sourceMappingURL=FileUpload.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"FileUpload.js","sourceRoot":"","sources":["../../../../src/components/FileUpload/FileUpload.tsx"],"names":[],"mappings":"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","sourcesContent":["\"use client\"\n\nimport clsx from \"clsx\"\nimport { useCallback, useId, useRef, useState } from \"react\"\nimport {\n FileDocument,\n ImageSquare,\n FileBlank,\n UploadDocuments,\n X,\n} from \"../Icon\"\nimport { Button } from \"../Button\"\nimport { FieldError } from \"../FieldError\"\nimport s from \"./FileUpload.module.css\"\n\nexport type FileUploadProps = {\n variant?: \"compact\" | \"area\"\n label?: string\n description?: string\n accept?: string\n multiple?: boolean\n maxFiles?: number\n maxSize?: number\n value?: File[]\n onValueChange?: (files: File[]) => void\n onFileReject?: (file: File, reason: string) => void\n errorMessage?: string\n invalid?: boolean\n disabled?: boolean\n id?: string\n className?: string\n \"aria-describedby\"?: string\n \"aria-invalid\"?: boolean\n}\n\nfunction getFileIcon(file: File) {\n const type = file.type\n const name = file.name.toLowerCase()\n\n if (\n type === \"application/pdf\" ||\n name.endsWith(\".pdf\") ||\n name.endsWith(\".doc\") ||\n name.endsWith(\".docx\") ||\n name.endsWith(\".txt\") ||\n name.endsWith(\".rtf\")\n ) {\n return <FileDocument />\n }\n\n if (type.startsWith(\"image/\") || /\\.(png|jpe?g|gif|svg|webp|bmp|ico)$/.test(name)) {\n return <ImageSquare />\n }\n\n return <FileBlank />\n}\n\nfunction formatFileSize(bytes: number): string {\n if (bytes === 0) return \"0 B\"\n if (bytes < 1024) return `${bytes} B`\n if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(0)} KB`\n return `${(bytes / (1024 * 1024)).toFixed(1)} MB`\n}\n\nfunction splitFileName(name: string): { stem: string; ext: string } {\n const lastDot = name.lastIndexOf(\".\")\n if (lastDot <= 0) return { stem: name, ext: \"\" }\n return {\n stem: name.slice(0, lastDot),\n ext: name.slice(lastDot),\n }\n}\n\nfunction matchesAccept(file: File, accept: string): boolean {\n const acceptTypes = accept.split(\",\").map((t) => t.trim().toLowerCase())\n const fileName = file.name.toLowerCase()\n const fileType = file.type.toLowerCase()\n\n return acceptTypes.some((acceptType) => {\n if (acceptType.startsWith(\".\")) {\n return fileName.endsWith(acceptType)\n }\n if (acceptType.endsWith(\"/*\")) {\n const prefix = acceptType.slice(0, -1)\n return fileType.startsWith(prefix)\n }\n return fileType === acceptType\n })\n}\n\nexport const FileUpload = (props: FileUploadProps) => {\n const {\n variant = \"compact\",\n label,\n description,\n accept,\n multiple = false,\n maxFiles,\n maxSize,\n value: controlledValue,\n onValueChange,\n onFileReject,\n errorMessage,\n \"invalid\": invalidProp,\n disabled = false,\n className,\n id: idProp,\n \"aria-describedby\": ariaDescribedByProp,\n } = props\n\n const [internalFiles, setInternalFiles] = useState<File[]>([])\n const inputRef = useRef<HTMLInputElement>(null)\n const generatedId = useId()\n const inputId = idProp || `file-upload-${generatedId}`\n const errorId = `${inputId}-error`\n\n const files = controlledValue ?? internalFiles\n const isControlled = controlledValue !== undefined\n const invalid = invalidProp ?? !!errorMessage\n\n const ariaDescribedBy =\n [ariaDescribedByProp, errorMessage ? errorId : undefined].filter(Boolean).join(\" \") ||\n undefined\n\n const updateFiles = useCallback(\n (next: File[]) => {\n onValueChange?.(next)\n if (!isControlled) {\n setInternalFiles(next)\n }\n },\n [onValueChange, isControlled],\n )\n\n const handleFileSelect = useCallback(\n (e: React.ChangeEvent<HTMLInputElement>) => {\n const selected = Array.from(e.target.files || [])\n const accepted: File[] = []\n\n for (const file of selected) {\n if (accept && !matchesAccept(file, accept)) {\n onFileReject?.(file, `File type not accepted: ${file.name}`)\n continue\n }\n if (maxSize && file.size > maxSize) {\n onFileReject?.(file, `File too large: ${file.name} (${formatFileSize(file.size)})`)\n continue\n }\n if (maxFiles && files.length + accepted.length >= maxFiles) {\n onFileReject?.(file, `Maximum ${maxFiles} files allowed`)\n continue\n }\n accepted.push(file)\n }\n\n if (accepted.length > 0) {\n updateFiles([...files, ...accepted])\n }\n\n e.target.value = \"\"\n },\n [accept, maxSize, maxFiles, files, updateFiles, onFileReject],\n )\n\n const removeFile = useCallback(\n (index: number) => {\n const next = files.filter((_, i) => i !== index)\n updateFiles(next)\n },\n [files, updateFiles],\n )\n\n const openPicker = useCallback(() => {\n inputRef.current?.click()\n }, [])\n\n const atLimit = !!maxFiles && files.length >= maxFiles\n\n return (\n <div className={clsx(s.Root, className)}>\n {variant === \"area\" ? (\n <button\n type=\"button\"\n className={s.AreaTrigger}\n data-invalid={invalid ? \"\" : undefined}\n onClick={openPicker}\n disabled={disabled || atLimit}\n >\n <UploadDocuments className={s.AreaIcon} />\n <span className={s.AreaLabel}>{label || \"Tap to upload files\"}</span>\n {description && <span className={s.AreaDescription}>{description}</span>}\n </button>\n ) : (\n <Button\n color=\"secondary\"\n variant=\"soft\"\n size=\"3xl\"\n pill\n block\n disabled={disabled || atLimit}\n onClick={openPicker}\n className=\"h-[3.25rem]\"\n >\n {label || \"Upload file\"}\n </Button>\n )}\n\n {files.length > 0 && (\n <ul\n className={s.FileList}\n data-variant={variant}\n aria-label=\"Attached files\"\n >\n {files.map((file, index) => {\n const { stem, ext } = splitFileName(file.name)\n return (\n <li\n key={`${file.name}-${file.size}-${file.lastModified}`}\n className={s.FileItem}\n >\n <div className={s.FileIcon}>{getFileIcon(file)}</div>\n <div className={s.FileInfo}>\n <div className={s.FileNameRow}>\n <span className={s.FileNameStem}>{stem}</span>\n {ext && <span className={s.FileNameExt}>{ext}</span>}\n </div>\n <span className={s.FileSize}>{formatFileSize(file.size)}</span>\n </div>\n <button\n type=\"button\"\n className={s.FileRemove}\n onClick={() => removeFile(index)}\n aria-label={`Remove ${file.name}`}\n disabled={disabled}\n >\n <X />\n </button>\n </li>\n )\n })}\n </ul>\n )}\n\n <input\n ref={inputRef}\n type=\"file\"\n className={s.HiddenInput}\n accept={accept}\n multiple={multiple}\n onChange={handleFileSelect}\n disabled={disabled}\n id={inputId}\n aria-describedby={ariaDescribedBy}\n tabIndex={-1}\n />\n\n {errorMessage && (\n <FieldError id={errorId} className={s.ErrorMessage}>\n {errorMessage}\n </FieldError>\n )}\n </div>\n )\n}\n"]}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
@layer components {.Root {
|
|
2
|
+
position: relative;
|
|
3
|
+
--field-error-padding-inline: 0;
|
|
4
|
+
--field-error-margin-top: 0.25rem;
|
|
5
|
+
}.AreaTrigger {
|
|
6
|
+
display: flex;
|
|
7
|
+
flex-direction: column;
|
|
8
|
+
justify-content: center;
|
|
9
|
+
align-items: center;
|
|
10
|
+
width: 100%;
|
|
11
|
+
min-height: var(--file-upload-area-min-height);
|
|
12
|
+
padding: 2rem var(--file-upload-gutter);
|
|
13
|
+
gap: 0.5rem;
|
|
14
|
+
border: 1px solid var(--floating-input-border-color);
|
|
15
|
+
border-radius: var(--file-upload-area-border-radius);
|
|
16
|
+
background-color: var(--floating-input-background);
|
|
17
|
+
color: var(--color-text);
|
|
18
|
+
font-size: 1rem;
|
|
19
|
+
cursor: pointer;
|
|
20
|
+
transition: border-color var(--floating-input-transition-duration) ease-in-out;
|
|
21
|
+
}.AreaTrigger:hover:not(:disabled) {
|
|
22
|
+
border-color: var(--floating-input-border-color-hover);
|
|
23
|
+
}.AreaTrigger[data-invalid] {
|
|
24
|
+
border-color: var(--floating-input-border-color-invalid);
|
|
25
|
+
}.AreaTrigger:disabled {
|
|
26
|
+
opacity: 0.5;
|
|
27
|
+
cursor: not-allowed;
|
|
28
|
+
}.AreaIcon {
|
|
29
|
+
color: var(--color-text-tertiary);
|
|
30
|
+
font-size: 2rem;
|
|
31
|
+
}.AreaLabel {
|
|
32
|
+
font-weight: 500;
|
|
33
|
+
color: var(--color-text);
|
|
34
|
+
}.AreaDescription {
|
|
35
|
+
color: var(--color-text-tertiary);
|
|
36
|
+
font-size: 0.875rem;
|
|
37
|
+
}.FileList {
|
|
38
|
+
margin-top: var(--file-upload-list-gap);
|
|
39
|
+
list-style: none;
|
|
40
|
+
padding: 0;
|
|
41
|
+
overflow: hidden;
|
|
42
|
+
border: 1px solid var(--floating-input-border-color);
|
|
43
|
+
border-radius: calc(var(--floating-input-height) / 2);
|
|
44
|
+
background-color: var(--floating-input-background);
|
|
45
|
+
}.FileItem {
|
|
46
|
+
display: flex;
|
|
47
|
+
align-items: center;
|
|
48
|
+
gap: 10px;
|
|
49
|
+
padding: 12px 21px;
|
|
50
|
+
}.FileItem + .FileItem {
|
|
51
|
+
border-top: 1px solid var(--floating-input-border-color);
|
|
52
|
+
}.FileIcon {
|
|
53
|
+
color: var(--color-text-secondary);
|
|
54
|
+
font-size: 1rem;
|
|
55
|
+
flex-shrink: 0;
|
|
56
|
+
display: flex;
|
|
57
|
+
}.FileInfo {
|
|
58
|
+
flex: 1;
|
|
59
|
+
min-width: 0;
|
|
60
|
+
display: flex;
|
|
61
|
+
flex-direction: column;
|
|
62
|
+
}.FileNameRow {
|
|
63
|
+
display: flex;
|
|
64
|
+
min-width: 0;
|
|
65
|
+
}.FileNameStem {
|
|
66
|
+
overflow: hidden;
|
|
67
|
+
text-overflow: ellipsis;
|
|
68
|
+
white-space: nowrap;
|
|
69
|
+
font-size: 0.875rem;
|
|
70
|
+
line-height: 1.4285714286;
|
|
71
|
+
}.FileNameExt {
|
|
72
|
+
flex-shrink: 0;
|
|
73
|
+
font-size: 0.875rem;
|
|
74
|
+
line-height: 1.4285714286;
|
|
75
|
+
}.FileSize {
|
|
76
|
+
color: var(--color-text-tertiary);
|
|
77
|
+
font-size: 0.75rem;
|
|
78
|
+
line-height: 1.5;
|
|
79
|
+
}.FileRemove {
|
|
80
|
+
display: flex;
|
|
81
|
+
align-items: center;
|
|
82
|
+
justify-content: center;
|
|
83
|
+
width: 20px;
|
|
84
|
+
height: 20px;
|
|
85
|
+
padding: 0;
|
|
86
|
+
margin: 0;
|
|
87
|
+
border: none;
|
|
88
|
+
border-radius: var(--radius-full);
|
|
89
|
+
cursor: pointer;
|
|
90
|
+
color: var(--color-text-tertiary);
|
|
91
|
+
transition: background-color 150ms ease;
|
|
92
|
+
flex-shrink: 0;
|
|
93
|
+
}.FileRemove, :where([data-theme="light"]) .FileRemove {
|
|
94
|
+
background: rgb(0 0 0 / 8%);
|
|
95
|
+
}:where([data-theme="dark"]) .FileRemove {
|
|
96
|
+
background: rgb(255 255 255 / 8%);
|
|
97
|
+
}.FileRemove svg {
|
|
98
|
+
width: 15px;
|
|
99
|
+
height: 15px;
|
|
100
|
+
}.FileRemove:hover {
|
|
101
|
+
color: var(--color-text);
|
|
102
|
+
}.FileRemove:hover, :where([data-theme="light"]) .FileRemove:hover {
|
|
103
|
+
background: rgb(0 0 0 / 12%);
|
|
104
|
+
}:where([data-theme="dark"]) .FileRemove:hover {
|
|
105
|
+
background: rgb(255 255 255 / 12%);
|
|
106
|
+
}.FileRemove:disabled {
|
|
107
|
+
pointer-events: none;
|
|
108
|
+
}.HiddenInput {
|
|
109
|
+
position: absolute;
|
|
110
|
+
width: 1px;
|
|
111
|
+
height: 1px;
|
|
112
|
+
padding: 0;
|
|
113
|
+
margin: -1px;
|
|
114
|
+
overflow: hidden;
|
|
115
|
+
clip: rect(0, 0, 0, 0);
|
|
116
|
+
white-space: nowrap;
|
|
117
|
+
border-width: 0;
|
|
118
|
+
}.ErrorMessage {
|
|
119
|
+
margin-top: var(--field-error-margin-top);
|
|
120
|
+
}@media (prefers-reduced-motion: reduce) {
|
|
121
|
+
.AreaTrigger {
|
|
122
|
+
transition: none;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../src/components/FileUpload/index.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAwB,MAAM,cAAc,CAAA","sourcesContent":["export { FileUpload, type FileUploadProps } from \"./FileUpload\"\n"]}
|
|
@@ -142,6 +142,22 @@
|
|
|
142
142
|
--floating-input-transition-duration: 80ms;
|
|
143
143
|
--floating-input-label-scale: 0.88;
|
|
144
144
|
|
|
145
|
+
/* =============================================
|
|
146
|
+
FileUpload
|
|
147
|
+
============================================= */
|
|
148
|
+
--file-upload-height: var(--floating-input-height);
|
|
149
|
+
--file-upload-gutter: var(--floating-input-gutter);
|
|
150
|
+
--file-upload-border-radius: var(--floating-input-border-radius);
|
|
151
|
+
--file-upload-border-color: var(--floating-input-border-color);
|
|
152
|
+
--file-upload-border-color-hover: var(--floating-input-border-color-hover);
|
|
153
|
+
--file-upload-border-color-invalid: var(--floating-input-border-color-invalid);
|
|
154
|
+
--file-upload-background: var(--floating-input-background);
|
|
155
|
+
--file-upload-transition-duration: var(--floating-input-transition-duration);
|
|
156
|
+
--file-upload-list-border-radius: var(--radius-lg);
|
|
157
|
+
--file-upload-list-gap: 0.5rem;
|
|
158
|
+
--file-upload-area-border-radius: var(--radius-xl);
|
|
159
|
+
--file-upload-area-min-height: 10rem;
|
|
160
|
+
|
|
145
161
|
/* =============================================
|
|
146
162
|
Link
|
|
147
163
|
============================================= */
|
|
@@ -461,6 +477,7 @@
|
|
|
461
477
|
--input-outline-border-color-hover: var(--alpha-25);
|
|
462
478
|
--floating-input-border-color: rgb(0 0 0 / 15%);
|
|
463
479
|
--floating-input-border-color-hover: rgb(0 0 0 / 20%);
|
|
480
|
+
--file-upload-separator-color: rgb(0 0 0 / 8%);
|
|
464
481
|
--link-primary-text-color: var(--blue-500);
|
|
465
482
|
--link-primary-text-color-hover: var(--blue-600);
|
|
466
483
|
--user-message-background-color: var(--alpha-05);
|
|
@@ -498,6 +515,7 @@
|
|
|
498
515
|
--input-outline-border-color-hover: var(--alpha-30);
|
|
499
516
|
--floating-input-border-color: rgb(255 255 255 / 16%);
|
|
500
517
|
--floating-input-border-color-hover: rgb(255 255 255 / 24%);
|
|
518
|
+
--file-upload-separator-color: rgb(255 255 255 / 8%);
|
|
501
519
|
--link-primary-text-color: var(--blue-300);
|
|
502
520
|
--link-primary-text-color-hover: var(--blue-400);
|
|
503
521
|
--user-message-background-color: var(--alpha-08);
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export type FileUploadProps = {
|
|
2
|
+
variant?: "compact" | "area";
|
|
3
|
+
label?: string;
|
|
4
|
+
description?: string;
|
|
5
|
+
accept?: string;
|
|
6
|
+
multiple?: boolean;
|
|
7
|
+
maxFiles?: number;
|
|
8
|
+
maxSize?: number;
|
|
9
|
+
value?: File[];
|
|
10
|
+
onValueChange?: (files: File[]) => void;
|
|
11
|
+
onFileReject?: (file: File, reason: string) => void;
|
|
12
|
+
errorMessage?: string;
|
|
13
|
+
invalid?: boolean;
|
|
14
|
+
disabled?: boolean;
|
|
15
|
+
id?: string;
|
|
16
|
+
className?: string;
|
|
17
|
+
"aria-describedby"?: string;
|
|
18
|
+
"aria-invalid"?: boolean;
|
|
19
|
+
};
|
|
20
|
+
export declare const FileUpload: (props: FileUploadProps) => import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { FileUpload, type FileUploadProps } from "./FileUpload";
|