From 0e6adbe44fa39daf2ccfb25ee2b062e2b6d5d28c Mon Sep 17 00:00:00 2001 From: Natxo <1172351+natxocc@users.noreply.github.com> Date: Tue, 31 Mar 2026 12:07:51 +0200 Subject: [PATCH] Add Fileinput component for file selection --- src/components/Fileinput.js | 113 ++++++++++++++++++++++++++++++++++++ 1 file changed, 113 insertions(+) create mode 100644 src/components/Fileinput.js diff --git a/src/components/Fileinput.js b/src/components/Fileinput.js new file mode 100644 index 0000000..4c3e751 --- /dev/null +++ b/src/components/Fileinput.js @@ -0,0 +1,113 @@ +import { $, $html, $if, $for } from "sigpro"; +import { iconUpload, iconClose } from "../core/icons.js"; + +/** FILEINPUT */ +export const Fileinput = (props) => { + const { tooltip, max = 2, accept = "*", onSelect } = props; + + const selectedFiles = $([]); + const isDragging = $(false); + const error = $(null); + const MAX_BYTES = max * 1024 * 1024; + + const handleFiles = (files) => { + const fileList = Array.from(files); + error(null); + const oversized = fileList.find((f) => f.size > MAX_BYTES); + + if (oversized) { + error(`Máx ${max}MB`); + return; + } + + selectedFiles([...selectedFiles(), ...fileList]); + onSelect?.(selectedFiles()); + }; + + const removeFile = (index) => { + const updated = selectedFiles().filter((_, i) => i !== index); + selectedFiles(updated); + onSelect?.(updated); + }; + + return $html("fieldset", { class: "fieldset w-full p-0" }, [ + $html( + "div", + { + class: () => `w-full ${tooltip ? "tooltip tooltip-top before:z-50 after:z-50" : ""}`, + "data-tip": tooltip, + }, + [ + $html( + "label", + { + class: () => ` + relative flex items-center justify-between w-full h-12 px-4 + border-2 border-dashed rounded-lg cursor-pointer + transition-all duration-200 + ${isDragging() ? "border-primary bg-primary/10" : "border-base-content/20 bg-base-100 hover:bg-base-200"} + `, + ondragover: (e) => { + e.preventDefault(); + isDragging(true); + }, + ondragleave: () => isDragging(false), + ondrop: (e) => { + e.preventDefault(); + isDragging(false); + handleFiles(e.dataTransfer.files); + }, + }, + [ + $html("div", { class: "flex items-center gap-3 w-full" }, [ + $html("img", { src: iconUpload, class: "w-5 h-5 opacity-50 shrink-0" }), + $html("span", { class: "text-sm opacity-70 truncate grow text-left" }, "Arrastra o selecciona archivos..."), + $html("span", { class: "text-[10px] opacity-40 shrink-0" }, `Máx ${max}MB`), + ]), + $html("input", { + type: "file", + multiple: true, + accept: accept, + class: "hidden", + onchange: (e) => handleFiles(e.target.files), + }), + ], + ), + ], + ), + + () => (error() ? $html("span", { class: "text-[10px] text-error mt-1 px-1 font-medium" }, error()) : null), + + $if( + () => selectedFiles().length > 0, + () => + $html("ul", { class: "mt-2 space-y-1" }, [ + $for( + selectedFiles, + (file, index) => + $html("li", { class: "flex items-center justify-between p-1.5 pl-3 text-xs bg-base-200/50 rounded-md border border-base-300" }, [ + $html("div", { class: "flex items-center gap-2 truncate" }, [ + $html("span", { class: "opacity-50" }, "📄"), + $html("span", { class: "truncate font-medium max-w-[200px]" }, file.name), + $html("span", { class: "text-[9px] opacity-40" }, `(${(file.size / 1024).toFixed(0)} KB)`), + ]), + $html( + "button", + { + type: "button", + class: "btn btn-ghost btn-xs btn-circle", + onclick: (e) => { + e.preventDefault(); + e.stopPropagation(); + removeFile(index); + }, + }, + [$html("img", { src: iconClose, class: "w-3 h-3 opacity-70" })], + ), + ]), + (file) => file.name + file.lastModified, + ), + ]), + ), + ]); +};