@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.
@@ -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,2 @@
1
+ export { FileUpload } from "./FileUpload";
2
+ //# sourceMappingURL=index.js.map
@@ -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";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@plexui/ui",
3
- "version": "0.7.77",
3
+ "version": "0.7.78",
4
4
  "description": "Modern design system for building high-quality applications",
5
5
  "type": "module",
6
6
  "license": "MIT",