From c16789b099e143b3760bdc87698ac01d7a4b106e Mon Sep 17 00:00:00 2001 From: nic Date: Tue, 6 May 2025 16:41:52 -0400 Subject: [PATCH] feat: multi-upload modal setup, need to js my way to client-side handing this info off to the backend now --- internal/handlers/web/documents.go | 330 +++++++----------------- static/css/upload.css | 246 ++++++++++++++++++ static/js/htmx.min.js | 1 - templates/layout.html | 1 - templates/partials/document_upload.html | 286 ++++++++++++++++---- 5 files changed, 573 insertions(+), 291 deletions(-) delete mode 100644 static/js/htmx.min.js diff --git a/internal/handlers/web/documents.go b/internal/handlers/web/documents.go index 93bc7f1..21548ea 100644 --- a/internal/handlers/web/documents.go +++ b/internal/handlers/web/documents.go @@ -173,7 +173,6 @@ func UploadDocumentsHandler(w http.ResponseWriter, r *http.Request) { return } - // Check if the request method is POST if r.Method != http.MethodPost { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return @@ -182,8 +181,6 @@ func UploadDocumentsHandler(w http.ResponseWriter, r *http.Request) { log.Printf("Starting document upload handler with Content-Length: %.2f MB", float64(r.ContentLength)/(1024*1024)) - // Parse the multipart form with a reasonable buffer size - // Files larger than this will be saved to temporary files automatically maxMemory := int64(32 << 20) // 32MB in memory, rest to disk if err := r.ParseMultipartForm(maxMemory); err != nil { log.Printf("Error parsing multipart form: %v", err) @@ -192,14 +189,12 @@ func UploadDocumentsHandler(w http.ResponseWriter, r *http.Request) { } defer r.MultipartForm.RemoveAll() // Clean up temporary files - // Get job numbers from form values jobNumbers := r.FormValue("jobNumbers") if jobNumbers == "" { log.Printf("No job numbers found in hidden 'jobNumbers' input.") http.Error(w, "No job numbers provided", http.StatusBadRequest) return } - log.Printf("Job numbers: %s", jobNumbers) jobs := strings.Split(jobNumbers, ",") if len(jobs) == 0 { @@ -207,118 +202,66 @@ func UploadDocumentsHandler(w http.ResponseWriter, r *http.Request) { return } - // Get the single document type - docType := r.FormValue("documentType") - if docType == "" { - log.Printf("No document type selected") - http.Error(w, "Please select a document type", http.StatusBadRequest) - return - } - log.Printf("Document Type selected: %s", docType) - - // Get the uploaded files from the 'documentFiles' input + // Simple multi-upload: use original filenames as display names and default type "1" fileHeaders := r.MultipartForm.File["documentFiles"] if len(fileHeaders) == 0 { http.Error(w, "No documents selected for upload", http.StatusBadRequest) return } - - // Store file metadata - type FileMetadata struct { - FileName string - Type string - TempFile string // Path to temp file - File *os.File // Open file handle for the temp file + type FileToUploadMetadata struct { + OriginalFilename string + DisplayName string + Type string + TempFile string + File *os.File } - - var filesToUpload []FileMetadata - - // Process each uploaded file + var filesToUpload []FileToUploadMetadata for _, fileHeader := range fileHeaders { - if fileHeader.Filename == "" { - log.Printf("Skipping file header with empty filename.") - continue - } - - // Open the uploaded file uploadedFile, err := fileHeader.Open() if err != nil { - log.Printf("Error opening uploaded file %s: %v", fileHeader.Filename, err) - // Optionally: decide if one error should halt all uploads or just skip this file - continue // Skip this file + log.Printf("Error opening uploaded file %s: %v. Skipping.", fileHeader.Filename, err) + continue } - - // Prepare metadata - metadata := FileMetadata{ - FileName: fileHeader.Filename, - Type: docType, // Use the single document type for all files + metadata := FileToUploadMetadata{ + OriginalFilename: fileHeader.Filename, + DisplayName: fileHeader.Filename, + Type: "1", } - - // Create a temp file for the upload (regardless of size to ensure streaming) - tempFile, err := os.CreateTemp("", "upload-*"+filepath.Ext(fileHeader.Filename)) + tempFileHandle, err := os.CreateTemp("", "upload-*"+filepath.Ext(fileHeader.Filename)) if err != nil { - log.Printf("Error creating temp file for %s: %v", fileHeader.Filename, err) + log.Printf("Error creating temp file for %s: %v. Skipping.", fileHeader.Filename, err) uploadedFile.Close() - continue // Skip this file + continue } - - // Copy the file content to the temp file - bytesCopied, err := io.Copy(tempFile, uploadedFile) - uploadedFile.Close() // Close the original multipart file handle - if err != nil { - log.Printf("Error copying to temp file for %s: %v", fileHeader.Filename, err) - tempFile.Close() // Close the temp file handle - os.Remove(tempFile.Name()) // Remove the partially written temp file - continue // Skip this file + if _, err := io.Copy(tempFileHandle, uploadedFile); err != nil { + log.Printf("Error copying to temp file for %s: %v. Skipping.", fileHeader.Filename, err) + uploadedFile.Close() + tempFileHandle.Close() + os.Remove(tempFileHandle.Name()) + continue } - - log.Printf("Copied %d bytes of %s to temporary file: %s", - bytesCopied, fileHeader.Filename, tempFile.Name()) - - // Seek back to beginning for later reading by upload goroutines - if _, err := tempFile.Seek(0, 0); err != nil { - log.Printf("Error seeking temp file for %s: %v", fileHeader.Filename, err) - tempFile.Close() - os.Remove(tempFile.Name()) - continue // Skip this file + uploadedFile.Close() + if _, err := tempFileHandle.Seek(0, 0); err != nil { + log.Printf("Error seeking temp file for %s: %v. Skipping.", fileHeader.Filename, err) + tempFileHandle.Close() + os.Remove(tempFileHandle.Name()) + continue } - - metadata.TempFile = tempFile.Name() - metadata.File = tempFile // Store the open temp file handle + metadata.TempFile = tempFileHandle.Name() + metadata.File = tempFileHandle filesToUpload = append(filesToUpload, metadata) } - - // Ensure temp files associated with metadata are closed and removed later - defer func() { - log.Println("Running deferred cleanup for temp files...") - for _, fm := range filesToUpload { - if fm.File != nil { - fm.File.Close() - if fm.TempFile != "" { - err := os.Remove(fm.TempFile) - if err != nil && !os.IsNotExist(err) { // Don't log error if file already gone - log.Printf("Error cleaning up temp file %s: %v", fm.TempFile, err) - } else if err == nil { - log.Printf("Cleaned up temp file: %s", fm.TempFile) - } - } - } - } - }() - - if len(filesToUpload) == 0 { - http.Error(w, "No valid documents could be processed for upload", http.StatusBadRequest) + activeFilesProcessedCount := len(filesToUpload) + if activeFilesProcessedCount == 0 { + log.Println("No files processed for upload.") + http.Error(w, "No documents were processed for upload.", http.StatusBadRequest) return } + log.Printf("Total active files prepared for upload: %d", activeFilesProcessedCount) - log.Printf("Total valid files to upload: %d", len(filesToUpload)) - - // Concurrent upload with throttling - // ServiceTrade API allows 30s of availability per minute (approximately 15 requests at 2s each) - const maxConcurrent = 5 // A conservative limit to avoid rate limiting - const requestDelay = 300 * time.Millisecond // Delay between requests + const maxConcurrent = 5 + const requestDelay = 300 * time.Millisecond - // Channel for collecting results type UploadResult struct { JobID string DocName string @@ -328,140 +271,112 @@ func UploadDocumentsHandler(w http.ResponseWriter, r *http.Request) { FileSize int64 } - totalUploads := len(jobs) * len(filesToUpload) + totalUploads := len(jobs) * activeFilesProcessedCount resultsChan := make(chan UploadResult, totalUploads) - - // Create a wait group to track when all uploads are done var wg sync.WaitGroup - - // Create a semaphore channel to limit concurrent uploads semaphore := make(chan struct{}, maxConcurrent) - // Start the upload workers - log.Printf("Starting %d upload workers for %d total uploads (%d jobs x %d files)", - maxConcurrent, totalUploads, len(jobs), len(filesToUpload)) + log.Printf("Starting up to %d concurrent upload workers for %d total uploads (%d jobs x %d active files)", + maxConcurrent, totalUploads, len(jobs), activeFilesProcessedCount) for _, jobID := range jobs { - for _, metadata := range filesToUpload { - // Create a closure capture of the metadata for the goroutine - // This is crucial because the 'metadata' variable in the loop will change - currentMetadata := metadata - + for _, metadataToUpload := range filesToUpload { + currentUploadMetadata := metadataToUpload wg.Add(1) - - // Launch a goroutine for each job+document combination - go func(jobID string, meta FileMetadata) { + go func(jobID string, meta FileToUploadMetadata) { defer wg.Done() - - // Acquire a semaphore slot semaphore <- struct{}{} - defer func() { <-semaphore }() // Release when done - - // Add a small delay to avoid overwhelming the API + defer func() { <-semaphore }() time.Sleep(requestDelay) - // Get the file name to use (original filename) - fileName := meta.FileName - - // Use a fresh reader for the temp file for each upload goroutine - // Re-open the temp file for reading to avoid race conditions on the file pointer - fileHandle, err := os.Open(meta.TempFile) + fileNameForUpload := meta.DisplayName + fileHandleForUpload, err := os.Open(meta.TempFile) if err != nil { - log.Printf("Error re-opening temp file %s for job %s: %v", meta.TempFile, jobID, err) + log.Printf("Goroutine Error: Failed to re-open temp file %s for job %s (uploading as %s): %v", + meta.TempFile, jobID, fileNameForUpload, err) resultsChan <- UploadResult{ JobID: jobID, - DocName: fileName, + DocName: fileNameForUpload, Success: false, - Error: fmt.Sprintf("Error preparing file: %v", err), + Error: fmt.Sprintf("Error preparing file for upload: %v", err), FileSize: 0, } return } - defer fileHandle.Close() // Close this handle when done with this upload + defer fileHandleForUpload.Close() - // Get the expected file size for validation - fileInfo, statErr := fileHandle.Stat() // Stat the newly opened handle + fileInfo, statErr := fileHandleForUpload.Stat() var expectedSize int64 if statErr == nil { expectedSize = fileInfo.Size() } else { - log.Printf("Error getting file info for %s (job %s): %v", fileName, jobID, statErr) - // Continue without size check if stat fails, but log it + log.Printf("Goroutine Warning: Failed to get file info for %s (original: %s, job %s, uploading as %s): %v", + meta.TempFile, meta.OriginalFilename, jobID, fileNameForUpload, statErr) } - // Add jitter delay for large batch uploads (more than 10 jobs) if len(jobs) > 10 { jitter := time.Duration(100+(time.Now().UnixNano()%400)) * time.Millisecond time.Sleep(jitter) } - // Wrap with size tracker - sizeTracker := &readCloserWithSize{reader: fileHandle, size: 0} - - log.Printf("Starting to stream file %s to job %s from temp file %s", fileName, jobID, meta.TempFile) - - // Call ServiceTrade API with the file reader - uploadStart := time.Now() - result, err := session.UploadAttachmentFile(jobID, fileName, meta.Type, sizeTracker) - uploadDuration := time.Since(uploadStart) + sizeTracker := &readCloserWithSize{reader: fileHandleForUpload, size: 0} + log.Printf("Goroutine Info: Starting to stream file %s (original: %s, uploading as %s, type: %s) to job %s from temp file %s", + meta.TempFile, meta.OriginalFilename, fileNameForUpload, meta.Type, jobID, meta.TempFile) - // Get the actual size that was uploaded - fileSize := sizeTracker.Size() + // Define uploadStart here for per-goroutine timing + uploadStartGoroutine := time.Now() + uploadResultData, errUpload := session.UploadAttachmentFile(jobID, fileNameForUpload, meta.Type, sizeTracker) + uploadDuration := time.Since(uploadStartGoroutine) - // Verify the upload size matches the expected file size + fileSizeUploaded := sizeTracker.Size() sizeMatch := true - if expectedSize > 0 && math.Abs(float64(expectedSize-fileSize)) > float64(expectedSize)*0.05 { // Allow 5% tolerance + if expectedSize > 0 && math.Abs(float64(expectedSize-fileSizeUploaded)) > float64(expectedSize)*0.05 { sizeMatch = false - log.Printf("WARNING: Size mismatch for %s to job %s. Expected: %d, Uploaded: %d", - fileName, jobID, expectedSize, fileSize) + log.Printf("Goroutine WARNING: Size mismatch for %s (original: %s, uploaded as %s) to job %s. Expected: %d, Uploaded: %d", + meta.TempFile, meta.OriginalFilename, fileNameForUpload, jobID, expectedSize, fileSizeUploaded) } - if err != nil { - log.Printf("Error uploading %s to job %s after %v: %v", - fileName, jobID, uploadDuration, err) + if errUpload != nil { + log.Printf("Goroutine Error: Uploading %s (original: %s, as %s) to job %s failed after %v: %v", + meta.TempFile, meta.OriginalFilename, fileNameForUpload, jobID, uploadDuration, errUpload) resultsChan <- UploadResult{ JobID: jobID, - DocName: fileName, + DocName: fileNameForUpload, Success: false, - Error: err.Error(), - FileSize: fileSize, + Error: errUpload.Error(), + FileSize: fileSizeUploaded, } } else if !sizeMatch { - // API returned success, but we detected size mismatch - log.Printf("Corrupted upload of %s to job %s detected. API returned success but file sizes don't match.", - fileName, jobID) + log.Printf("Goroutine Error: Upload of %s (original: %s, as %s) to job %s appears corrupted. API reported success but file sizes mismatch.", + meta.TempFile, meta.OriginalFilename, fileNameForUpload, jobID) resultsChan <- UploadResult{ JobID: jobID, - DocName: fileName, + DocName: fileNameForUpload, Success: false, Error: "Upload appears corrupted (file size mismatch)", - FileSize: fileSize, + FileSize: fileSizeUploaded, } } else { - log.Printf("Successfully uploaded %s (%.2f MB) to job %s in %v", - fileName, float64(fileSize)/(1024*1024), jobID, uploadDuration) + log.Printf("Goroutine Success: Uploaded %s (original: %s, %.2f MB, as %s, type: %s) to job %s in %v", + meta.TempFile, meta.OriginalFilename, float64(fileSizeUploaded)/(1024*1024), fileNameForUpload, meta.Type, jobID, uploadDuration) resultsChan <- UploadResult{ JobID: jobID, - DocName: fileName, + DocName: fileNameForUpload, Success: true, - Data: result, - FileSize: fileSize, + Data: uploadResultData, + FileSize: fileSizeUploaded, } } - }(jobID, currentMetadata) // Pass the captured metadata + }(jobID, currentUploadMetadata) } } - // NOTE: The deferred cleanup function for temp files defined earlier will run after this point. - - // Close the results channel when all uploads are done go func() { wg.Wait() close(resultsChan) log.Println("All upload goroutines finished.") }() - // Collect results results := make(map[string][]UploadResult) resultsCount := 0 var totalBytesUploaded int64 @@ -471,11 +386,9 @@ func UploadDocumentsHandler(w http.ResponseWriter, r *http.Request) { log.Printf("Received result %d/%d: Job %s, File %s, Success: %v, Size: %.2f MB", resultsCount, totalUploads, result.JobID, result.DocName, result.Success, float64(result.FileSize)/(1024*1024)) - if result.Success { totalBytesUploaded += result.FileSize } - if _, exists := results[result.JobID]; !exists { results[result.JobID] = []UploadResult{} } @@ -485,10 +398,7 @@ func UploadDocumentsHandler(w http.ResponseWriter, r *http.Request) { log.Printf("All results collected. Total: %d, Total bytes uploaded: %.2f MB", resultsCount, float64(totalBytesUploaded)/(1024*1024)) - // Generate HTML for results var resultHTML bytes.Buffer - - // Count successes and failures var totalSuccess, totalFailure int for _, jobResults := range results { for _, result := range jobResults { @@ -500,39 +410,15 @@ func UploadDocumentsHandler(w http.ResponseWriter, r *http.Request) { } } - // Add summary section resultHTML.WriteString("
") resultHTML.WriteString("

Upload Results

") resultHTML.WriteString("
") - - // Total jobs stat - resultHTML.WriteString("
") - resultHTML.WriteString(fmt.Sprintf("
%d
", len(results))) - resultHTML.WriteString("
Total Jobs
") + resultHTML.WriteString(fmt.Sprintf("
%d
Total Jobs
", len(results))) + resultHTML.WriteString(fmt.Sprintf("
%d
Successful Uploads
", totalSuccess)) + resultHTML.WriteString(fmt.Sprintf("
%d
Failed Uploads
", totalFailure)) + resultHTML.WriteString(fmt.Sprintf("
%d
Files Processed
", resultsCount)) resultHTML.WriteString("
") - // Success stat - resultHTML.WriteString("
") - resultHTML.WriteString(fmt.Sprintf("
%d
", totalSuccess)) - resultHTML.WriteString("
Successful Uploads
") - resultHTML.WriteString("
") - - // Failure stat - resultHTML.WriteString("
") - resultHTML.WriteString(fmt.Sprintf("
%d
", totalFailure)) - resultHTML.WriteString("
Failed Uploads
") - resultHTML.WriteString("
") - - // File count stat - resultHTML.WriteString("
") - // Use resultsCount which reflects total files attempted - resultHTML.WriteString(fmt.Sprintf("
%d
", resultsCount)) - resultHTML.WriteString("
Files Processed
") - resultHTML.WriteString("
") - - resultHTML.WriteString("
") // End of upload-stats - - // Add completion message if totalFailure == 0 && resultsCount > 0 { resultHTML.WriteString("

All documents were successfully uploaded to ServiceTrade!

") } else if resultsCount == 0 { @@ -540,13 +426,9 @@ func UploadDocumentsHandler(w http.ResponseWriter, r *http.Request) { } else { resultHTML.WriteString("

Some documents failed to upload. See details below.

") } + resultHTML.WriteString("
") - resultHTML.WriteString("") // End of upload-summary - - // Add detailed job results resultHTML.WriteString("
") - - // Sort job IDs for consistent display sortedJobs := make([]string, 0, len(results)) for jobID := range results { sortedJobs = append(sortedJobs, jobID) @@ -555,8 +437,6 @@ func UploadDocumentsHandler(w http.ResponseWriter, r *http.Request) { for _, jobID := range sortedJobs { jobResults := results[jobID] - - // Determine job success status based on results for *this job* jobHasSuccess := false jobHasFailure := false for _, result := range jobResults { @@ -566,57 +446,42 @@ func UploadDocumentsHandler(w http.ResponseWriter, r *http.Request) { jobHasFailure = true } } - - // Job result row styling - jobClass := "neutral" // Default if somehow no results for a job ID + jobClass := "neutral" if jobHasSuccess && !jobHasFailure { jobClass = "success" } else if jobHasFailure { - jobClass = "error" // Prioritize showing error if any file failed for this job + jobClass = "error" } - resultHTML.WriteString(fmt.Sprintf("
", jobClass)) - resultHTML.WriteString(fmt.Sprintf("
Job ID: %s
", jobID)) // Wrap ID for better styling - - // File results + resultHTML.WriteString(fmt.Sprintf("
Job ID: %s
", jobID)) if len(jobResults) > 0 { resultHTML.WriteString("
") - - // Sort file results by name for consistency sort.Slice(jobResults, func(i, j int) bool { return jobResults[i].DocName < jobResults[j].DocName }) - for _, result := range jobResults { fileClass := "success" icon := "✓" message := "Successfully uploaded" - if !result.Success { fileClass = "error" icon = "✗" - // Sanitize error message slightly for HTML display if needed message = strings.ReplaceAll(result.Error, "<", "<") message = strings.ReplaceAll(message, ">", ">") } - resultHTML.WriteString(fmt.Sprintf("
", fileClass)) resultHTML.WriteString(fmt.Sprintf("%s", icon)) resultHTML.WriteString(fmt.Sprintf("%s:", result.DocName)) resultHTML.WriteString(fmt.Sprintf("%s", message)) resultHTML.WriteString("
") } - - resultHTML.WriteString("
") // End of file-results + resultHTML.WriteString("
") } else { - resultHTML.WriteString("

No file upload results for this job.

") // More specific message + resultHTML.WriteString("

No file upload results for this job.

") } - - resultHTML.WriteString("
") // End of job-result + resultHTML.WriteString("") } - - resultHTML.WriteString("") // End of job-results - + resultHTML.WriteString("") w.Header().Set("Content-Type", "text/html") w.Write(resultHTML.Bytes()) } @@ -645,8 +510,5 @@ func (r *readCloserWithSize) Size() int64 { return r.size } -// DocumentFieldAddHandler generates a new document field for the form -// REMOVED as it's no longer needed with the multi-file input - -// DocumentFieldRemoveHandler handles the removal of a document field -// REMOVED as it's no longer needed with the multi-file input +// DocumentFieldAddHandler and DocumentFieldRemoveHandler are REMOVED +// as they are no longer needed with the multi-file input and new chip UI. diff --git a/static/css/upload.css b/static/css/upload.css index d133a7a..165d757 100644 --- a/static/css/upload.css +++ b/static/css/upload.css @@ -449,4 +449,250 @@ 100% { transform: rotate(360deg); } +} + +/* Styles for Modal and File Chips */ + +/* Basic Modal Styling */ +.modal { + display: none; + /* Hidden by default */ + position: fixed; + /* Stay in place */ + z-index: 1001; + /* Sit on top, above overlays which are 1000 */ + left: 0; + top: 0; + width: 100%; + /* Full width */ + height: 100%; + /* Full height */ + overflow: auto; + /* Enable scroll if needed */ + background-color: rgba(0, 0, 0, 0.4); + /* Semi-transparent black backdrop for light theme */ + justify-content: center; + align-items: center; +} + +.modal-content { + background-color: var(--content-bg); + color: var(--content-text); + margin: auto; + padding: 20px; + /* Standard padding */ + border: 1px solid var(--input-border); + border-radius: 8px; + /* Consistent border-radius */ + width: 90%; + /* Responsive width */ + max-width: 500px; + /* Max width for the modal */ + box-shadow: var(--dashboard-shadow); +} + +/* Modal uses existing .form-group, ensure label is bold if that's the standard */ +.modal-content .form-group label { + /* font-weight: bold; Inherits from global .form-group label if already bold */ +} + +.modal-content .card-input { + /* Ensure modal inputs are full width and box-sizing */ + width: 100%; + box-sizing: border-box; +} + +.close-button { + color: var(--label-color); + float: right; + font-size: 1.75rem; + /* Clearer size */ + font-weight: bold; + line-height: 1; + padding: 0; + background: transparent; + border: none; + cursor: pointer; + opacity: 0.7; +} + +.close-button:hover { + color: var(--content-text); + opacity: 1; +} + +/* File Chip Styling */ +.selected-files-grid { + display: flex; + flex-wrap: wrap; + gap: 0.75rem; + /* padding: 0.5rem 0; No specific padding, gap handles spacing */ + margin-top: 1rem; + /* Retain consistent margins */ + margin-bottom: 1rem; +} + +.file-chip { + background-color: var(--input-bg); + border: 1px solid var(--input-border); + border-radius: 16px; + /* Pill shape for chips */ + padding: 0.4rem 0.8rem; + display: flex; + align-items: center; + font-size: 0.875rem; + /* Slightly smaller font for chips */ + box-shadow: var(--dashboard-shadow); + transition: background-color 0.2s, opacity 0.2s, box-shadow 0.2s; + color: var(--content-text); +} + +.file-chip:hover { + box-shadow: var(--button-shadow); + /* Use button-shadow for hover if defined and suitable */ +} + +.file-chip.removed { + opacity: 0.65; + /* Make it visibly less prominent */ + background-color: var(--progress-bg); + /* Using a neutral theme color */ + text-decoration: line-through; + box-shadow: none; + /* No shadow for removed items */ +} + +.file-chip.removed .file-chip-name { + color: var(--label-color); + /* Dim the text of removed items */ +} + +.file-chip-icon { + margin-right: 0.5rem; + font-size: 1.1em; + /* Relative to chip's font size */ + color: var(--btn-primary-bg); + /* Use a theme color for the icon */ +} + +.file-chip.removed .file-chip-icon { + color: var(--label-color); + /* Dimmed icon for removed items */ +} + +.file-chip-name { + margin-right: 0.5rem; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + max-width: 150px; + /* Adjust as needed */ + cursor: pointer; + /* For the title attribute & clickability */ +} + +.file-chip-doctype { + font-size: 0.8em; + /* Smaller than the filename */ + color: var(--label-color); + background-color: var(--content-bg); + /* Slightly different background */ + padding: 0.1em 0.4em; + border-radius: 4px; + margin-right: 0.5rem; + white-space: nowrap; +} + +.file-chip-edit, +.file-chip-remove { + background: none; + border: none; + color: var(--label-color); + /* Subtler color for actions */ + cursor: pointer; + font-size: 1em; + /* Relative to chip's font size */ + padding: 0 0.25rem; + margin-left: 0.25rem; + /* Slight spacing */ + line-height: 1; + opacity: 0.8; +} + +.file-chip-edit:hover, +.file-chip-remove:hover { + color: var(--content-text); + /* Darker on hover for clarity */ + opacity: 1; +} + +.file-chip.removed .file-chip-edit, +.file-chip.removed .file-chip-remove { + /* Actions on removed chips */ + color: var(--label-color); + cursor: not-allowed; + opacity: 0.5; + /* Make them more faded */ +} + +#no-files-selected-placeholder { + color: var(--label-color); + font-style: italic; + width: 100%; + /* Take full width */ + text-align: center; + /* Center placeholder text */ + padding: 1rem 0; + /* Give it some space */ +} + +/* Modal Buttons Styling */ +.modal-content .form-actions { + /* If you have a .form-actions div in modal */ + margin-top: 1.5rem; + /* Space above action buttons */ + text-align: right; + /* Align buttons to the right */ +} + +.modal-content .btn-primary, +.modal-content .btn-secondary { + margin-left: 0.5rem; + /* Space between buttons if aligned right */ +} + +/* If not using .form-actions, direct styling: */ +/* .modal-content > button { margin-top: 1rem; margin-right: 0.5rem; } */ + + +/* Dark theme adjustments for modal backdrop & specific modal elements */ +/* Using @media (prefers-color-scheme: dark) as per existing structure at end of file */ +@media (prefers-color-scheme: dark) { + .modal { + background-color: rgba(20, 20, 20, 0.75); + /* Darker, more opaque backdrop for dark theme */ + } + + .close-button { + /* Ensure close button is visible in dark mode */ + color: var(--label-color); + /* Should pick up dark theme var */ + } + + .close-button:hover { + color: var(--content-text); + /* Should pick up dark theme var */ + } + + /* Specific modal button styling for dark theme if needed, e.g., for btn-secondary */ + .modal-content .btn-secondary { + /* Assuming global .btn-secondary might not be fully dark-theme aware */ + /* background-color: var(--input-bg); Picks up dark theme variable */ + /* color: var(--content-text); Picks up dark theme variable */ + /* border: 1px solid var(--input-border); Picks up dark theme variable */ + } + + /* .modal-content .btn-secondary:hover { */ + /* background-color: var(--content-bg); Slightly different hover for dark */ + /* } */ } \ No newline at end of file diff --git a/static/js/htmx.min.js b/static/js/htmx.min.js deleted file mode 100644 index 6763086..0000000 --- a/static/js/htmx.min.js +++ /dev/null @@ -1 +0,0 @@ -Redirecting to https://unpkg.com/htmx.org@1.9.12/dist/htmx.min.js \ No newline at end of file diff --git a/templates/layout.html b/templates/layout.html index 0443247..f9404bb 100644 --- a/templates/layout.html +++ b/templates/layout.html @@ -52,7 +52,6 @@ - \ No newline at end of file diff --git a/templates/partials/document_upload.html b/templates/partials/document_upload.html index da66847..ec53e05 100644 --- a/templates/partials/document_upload.html +++ b/templates/partials/document_upload.html @@ -49,7 +49,7 @@