diff --git a/docs/inputs.rst b/docs/inputs.rst index d87acd2203..3a3c7f14bb 100644 --- a/docs/inputs.rst +++ b/docs/inputs.rst @@ -19,6 +19,11 @@ JSON format). When uploading through the Web UI, navigate to your project and use the upload interface in the "Inputs" panel. +.. note:: + + Uploading directories or folders directly is not supported. Please compress + directories into an archive (such as ``.zip`` or ``.tar.gz``) before uploading. + For REST API uploads, refer to the :ref:`rest_api` documentation for endpoint details. .. _inputs_download_url: diff --git a/scancodeio/static/js/add_inputs.js b/scancodeio/static/js/add_inputs.js index 1850759c38..34eac57aa6 100644 --- a/scancodeio/static/js/add_inputs.js +++ b/scancodeio/static/js/add_inputs.js @@ -22,11 +22,103 @@ const fileInput = document.querySelector("#id_input_files"); let selectedFiles = []; // Store selected files -fileInput.onchange = updateFiles; +let folderAlertTimeout = null; +if (fileInput) { + fileInput.onchange = updateFiles; +} + +function formatFileSize(bytes) { + if (!bytes || bytes <= 0) return "0 B"; + const units = ["B", "KB", "MB", "GB", "TB"]; + const i = Math.min(Math.floor(Math.log(bytes) / Math.log(1024)), units.length - 1); + const size = (bytes / Math.pow(1024, i)).toFixed(i === 0 ? 0 : 1); + return `${size} ${units[i]}`; +} + +function updateHeaderSummary() { + const header = document.querySelector(".file-list-header"); + if (!header) return; + const countBadge = header.querySelector(".file-list-count"); + const sizeBadge = header.querySelector(".file-list-size"); + const totalCount = selectedFiles.length; + const totalBytes = selectedFiles.reduce((acc, f) => acc + (f.size || 0), 0); + if (countBadge) countBadge.innerHTML = `${totalCount} file${totalCount === 1 ? "" : "s"}`; + if (sizeBadge) sizeBadge.textContent = formatFileSize(totalBytes); +} + +function renderFileListHeader(container) { + const totalCount = selectedFiles.length; + const totalBytes = selectedFiles.reduce((acc, f) => acc + (f.size || 0), 0); + + const header = document.createElement("div"); + header.className = "file-list-header"; + + const info = document.createElement("div"); + info.className = "file-list-info"; + + const countBadge = document.createElement("span"); + countBadge.className = "file-list-count"; + countBadge.innerHTML = `${totalCount} file${totalCount === 1 ? "" : "s"}`; + + const dot = document.createElement("span"); + dot.className = "file-list-dot"; + dot.textContent = " • "; + + const sizeBadge = document.createElement("span"); + sizeBadge.className = "file-list-size"; + sizeBadge.textContent = formatFileSize(totalBytes); + + info.appendChild(countBadge); + info.appendChild(dot); + info.appendChild(sizeBadge); + + const deleteAllBtn = document.createElement("button"); + deleteAllBtn.type = "button"; + deleteAllBtn.className = "file-list-delete-all"; + deleteAllBtn.id = "file-delete-all-btn"; + deleteAllBtn.setAttribute("aria-label", "Delete all selected files"); + deleteAllBtn.innerHTML = 'Delete all'; + deleteAllBtn.addEventListener("click", function(event) { + disableEvent(event); + deleteAllFiles(); + }); + + header.appendChild(info); + header.appendChild(deleteAllBtn); + container.appendChild(header); +} + +function deleteAllFiles() { + const rows = document.querySelectorAll(".file-item-row"); + rows.forEach(row => row.classList.add("is-deleting")); + const header = document.querySelector(".file-list-header"); + if (header) { + header.style.opacity = "0"; + header.style.transition = "opacity 150ms ease"; + } + + selectedFiles = []; + const dataTransfer = new DataTransfer(); + if (fileInput) { + fileInput.files = dataTransfer.files; + } + + setTimeout(() => { + if (selectedFiles.length === 0) { + const fileNamesContainer = document.querySelector("#inputs_file_name"); + if (fileNamesContainer) { + const emptyNotice = document.createElement("i"); + emptyNotice.className = "file-empty-notice"; + emptyNotice.textContent = "No files selected"; + fileNamesContainer.replaceChildren(emptyNotice); + } + } + }, 180); +} // Update the list of files to be uploaded in the UI function updateFiles() { - if (fileInput.files.length > 0) { + if (fileInput && fileInput.files.length > 0) { const fileName = document.querySelector("#inputs_file_name"); fileName.replaceChildren(); @@ -39,40 +131,51 @@ function updateFiles() { // Concatenate the unique files to the existing selectedFiles array selectedFiles = selectedFiles.concat(filteredNewFiles); + const dataTransfer = new DataTransfer(); + for (const file of selectedFiles) { + dataTransfer.items.add(file); + } + fileInput.files = dataTransfer.files; + + if (selectedFiles.length > 0) { + renderFileListHeader(fileName); + } + for (let file of selectedFiles) { const fileNameWithoutSpaces = file.name.replace(/\s/g, ""); // Build the wrapper span const wrapper = document.createElement("span"); - wrapper.className = "is-flex is-justify-content-space-between is-block"; + wrapper.className = "file-item-row"; wrapper.id = `file-name-${fileNameWithoutSpaces}`; + wrapper.dataset.fileName = file.name; - // File name label - textContent is safe, no HTML injection possible + // File name label const label = document.createElement("span"); - label.className = "is-block"; + label.className = "file-item-name"; label.textContent = file.name; + label.title = file.name; // Delete button - const deleteLink = document.createElement("a"); - deleteLink.href = "#"; - deleteLink.className = "model-button"; - deleteLink.id = `file-delete-btn-${fileNameWithoutSpaces}`; - deleteLink.addEventListener("click", function(event) { + const deleteBtn = document.createElement("button"); + deleteBtn.type = "button"; + deleteBtn.className = "file-item-delete"; + deleteBtn.id = `file-delete-btn-${fileNameWithoutSpaces}`; + deleteBtn.setAttribute("aria-label", `Remove ${file.name}`); + deleteBtn.title = `Remove ${file.name}`; + deleteBtn.addEventListener("click", function(event) { disableEvent(event); - removeFile(fileNameWithoutSpaces); - if (selectedFiles.length == 0) { - const emptyNotice = document.createElement("i"); - emptyNotice.textContent = "No files selected"; - fileName.replaceChildren(emptyNotice); - } + if (wrapper.classList.contains("is-deleting")) return; + removeFile(file.name); }); const icon = document.createElement("i"); icon.className = "fa-solid fa-trash-can"; + icon.setAttribute("aria-hidden", "true"); - deleteLink.appendChild(icon); + deleteBtn.appendChild(icon); wrapper.appendChild(label); - wrapper.appendChild(deleteLink); + wrapper.appendChild(deleteBtn); fileName.appendChild(wrapper); } } @@ -84,40 +187,274 @@ function disableEvent(event) { event.preventDefault(); } -function removeFile(fileName) { - selectedFiles = selectedFiles.filter(file => { - const fileNameWithoutSpaces = file.name.replace(/\s/g, ""); - return fileNameWithoutSpaces !== fileName; - }); - - const fileNameElement = document.getElementById(`file-name-${fileName}`); - if (fileNameElement) { - fileNameElement.remove(); - } +function removeFile(targetFileName) { + selectedFiles = selectedFiles.filter(file => file.name !== targetFileName); const dataTransfer = new DataTransfer(); for (let file of selectedFiles) { dataTransfer.items.add(file); } - fileInput.files = dataTransfer.files; + if (fileInput) { + fileInput.files = dataTransfer.files; + } + updateHeaderSummary(); + + if (selectedFiles.length === 0) { + const header = document.querySelector(".file-list-header"); + if (header) { + header.style.opacity = "0"; + header.style.transition = "opacity 150ms ease"; + } + } + + const fileNameWithoutSpaces = targetFileName.replace(/\s/g, ""); + const fileNameElement = document.getElementById(`file-name-${fileNameWithoutSpaces}`); + if (fileNameElement) { + fileNameElement.classList.add("is-deleting"); + setTimeout(() => { + if (fileNameElement.parentNode) { + fileNameElement.remove(); + } + if (selectedFiles.length === 0) { + const fileNamesContainer = document.querySelector("#inputs_file_name"); + if (fileNamesContainer && !fileNamesContainer.querySelector(".file-item-row:not(.is-deleting)")) { + const emptyNotice = document.createElement("i"); + emptyNotice.className = "file-empty-notice"; + emptyNotice.textContent = "No files selected"; + fileNamesContainer.replaceChildren(emptyNotice); + } + } + }, 180); + } +} + +const fileNameContainer = document.querySelector("#inputs_file_name"); +if (fileNameContainer) { + fileNameContainer.addEventListener("click", function(event) { + if (selectedFiles.length > 0) { + event.preventDefault(); + } + + if (event.target.closest("#file-delete-all-btn")) { + return; + } + + if (event.target.closest(".file-item-delete")) { + return; + } + + const rect = fileNameContainer.getBoundingClientRect(); + if (event.clientX > rect.left + fileNameContainer.clientWidth) { + return; + } + + if (event.clientX >= rect.right - 55) { + const rowUnderCursor = document.elementFromPoint(event.clientX, event.clientY)?.closest(".file-item-row:not(.is-deleting)"); + if (rowUnderCursor && rowUnderCursor.dataset.fileName) { + removeFile(rowUnderCursor.dataset.fileName); + } + } + }); +} + +function inspectDataTransfer(dataTransfer) { + const validFiles = []; + const rejectedFolders = []; + + const items = dataTransfer.items; + if (items && items.length > 0) { + for (let i = 0; i < items.length; i++) { + const item = items[i]; + if (item.kind !== "file") continue; + + const entry = item.webkitGetAsEntry ? item.webkitGetAsEntry() : null; + if (entry) { + if (entry.isDirectory) { + rejectedFolders.push(entry.name || "folder"); + continue; + } + } + + const file = item.getAsFile ? item.getAsFile() : null; + if (file) { + validFiles.push(file); + } + } + } else if (dataTransfer.files && dataTransfer.files.length > 0) { + for (const file of dataTransfer.files) { + validFiles.push(file); + } + } + + return { validFiles, rejectedFolders }; +} + +function showFolderUploadWarning(count) { + const inputFilesBox = document.querySelector("#input_files_box"); + if (!inputFilesBox) return; + + // Gentle tactile pulse on the dropzone without shaking + const fileCta = inputFilesBox.querySelector(".file-cta"); + if (fileCta) { + fileCta.classList.remove("has-folder-error"); + void fileCta.offsetWidth; + fileCta.classList.add("has-folder-error"); + setTimeout(() => { + fileCta.classList.remove("has-folder-error"); + }, 600); + } + + if (folderAlertTimeout) { + clearTimeout(folderAlertTimeout); + folderAlertTimeout = null; + } + + let tray = inputFilesBox.querySelector(".folder-notice-tray"); + let isNew = false; + + if (!tray) { + isNew = true; + tray = document.createElement("div"); + tray.className = "folder-notice-tray"; + tray.setAttribute("role", "alert"); + tray.setAttribute("aria-live", "polite"); + + const inner = document.createElement("div"); + inner.className = "folder-notice-inner"; + + const bar = document.createElement("div"); + bar.className = "folder-notice-bar"; + + const main = document.createElement("div"); + main.className = "folder-notice-main"; + + const iconSpan = document.createElement("span"); + iconSpan.className = "folder-notice-icon"; + const icon = document.createElement("i"); + icon.className = "fa-solid fa-folder-minus"; + iconSpan.appendChild(icon); + main.appendChild(iconSpan); + + const msg = document.createElement("span"); + msg.className = "folder-notice-msg"; + msg.append("Folders cannot be uploaded directly. Please upload a "); + + const zipKbd = document.createElement("kbd"); + zipKbd.className = "folder-notice-kbd"; + zipKbd.textContent = ".zip"; + msg.appendChild(zipKbd); + + msg.append(" or "); + + const tarKbd = document.createElement("kbd"); + tarKbd.className = "folder-notice-kbd"; + tarKbd.textContent = ".tar.gz"; + msg.appendChild(tarKbd); + + msg.append(" archive instead."); + + const countSpan = document.createElement("span"); + countSpan.className = "folder-notice-count"; + countSpan.textContent = `(Folders skipped: ${count || 1})`; + msg.appendChild(countSpan); + + main.appendChild(msg); + + const dismissBtn = document.createElement("button"); + dismissBtn.type = "button"; + dismissBtn.className = "folder-notice-dismiss"; + dismissBtn.setAttribute("aria-label", "Dismiss notice"); + dismissBtn.setAttribute("title", "Dismiss"); + const dismissIcon = document.createElement("i"); + dismissIcon.className = "fa-solid fa-xmark"; + dismissBtn.appendChild(dismissIcon); + + bar.appendChild(main); + bar.appendChild(dismissBtn); + inner.appendChild(bar); + tray.appendChild(inner); + + inputFilesBox.appendChild(tray); + inputFilesBox.classList.add("has-folder-notice"); + } else { + const countSpan = tray.querySelector(".folder-notice-count"); + if (countSpan && count) { + countSpan.textContent = `(Folders skipped: ${count})`; + } + } + + function dismiss() { + if (folderAlertTimeout) { + clearTimeout(folderAlertTimeout); + folderAlertTimeout = null; + } + if (tray && tray.parentNode) { + tray.classList.remove("is-open"); + setTimeout(() => { + if (tray && tray.parentNode) { + tray.remove(); + } + if (inputFilesBox && !inputFilesBox.querySelector(".folder-notice-tray")) { + inputFilesBox.classList.remove("has-folder-notice"); + } + }, 230); + } + } + + const dismissBtn = tray.querySelector(".folder-notice-dismiss"); + if (dismissBtn) { + dismissBtn.onclick = dismiss; + } + + if (isNew) { + requestAnimationFrame(() => { + requestAnimationFrame(() => { + tray.classList.add("is-open"); + }); + }); + } + + folderAlertTimeout = setTimeout(dismiss, 8000); + tray.onmouseenter = () => { + if (folderAlertTimeout) { + clearTimeout(folderAlertTimeout); + folderAlertTimeout = null; + } + }; + tray.onmouseleave = () => { + folderAlertTimeout = setTimeout(dismiss, 3000); + }; } function dropHandler(event) { disableEvent(event); - // Merge existing files and dropped files, let updateFiles handle dedup - const dataTransfer = new DataTransfer(); - for (const file of [...fileInput.files, ...event.dataTransfer.files]) { - dataTransfer.items.add(file); + const { validFiles, rejectedFolders } = inspectDataTransfer(event.dataTransfer); + + if (rejectedFolders.length > 0) { + showFolderUploadWarning(rejectedFolders.length); } - fileInput.files = dataTransfer.files; - updateFiles(); + if (validFiles.length > 0) { + // Merge existing files and dropped valid files, let updateFiles handle dedup + const dataTransfer = new DataTransfer(); + const existing = fileInput ? fileInput.files : []; + for (const file of [...existing, ...validFiles]) { + dataTransfer.items.add(file); + } + + if (fileInput) { + fileInput.files = dataTransfer.files; + fileInput.dispatchEvent(new Event("change")); + } + } } // Handle drag and drop events const inputFilesBox = document.querySelector("#input_files_box"); -inputFilesBox.addEventListener("dragenter", disableEvent); -inputFilesBox.addEventListener("dragover", disableEvent); -inputFilesBox.addEventListener("drop", dropHandler); +if (inputFilesBox) { + inputFilesBox.addEventListener("dragenter", disableEvent); + inputFilesBox.addEventListener("dragover", disableEvent); + inputFilesBox.addEventListener("drop", dropHandler); +} diff --git a/scancodeio/static/main.css b/scancodeio/static/main.css index f85577d7d1..d6ca714d09 100644 --- a/scancodeio/static/main.css +++ b/scancodeio/static/main.css @@ -331,6 +331,463 @@ button.as-link { border-width: medium; border-style: dashed; } +/* Folder upload rejection notice & dropzone interaction */ +:root { + --folder-notice-bg: #fffdf7; + --folder-notice-border: #fcd34d; + --folder-notice-divider: rgba(217, 119, 6, 0.2); + --folder-notice-accent: #d97706; + --folder-notice-text: #78350f; + --folder-notice-kbd-bg: rgba(217, 119, 6, 0.08); + --folder-notice-kbd-border: rgba(217, 119, 6, 0.22); + --folder-notice-kbd-text: #92400e; + --folder-notice-hover: rgba(120, 53, 15, 0.08); + --folder-notice-glow: rgba(217, 119, 6, 0.18); + --folder-notice-cta-tint: rgba(245, 158, 11, 0.04); +} + +@media (prefers-color-scheme: dark) { + :root:not([data-theme="light"]) { + --folder-notice-bg: #1c1f26; + --folder-notice-border: #374151; + --folder-notice-divider: rgba(245, 158, 11, 0.25); + --folder-notice-accent: #fbbf24; + --folder-notice-text: #f3f4f6; + --folder-notice-kbd-bg: rgba(255, 255, 255, 0.08); + --folder-notice-kbd-border: rgba(255, 255, 255, 0.16); + --folder-notice-kbd-text: #fde68a; + --folder-notice-hover: rgba(255, 255, 255, 0.1); + --folder-notice-glow: rgba(245, 158, 11, 0.2); + --folder-notice-cta-tint: rgba(245, 158, 11, 0.06); + } +} + +[data-theme="dark"], +.theme-dark { + --folder-notice-bg: #1c1f26; + --folder-notice-border: #374151; + --folder-notice-divider: rgba(245, 158, 11, 0.25); + --folder-notice-accent: #fbbf24; + --folder-notice-text: #f3f4f6; + --folder-notice-kbd-bg: rgba(255, 255, 255, 0.08); + --folder-notice-kbd-border: rgba(255, 255, 255, 0.16); + --folder-notice-kbd-text: #fde68a; + --folder-notice-hover: rgba(255, 255, 255, 0.1); + --folder-notice-glow: rgba(245, 158, 11, 0.2); + --folder-notice-cta-tint: rgba(245, 158, 11, 0.06); +} + +[data-theme="light"], +.theme-light { + --folder-notice-bg: #fffdf7; + --folder-notice-border: #fcd34d; + --folder-notice-divider: rgba(217, 119, 6, 0.2); + --folder-notice-accent: #d97706; + --folder-notice-text: #78350f; + --folder-notice-kbd-bg: rgba(217, 119, 6, 0.08); + --folder-notice-kbd-border: rgba(217, 119, 6, 0.22); + --folder-notice-kbd-text: #92400e; + --folder-notice-hover: rgba(120, 53, 15, 0.08); + --folder-notice-glow: rgba(217, 119, 6, 0.18); + --folder-notice-cta-tint: rgba(245, 158, 11, 0.04); +} + +#input_files_box { + flex-direction: column; +} + +.file.is-boxed.has-name .file-cta { + border-width: medium; + border-style: dashed; + transition: border-color 220ms cubic-bezier(0.25, 1, 0.5, 1), + background-color 220ms cubic-bezier(0.25, 1, 0.5, 1), + box-shadow 220ms cubic-bezier(0.25, 1, 0.5, 1); +} + +.file.is-boxed.has-name .file-cta.has-folder-error { + border-color: var(--folder-notice-accent) !important; + background-color: var(--folder-notice-cta-tint) !important; + box-shadow: 0 0 0 3px var(--folder-notice-glow) !important; +} + +#input_files_box.has-folder-notice #inputs_file_name { + border-bottom-left-radius: 0 !important; + border-bottom-right-radius: 0 !important; + border-bottom-color: var(--folder-notice-divider) !important; + transition: border-radius 220ms cubic-bezier(0.25, 1, 0.5, 1), + border-bottom-color 220ms ease; +} + +.folder-notice-tray { + display: grid; + grid-template-rows: 0fr; + opacity: 0; + width: 100%; + transition: grid-template-rows 220ms cubic-bezier(0.25, 1, 0.5, 1), + opacity 180ms ease; + will-change: grid-template-rows, opacity; +} + +.folder-notice-tray.is-open { + grid-template-rows: 1fr; + opacity: 1; + transition: grid-template-rows 280ms cubic-bezier(0.16, 1, 0.3, 1), + opacity 200ms ease; +} + +.folder-notice-inner { + overflow: hidden; + min-height: 0; +} + +.folder-notice-bar { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 0.85rem; + padding: 0.55rem 0.85rem; + font-size: 0.8125rem; + line-height: 1.45; + background-color: var(--folder-notice-bg); + border: 1px solid var(--folder-notice-border); + border-top: none; + border-bottom-left-radius: var(--bulma-file-radius, var(--bulma-radius, 6px)); + border-bottom-right-radius: var(--bulma-file-radius, var(--bulma-radius, 6px)); + color: var(--folder-notice-text); + box-shadow: inset 0 1px 0 var(--folder-notice-divider); +} + +.folder-notice-main { + display: flex; + align-items: flex-start; + gap: 0.75rem; + flex: 1; + min-width: 0; +} + +.folder-notice-icon { + flex-shrink: 0; + display: inline-flex; + align-items: center; + justify-content: center; + color: var(--folder-notice-accent); + font-size: 0.95rem; + line-height: 1; + margin-top: 0.125rem; +} + +.folder-notice-msg { + color: var(--folder-notice-text); +} + +.folder-notice-msg strong { + color: inherit; + font-weight: 600; +} + +.folder-notice-kbd { + display: inline-block; + padding: 0.08em 0.35em; + font-family: var(--bulma-family-monospace, monospace); + font-size: 0.75rem; + font-weight: 600; + line-height: 1.2; + border-radius: 3px; + background-color: var(--folder-notice-kbd-bg); + border: 1px solid var(--folder-notice-kbd-border); + color: var(--folder-notice-kbd-text); + vertical-align: baseline; +} + +.folder-notice-count { + display: inline-block; + margin-left: 0.4rem; + font-size: 0.775rem; + opacity: 0.85; + white-space: nowrap; +} + +.folder-notice-dismiss { + flex-shrink: 0; + display: inline-flex; + align-items: center; + justify-content: center; + width: 1.35rem; + height: 1.35rem; + padding: 0; + border: none; + background: transparent; + color: var(--folder-notice-text); + opacity: 0.6; + border-radius: 4px; + cursor: pointer; + font-size: 0.875rem; + line-height: 1; + margin-top: -0.05rem; + transition: opacity 140ms ease, background-color 140ms ease, transform 80ms ease; +} + +.folder-notice-dismiss:hover { + opacity: 1; + background-color: var(--folder-notice-hover); +} + +.folder-notice-dismiss:active { + transform: scale(0.9); +} + +#inputs_file_name { + max-height: 180px; + overflow-y: scroll; + scrollbar-gutter: stable; + overflow-x: hidden; + overscroll-behavior: contain; + scroll-behavior: smooth; + scrollbar-width: thin; + scrollbar-color: var(--bulma-border, #dbdbdb) transparent; + padding: 0.35rem 0.5rem; +} + +#inputs_file_name::-webkit-scrollbar { + width: 6px; +} + +#inputs_file_name::-webkit-scrollbar-track { + background: transparent; +} + +#inputs_file_name::-webkit-scrollbar-thumb { + background-color: var(--bulma-border, #dbdbdb); + border-radius: 9999px; +} + +#inputs_file_name::-webkit-scrollbar-thumb:hover { + background-color: var(--bulma-border-hover, #b5b5b5); +} + +.file-item-row { + display: flex; + align-items: center; + justify-content: space-between; + gap: 0.75rem; + padding: 0.35rem 0.5rem; + border-radius: 6px; + max-height: 48px; + opacity: 1; + transform: translateZ(0) scale(1) translateX(0); + transition: + opacity 180ms cubic-bezier(0.16, 1, 0.3, 1), + transform 180ms cubic-bezier(0.16, 1, 0.3, 1), + max-height 180ms cubic-bezier(0.25, 1, 0.5, 1), + padding 180ms cubic-bezier(0.25, 1, 0.5, 1), + margin 180ms cubic-bezier(0.25, 1, 0.5, 1), + background-color 150ms ease; + overflow: hidden; +} + +.file-item-row:hover { + background-color: rgba(125, 125, 125, 0.08); +} + +.file-item-row.is-deleting { + opacity: 0; + transform: scale(0.96) translateX(12px); + max-height: 0; + padding-top: 0; + padding-bottom: 0; + margin-top: 0; + margin-bottom: 0; + pointer-events: none !important; +} + +.file-list-header { + position: sticky; + top: -0.35rem; + z-index: 5; + display: flex; + align-items: center; + justify-content: space-between; + padding: 0.35rem 0.5rem; + margin: -0.35rem -0.5rem 0.4rem -0.5rem; + background-color: var(--bulma-scheme-main-ter, #f5f5f5); + border-bottom: 1px solid var(--bulma-border, #dbdbdb); + backdrop-filter: blur(8px); + -webkit-backdrop-filter: blur(8px); + user-select: none; +} + +[data-theme="dark"] .file-list-header, +.theme-dark .file-list-header, +:root:not([data-theme="light"]) .file-list-header { + background-color: rgba(30, 34, 42, 0.95); + border-bottom-color: rgba(255, 255, 255, 0.1); +} + +.file-list-info { + display: flex; + align-items: center; + gap: 0.3rem; + font-size: 0.8rem; + color: var(--bulma-text, #4a4a4a); + min-width: 0; +} + +.file-list-count strong { + color: var(--bulma-text-strong, #363636); +} + +.file-list-dot { + opacity: 0.5; +} + +.file-list-size { + opacity: 0.75; +} + +.file-list-delete-all { + display: inline-flex; + align-items: center; + gap: 0.35rem; + background-color: var(--bulma-danger, #f14668); + color: #ffffff; + border: none; + border-radius: 5px; + padding: 0.2rem 0.55rem; + font-size: 0.75rem; + font-weight: 600; + cursor: pointer; + transition: background-color 150ms ease, transform 100ms ease; + line-height: 1.2; +} + +.file-list-delete-all:hover { + background-color: #e03131; + transform: scale(1.02); +} + +.file-list-delete-all:active { + transform: scale(0.98); +} + +.file-empty-notice { + display: inline-block; + opacity: 0; + animation: fileFadeIn 180ms cubic-bezier(0.16, 1, 0.3, 1) forwards; +} + +@keyframes fileFadeIn { + from { + opacity: 0; + transform: translateY(3px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.file-item-name { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + min-width: 0; + flex: 1; + font-size: 0.875rem; +} + +.file-item-delete { + flex-shrink: 0; + color: var(--bulma-text, #4a4a4a); + opacity: 0.6; + padding: 0.15rem 0.35rem; + border: none; + background: transparent; + border-radius: 3px; + cursor: pointer; + transition: opacity 150ms ease, color 150ms ease, transform 100ms ease; + line-height: 1; +} + +.file-item-delete:hover { + opacity: 1; + color: var(--bulma-danger, #f14668); + transform: scale(1.1); +} + +#project-form-columns { + min-height: calc(100vh - 140px); +} + +.project-create-form { + display: flex; + flex-direction: column; + flex-grow: 1; + padding-bottom: 0; +} + +.project-create-form .box { + box-shadow: none; +} + +.project-form-sticky-actions { + position: sticky; + bottom: 1rem; + margin-top: auto; + z-index: 25; + width: 100%; + border-radius: 14px; + padding: 1.15rem 1.25rem 1rem; + box-sizing: border-box; + background-color: var(--bulma-scheme-main, #ffffff); + border: none; + box-shadow: none; +} + +@supports (backdrop-filter: blur(16px)) { + .project-form-sticky-actions { + background-color: color-mix(in srgb, var(--bulma-scheme-main, #ffffff) 88%, transparent); + backdrop-filter: blur(16px); + -webkit-backdrop-filter: blur(16px); + } +} + +@media (prefers-color-scheme: dark) { + :root:not([data-theme="light"]) .project-form-sticky-actions { + background-color: #1a1d24; + border: none; + box-shadow: none; + } + + @supports (backdrop-filter: blur(16px)) { + :root:not([data-theme="light"]) .project-form-sticky-actions { + background-color: rgba(26, 29, 36, 0.88); + } + } +} + +[data-theme="dark"] .project-form-sticky-actions, +.theme-dark .project-form-sticky-actions { + background-color: #1a1d24; + border: none; + box-shadow: none; +} + +@supports (backdrop-filter: blur(16px)) { + [data-theme="dark"] .project-form-sticky-actions, + .theme-dark .project-form-sticky-actions { + background-color: rgba(26, 29, 36, 0.88); + } +} + +@media (prefers-reduced-motion: reduce) { + .folder-notice-tray, + .folder-notice-bar, + .file.is-boxed.has-name .file-cta, + #input_files_box.has-folder-notice #inputs_file_name { + transition: none !important; + transform: none !important; + } +} progress.file-upload::before { content: "Files upload: " attr(value) "%"; position: absolute; @@ -394,6 +851,36 @@ th#column-summary { } textarea.is-dynamic { min-height: 70px; + max-height: 180px; + overflow-y: auto; + resize: vertical; +} + +textarea.is-dynamic::-webkit-scrollbar { + width: 6px; +} + +textarea.is-dynamic::-webkit-scrollbar-track { + background: transparent; +} + +textarea.is-dynamic::-webkit-scrollbar-thumb { + background-color: var(--bulma-border, #dbdbdb); + border-radius: 9999px; +} + +textarea.is-dynamic::-webkit-scrollbar-thumb:hover { + background-color: var(--bulma-border-hover, #b5b5b5); +} + +[data-theme="dark"] textarea.is-dynamic::-webkit-scrollbar-thumb, +.theme-dark textarea.is-dynamic::-webkit-scrollbar-thumb { + background-color: rgba(255, 255, 255, 0.2); +} + +[data-theme="dark"] textarea.is-dynamic::-webkit-scrollbar-thumb:hover, +.theme-dark textarea.is-dynamic::-webkit-scrollbar-thumb:hover { + background-color: rgba(255, 255, 255, 0.35); } #project-list-header ul#id_is_archived { display: inline-flex; diff --git a/scanpipe/templates/scanpipe/project_form.html b/scanpipe/templates/scanpipe/project_form.html index 9cf97735bd..227bb379ed 100644 --- a/scanpipe/templates/scanpipe/project_form.html +++ b/scanpipe/templates/scanpipe/project_form.html @@ -14,10 +14,10 @@