diff --git a/.gitignore b/.gitignore index 8b9fe5df9..100410d68 100644 --- a/.gitignore +++ b/.gitignore @@ -43,6 +43,7 @@ app/core/src/main/resources/static/og_images/ app/core/src/main/resources/static/samples/ app/core/src/main/resources/static/manifest-classic.json app/core/src/main/resources/static/robots.txt +app/core/src/main/resources/static/pdfium/ # Note: Keep backend-managed files like fonts/, css/, js/, pdfjs/, etc. # Gradle diff --git a/app/common/src/main/java/stirling/software/common/service/MobileScannerService.java b/app/common/src/main/java/stirling/software/common/service/MobileScannerService.java new file mode 100644 index 000000000..4b6137a1d --- /dev/null +++ b/app/common/src/main/java/stirling/software/common/service/MobileScannerService.java @@ -0,0 +1,291 @@ +package stirling.software.common.service; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; +import org.springframework.web.multipart.MultipartFile; + +import lombok.extern.slf4j.Slf4j; + +/** + * Service for handling mobile scanner file uploads and temporary storage. Files are stored + * temporarily and automatically cleaned up after 10 minutes or upon retrieval. + */ +@Service +@Slf4j +public class MobileScannerService { + + private static final long SESSION_TIMEOUT_MS = 10 * 60 * 1000; // 10 minutes + private final Map activeSessions = new ConcurrentHashMap<>(); + private final Path tempDirectory; + + public MobileScannerService() throws IOException { + // Create temp directory for mobile scanner uploads + this.tempDirectory = + Paths.get(System.getProperty("java.io.tmpdir"), "stirling-mobile-scanner"); + Files.createDirectories(tempDirectory); + log.info("Mobile scanner temp directory: {}", tempDirectory); + } + + /** + * Stores uploaded files for a session + * + * @param sessionId Unique session identifier + * @param files Files to upload + * @throws IOException If file storage fails + */ + public void uploadFiles(String sessionId, List files) throws IOException { + validateSessionId(sessionId); + + SessionData session = + activeSessions.computeIfAbsent(sessionId, id -> new SessionData(sessionId)); + + // Create session directory + Path sessionDir = tempDirectory.resolve(sessionId); + Files.createDirectories(sessionDir); + + // Save each file + for (MultipartFile file : files) { + if (file.isEmpty()) { + continue; + } + + String originalFilename = file.getOriginalFilename(); + if (originalFilename == null || originalFilename.isBlank()) { + originalFilename = "upload-" + System.currentTimeMillis(); + } + + // Sanitize filename + String safeFilename = sanitizeFilename(originalFilename); + Path filePath = sessionDir.resolve(safeFilename); + + // Handle duplicate filenames + int counter = 1; + while (Files.exists(filePath)) { + String nameWithoutExt = safeFilename.replaceFirst("[.][^.]+$", ""); + String ext = + safeFilename.contains(".") + ? safeFilename.substring(safeFilename.lastIndexOf(".")) + : ""; + safeFilename = nameWithoutExt + "-" + counter + ext; + filePath = sessionDir.resolve(safeFilename); + counter++; + } + + file.transferTo(filePath); + session.addFile(new FileMetadata(safeFilename, file.getSize(), file.getContentType())); + log.info( + "Uploaded file for session {}: {} ({} bytes)", + sessionId, + safeFilename, + file.getSize()); + } + + session.updateLastAccess(); + } + + /** + * Retrieves file metadata for a session + * + * @param sessionId Session identifier + * @return List of file metadata, or empty list if session doesn't exist + */ + public List getSessionFiles(String sessionId) { + SessionData session = activeSessions.get(sessionId); + if (session == null) { + return List.of(); + } + session.updateLastAccess(); + return new ArrayList<>(session.getFiles()); + } + + /** + * Retrieves actual file data for download + * + * @param sessionId Session identifier + * @param filename Filename to retrieve + * @return File path + * @throws IOException If file not found or session doesn't exist + */ + public Path getFile(String sessionId, String filename) throws IOException { + SessionData session = activeSessions.get(sessionId); + if (session == null) { + throw new IOException("Session not found: " + sessionId); + } + + Path filePath = tempDirectory.resolve(sessionId).resolve(filename); + if (!Files.exists(filePath)) { + throw new IOException("File not found: " + filename); + } + + session.updateLastAccess(); + session.markFileAsDownloaded(filename); + return filePath; + } + + /** + * Deletes a file after it has been served to the client. Should be called after successful + * download. + * + * @param sessionId Session identifier + * @param filename Filename to delete + */ + public void deleteFileAfterDownload(String sessionId, String filename) { + try { + Path filePath = tempDirectory.resolve(sessionId).resolve(filename); + Files.deleteIfExists(filePath); + log.info("Deleted file after download: {}/{}", sessionId, filename); + + // Check if all files have been downloaded - if so, delete the entire session + SessionData session = activeSessions.get(sessionId); + if (session != null && session.allFilesDownloaded()) { + deleteSession(sessionId); + log.info("All files downloaded - deleted session: {}", sessionId); + } + } catch (IOException e) { + log.warn("Failed to delete file after download: {}/{}", sessionId, filename, e); + } + } + + /** + * Deletes a session and all its files + * + * @param sessionId Session to delete + */ + public void deleteSession(String sessionId) { + SessionData session = activeSessions.remove(sessionId); + if (session != null) { + try { + Path sessionDir = tempDirectory.resolve(sessionId); + if (Files.exists(sessionDir)) { + // Delete all files in session directory + Files.walk(sessionDir) + .sorted( + (a, b) -> + -a.compareTo(b)) // Reverse order to delete files before + // directory + .forEach( + path -> { + try { + Files.deleteIfExists(path); + } catch (IOException e) { + log.warn("Failed to delete file: {}", path, e); + } + }); + } + log.info("Deleted session: {}", sessionId); + } catch (IOException e) { + log.error("Error deleting session directory: {}", sessionId, e); + } + } + } + + /** Scheduled cleanup of expired sessions (runs every 5 minutes) */ + @Scheduled(fixedRate = 5 * 60 * 1000) + public void cleanupExpiredSessions() { + long now = System.currentTimeMillis(); + List expiredSessions = new ArrayList<>(); + + activeSessions.forEach( + (sessionId, session) -> { + if (now - session.getLastAccessTime() > SESSION_TIMEOUT_MS) { + expiredSessions.add(sessionId); + } + }); + + if (!expiredSessions.isEmpty()) { + log.info("Cleaning up {} expired mobile scanner sessions", expiredSessions.size()); + expiredSessions.forEach(this::deleteSession); + } + } + + private void validateSessionId(String sessionId) { + if (sessionId == null || sessionId.isBlank()) { + throw new IllegalArgumentException("Session ID cannot be empty"); + } + // Basic validation: alphanumeric and hyphens only + if (!sessionId.matches("[a-zA-Z0-9-]+")) { + throw new IllegalArgumentException("Invalid session ID format"); + } + } + + private String sanitizeFilename(String filename) { + // Remove path traversal attempts and dangerous characters + return filename.replaceAll("[^a-zA-Z0-9._-]", "_"); + } + + /** File metadata for client */ + public static class FileMetadata { + private final String filename; + private final long size; + private final String contentType; + + public FileMetadata(String filename, long size, String contentType) { + this.filename = filename; + this.size = size; + this.contentType = contentType; + } + + public String getFilename() { + return filename; + } + + public long getSize() { + return size; + } + + public String getContentType() { + return contentType; + } + } + + /** Session data tracking */ + private static class SessionData { + private final String sessionId; + private final List files = new ArrayList<>(); + private final Map downloadedFiles = new HashMap<>(); + private final long createdAt; + private long lastAccessTime; + + public SessionData(String sessionId) { + this.sessionId = sessionId; + this.createdAt = System.currentTimeMillis(); + this.lastAccessTime = createdAt; + } + + public void addFile(FileMetadata file) { + files.add(file); + downloadedFiles.put(file.getFilename(), false); + } + + public List getFiles() { + return files; + } + + public void markFileAsDownloaded(String filename) { + downloadedFiles.put(filename, true); + } + + public boolean allFilesDownloaded() { + return !downloadedFiles.isEmpty() + && downloadedFiles.values().stream().allMatch(downloaded -> downloaded); + } + + public void updateLastAccess() { + this.lastAccessTime = System.currentTimeMillis(); + } + + public long getLastAccessTime() { + return lastAccessTime; + } + } +} diff --git a/app/common/src/main/java/stirling/software/common/util/RequestUriUtils.java b/app/common/src/main/java/stirling/software/common/util/RequestUriUtils.java index acad6f4a9..31321e49d 100644 --- a/app/common/src/main/java/stirling/software/common/util/RequestUriUtils.java +++ b/app/common/src/main/java/stirling/software/common/util/RequestUriUtils.java @@ -52,6 +52,11 @@ public class RequestUriUtils { return true; } + // Mobile scanner page for QR code-based file uploads (peer-to-peer, no backend auth needed) + if (normalizedUri.startsWith("/mobile-scanner")) { + return true; + } + // Treat common static file extensions as static resources return normalizedUri.endsWith(".svg") || normalizedUri.endsWith(".png") @@ -168,6 +173,8 @@ public class RequestUriUtils { "/api/v1/ui-data/footer-info") // Public footer configuration || trimmedUri.startsWith("/api/v1/invite/validate") || trimmedUri.startsWith("/api/v1/invite/accept") + || trimmedUri.startsWith( + "/api/v1/mobile-scanner/") // Mobile scanner endpoints (no auth) || trimmedUri.startsWith("/v1/api-docs"); } diff --git a/app/core/src/main/java/stirling/software/SPDF/config/CleanUrlInterceptor.java b/app/core/src/main/java/stirling/software/SPDF/config/CleanUrlInterceptor.java index 785743731..7d11efa7f 100644 --- a/app/core/src/main/java/stirling/software/SPDF/config/CleanUrlInterceptor.java +++ b/app/core/src/main/java/stirling/software/SPDF/config/CleanUrlInterceptor.java @@ -32,7 +32,8 @@ public class CleanUrlInterceptor implements HandlerInterceptor { "principal", "startDate", "endDate", - "async"); + "async", + "session"); @Override public boolean preHandle( diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/ConfigController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/ConfigController.java index 2f8d4b62b..f7d972466 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/ConfigController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/ConfigController.java @@ -71,6 +71,10 @@ public class ConfigController { configData.put("contextPath", appConfig.getContextPath()); configData.put("serverPort", appConfig.getServerPort()); + // Add frontendUrl for mobile scanner QR codes + String frontendUrl = applicationProperties.getSystem().getFrontendUrl(); + configData.put("frontendUrl", frontendUrl != null ? frontendUrl : ""); + // Extract values from ApplicationProperties configData.put("appNameNavbar", applicationProperties.getUi().getAppNameNavbar()); configData.put("languages", applicationProperties.getUi().getLanguages()); diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/MobileScannerController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/MobileScannerController.java new file mode 100644 index 000000000..bb88d2287 --- /dev/null +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/MobileScannerController.java @@ -0,0 +1,219 @@ +package stirling.software.SPDF.controller.api.misc; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.springframework.core.io.Resource; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; + +import io.swagger.v3.oas.annotations.Hidden; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.tags.Tag; + +import lombok.extern.slf4j.Slf4j; + +import stirling.software.common.service.MobileScannerService; +import stirling.software.common.service.MobileScannerService.FileMetadata; + +/** + * REST controller for mobile scanner functionality. Allows mobile devices to upload scanned images + * that can be retrieved by desktop clients via a session-based system. No authentication required + * for peer-to-peer scanning workflow. + */ +@RestController +@RequestMapping("/api/v1/mobile-scanner") +@Tag( + name = "Mobile Scanner", + description = + "Endpoints for mobile-to-desktop file transfer via QR code scanning. " + + "Files are temporarily stored and automatically cleaned up after 10 minutes.") +@Hidden +@Slf4j +public class MobileScannerController { + + private final MobileScannerService mobileScannerService; + + public MobileScannerController(MobileScannerService mobileScannerService) { + this.mobileScannerService = mobileScannerService; + } + + /** + * Upload files from mobile device + * + * @param sessionId Unique session identifier from QR code + * @param files Files to upload + * @return Upload status + */ + @PostMapping("/upload/{sessionId}") + @Operation( + summary = "Upload scanned files from mobile device", + description = "Mobile devices upload scanned images to a temporary session") + @ApiResponse( + responseCode = "200", + description = "Files uploaded successfully", + content = @Content(schema = @Schema(implementation = UploadResponse.class))) + @ApiResponse(responseCode = "400", description = "Invalid session ID or files") + @ApiResponse(responseCode = "500", description = "Upload failed") + public ResponseEntity> uploadFiles( + @Parameter(description = "Session ID from QR code", required = true) @PathVariable + String sessionId, + @Parameter(description = "Files to upload", required = true) @RequestParam("files") + List files) { + + try { + if (files == null || files.isEmpty()) { + return ResponseEntity.badRequest().body(Map.of("error", "No files provided")); + } + + mobileScannerService.uploadFiles(sessionId, files); + + Map response = new HashMap<>(); + response.put("success", true); + response.put("sessionId", sessionId); + response.put("filesUploaded", files.size()); + response.put("message", "Files uploaded successfully"); + + log.info("Mobile scanner upload: session={}, files={}", sessionId, files.size()); + return ResponseEntity.ok(response); + + } catch (IllegalArgumentException e) { + log.warn("Invalid mobile scanner upload request: {}", e.getMessage()); + return ResponseEntity.badRequest().body(Map.of("error", e.getMessage())); + } catch (IOException e) { + log.error("Failed to upload files for session: {}", sessionId, e); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .body(Map.of("error", "Failed to save files")); + } + } + + /** + * Get list of uploaded files for a session + * + * @param sessionId Session identifier + * @return List of file metadata + */ + @GetMapping("/files/{sessionId}") + @Operation( + summary = "Get uploaded files for a session", + description = "Desktop clients poll this endpoint to check for new uploads") + @ApiResponse( + responseCode = "200", + description = "File list retrieved", + content = @Content(schema = @Schema(implementation = FileListResponse.class))) + public ResponseEntity> getSessionFiles( + @Parameter(description = "Session ID", required = true) @PathVariable + String sessionId) { + + List files = mobileScannerService.getSessionFiles(sessionId); + + Map response = new HashMap<>(); + response.put("sessionId", sessionId); + response.put("files", files); + response.put("count", files.size()); + + return ResponseEntity.ok(response); + } + + /** + * Download a specific file from a session + * + * @param sessionId Session identifier + * @param filename Filename to download + * @return File content + */ + @GetMapping("/download/{sessionId}/{filename}") + @Operation( + summary = "Download a specific file", + description = + "Download a file that was uploaded to a session. File is automatically deleted after download.") + @ApiResponse(responseCode = "200", description = "File downloaded successfully") + @ApiResponse(responseCode = "404", description = "File or session not found") + public ResponseEntity downloadFile( + @Parameter(description = "Session ID", required = true) @PathVariable String sessionId, + @Parameter(description = "Filename to download", required = true) @PathVariable + String filename) { + + try { + Path filePath = mobileScannerService.getFile(sessionId, filename); + + // Read file into memory first, so we can delete it before sending + byte[] fileBytes = Files.readAllBytes(filePath); + + String contentType = Files.probeContentType(filePath); + if (contentType == null) { + contentType = MediaType.APPLICATION_OCTET_STREAM_VALUE; + } + + // Delete file immediately after reading into memory (server-side cleanup) + mobileScannerService.deleteFileAfterDownload(sessionId, filename); + + // Serve from memory + Resource resource = new org.springframework.core.io.ByteArrayResource(fileBytes); + + return ResponseEntity.ok() + .contentType(MediaType.parseMediaType(contentType)) + .header( + HttpHeaders.CONTENT_DISPOSITION, + "attachment; filename=\"" + filename + "\"") + .body(resource); + + } catch (IOException e) { + log.warn("File not found: session={}, file={}", sessionId, filename); + return ResponseEntity.notFound().build(); + } + } + + /** + * Delete a session and all its files + * + * @param sessionId Session to delete + * @return Deletion status + */ + @DeleteMapping("/session/{sessionId}") + @Operation( + summary = "Delete a session", + description = "Manually delete a session and all its uploaded files") + @ApiResponse(responseCode = "200", description = "Session deleted successfully") + public ResponseEntity> deleteSession( + @Parameter(description = "Session ID to delete", required = true) @PathVariable + String sessionId) { + + mobileScannerService.deleteSession(sessionId); + + return ResponseEntity.ok( + Map.of("success", true, "sessionId", sessionId, "message", "Session deleted")); + } + + // Response schemas for OpenAPI documentation + private static class UploadResponse { + public boolean success; + public String sessionId; + public int filesUploaded; + public String message; + } + + private static class FileListResponse { + public String sessionId; + public List files; + public int count; + } +} diff --git a/app/core/src/main/resources/static/css/cookieconsentCustomisation.css b/app/core/src/main/resources/static/css/cookieconsentCustomisation.css index 86ff70bfb..ec360c20b 100644 --- a/app/core/src/main/resources/static/css/cookieconsentCustomisation.css +++ b/app/core/src/main/resources/static/css/cookieconsentCustomisation.css @@ -1,53 +1,99 @@ +/* Light theme variables */ +:root { + --cc-bg: #ffffff; + --cc-primary-color: #1c1c1c; + --cc-secondary-color: #666666; + + --cc-btn-primary-bg: #007BFF; + --cc-btn-primary-color: #ffffff; + --cc-btn-primary-border-color: #007BFF; + --cc-btn-primary-hover-bg: #0056b3; + --cc-btn-primary-hover-color: #ffffff; + --cc-btn-primary-hover-border-color: #0056b3; + + --cc-btn-secondary-bg: #f1f3f4; + --cc-btn-secondary-color: #1c1c1c; + --cc-btn-secondary-border-color: #f1f3f4; + --cc-btn-secondary-hover-bg: #007BFF; + --cc-btn-secondary-hover-color: #ffffff; + --cc-btn-secondary-hover-border-color: #007BFF; + + --cc-separator-border-color: #e0e0e0; + + --cc-toggle-on-bg: #007BFF; + --cc-toggle-off-bg: #667481; + --cc-toggle-on-knob-bg: #ffffff; + --cc-toggle-off-knob-bg: #ffffff; + + --cc-toggle-enabled-icon-color: #ffffff; + --cc-toggle-disabled-icon-color: #ffffff; + + --cc-toggle-readonly-bg: #f1f3f4; + --cc-toggle-readonly-knob-bg: #79747E; + --cc-toggle-readonly-knob-icon-color: #f1f3f4; + + --cc-section-category-border: #e0e0e0; + + --cc-cookie-category-block-bg: #f1f3f4; + --cc-cookie-category-block-border: #f1f3f4; + --cc-cookie-category-block-hover-bg: #e9eff4; + --cc-cookie-category-block-hover-border: #e9eff4; + + --cc-cookie-category-expanded-block-bg: #f1f3f4; + --cc-cookie-category-expanded-block-hover-bg: #e9eff4; + + --cc-footer-bg: #ffffff; + --cc-footer-color: #1c1c1c; + --cc-footer-border-color: #ffffff; +} + +/* Dark theme variables */ .cc--darkmode{ - --cc-bg: var(--md-sys-color-inverse-on-surface); - --cc-primary-color: var(--md-sys-color-on-surface); - --cc-secondary-color: var(--md-sys-color-on-surface); + --cc-bg: #2d2d2d; + --cc-primary-color: #e5e5e5; + --cc-secondary-color: #b0b0b0; - --cc-btn-primary-bg: var(--md-sys-color-secondary); - --cc-btn-primary-color: var(--cc-bg); - --cc-btn-primary-border-color: var(--cc-btn-primary-bg); - --cc-btn-primary-hover-bg: var(--md-sys-color-surface-3); - --cc-btn-primary-hover-color: var(--md-sys-color-on-secondary-container); - --cc-btn-primary-hover-border-color: var(--md-sys-color-surface-3); + --cc-btn-primary-bg: #4dabf7; + --cc-btn-primary-color: #ffffff; + --cc-btn-primary-border-color: #4dabf7; + --cc-btn-primary-hover-bg: #3d3d3d; + --cc-btn-primary-hover-color: #ffffff; + --cc-btn-primary-hover-border-color: #3d3d3d; - --cc-btn-secondary-bg: var(--md-sys-color-surface-3); - --cc-btn-secondary-color: var(--md-sys-color-on-secondary-container); - --cc-btn-secondary-border-color: var(--md-sys-color-surface-3); - --cc-btn-secondary-hover-bg:var(--md-sys-color-secondary); - --cc-btn-secondary-hover-color: var(--cc-bg); - --cc-btn-secondary-hover-border-color: var(--md-sys-color-secondary); + --cc-btn-secondary-bg: #3d3d3d; + --cc-btn-secondary-color: #ffffff; + --cc-btn-secondary-border-color: #3d3d3d; + --cc-btn-secondary-hover-bg: #4dabf7; + --cc-btn-secondary-hover-color: #ffffff; + --cc-btn-secondary-hover-border-color: #4dabf7; - --cc-separator-border-color: var(--md-sys-color-outline); + --cc-separator-border-color: #555555; - --cc-toggle-on-bg: var(--cc-btn-primary-bg); - --cc-toggle-off-bg: var(--md-sys-color-outline); - --cc-toggle-on-knob-bg: var(--cc-btn-primary-color); - --cc-toggle-off-knob-bg: var(--cc-btn-primary-color); + --cc-toggle-on-bg: #4dabf7; + --cc-toggle-off-bg: #667481; + --cc-toggle-on-knob-bg: #2d2d2d; + --cc-toggle-off-knob-bg: #2d2d2d; - --cc-toggle-enabled-icon-color: var(--cc-btn-primary-color); - --cc-toggle-disabled-icon-color: var(--cc-btn-primary-color); + --cc-toggle-enabled-icon-color: #2d2d2d; + --cc-toggle-disabled-icon-color: #2d2d2d; - --cc-toggle-readonly-bg: var(--md-sys-color-surface); - --cc-toggle-readonly-knob-bg: var(--md-sys-color-outline); - --cc-toggle-readonly-knob-icon-color: var(--cc-toggle-readonly-bg); + --cc-toggle-readonly-bg: #555555; + --cc-toggle-readonly-knob-bg: #8e8e8e; + --cc-toggle-readonly-knob-icon-color: #555555; - --cc-section-category-border: var(--md-sys-color-outline); + --cc-section-category-border: #555555; - --cc-cookie-category-block-bg: var(--cc-btn-secondary-bg); - --cc-cookie-category-block-border: var(--cc-btn-secondary-bg); - --cc-cookie-category-block-hover-bg: var(--cc-btn-secondary-bg); - --cc-cookie-category-block-hover-border: var(--cc-btn-secondary-bg); + --cc-cookie-category-block-bg: #3d3d3d; + --cc-cookie-category-block-border: #3d3d3d; + --cc-cookie-category-block-hover-bg: #4d4d4d; + --cc-cookie-category-block-hover-border: #4d4d4d; + + --cc-cookie-category-expanded-block-bg: #3d3d3d; + --cc-cookie-category-expanded-block-hover-bg: #4d4d4d; - --cc-cookie-category-expanded-block-bg: var(--cc-btn-secondary-bg); - --cc-cookie-category-expanded-block-hover-bg: var(--cc-toggle-readonly-bg); - - /* --cc-overlay-bg: rgba(0, 0, 0, 0.65); - --cc-webkit-scrollbar-bg: var(--cc-section-category-border); - --cc-webkit-scrollbar-hover-bg: var(--cc-btn-primary-hover-bg); -*/ - --cc-footer-bg: var(--cc-bg); - --cc-footer-color: var(--cc-primary-color); - --cc-footer-border-color: var(--cc-bg); + --cc-footer-bg: #2d2d2d; + --cc-footer-color: #e5e5e5; + --cc-footer-border-color: #2d2d2d; } .cm__body{ max-width: 90% !important; @@ -78,7 +124,83 @@ } } +/* Toggle visibility fixes */ +#cc-main .section__toggle { + opacity: 0 !important; /* Keep invisible but functional */ +} + +#cc-main .toggle__icon { + display: flex !important; + align-items: center !important; + justify-content: flex-start !important; +} + +#cc-main .toggle__icon-circle { + display: block !important; + position: absolute !important; + transition: transform 0.25s ease !important; +} + +#cc-main .toggle__icon-on, +#cc-main .toggle__icon-off { + display: flex !important; + align-items: center !important; + justify-content: center !important; + position: absolute !important; + width: 100% !important; + height: 100% !important; +} + +/* Ensure toggles are visible in both themes */ +#cc-main .toggle__icon { + background: var(--cc-toggle-off-bg) !important; + border: 1px solid var(--cc-toggle-off-bg) !important; +} + +#cc-main .section__toggle:checked ~ .toggle__icon { + background: var(--cc-toggle-on-bg) !important; + border: 1px solid var(--cc-toggle-on-bg) !important; +} + +/* Ensure toggle text is visible */ +#cc-main .pm__section-title { + color: var(--cc-primary-color) !important; +} + +#cc-main .pm__section-desc { + color: var(--cc-secondary-color) !important; +} + +/* Make sure the modal has proper contrast */ +#cc-main .pm { + background: var(--cc-bg) !important; + color: var(--cc-primary-color) !important; +} + /* Lower z-index so cookie banner appears behind onboarding modals */ #cc-main { z-index: 100 !important; } + +/* Ensure consent modal text is visible in both themes */ +#cc-main .cm { + background: var(--cc-bg) !important; + color: var(--cc-primary-color) !important; +} + +#cc-main .cm__title { + color: var(--cc-primary-color) !important; +} + +#cc-main .cm__desc { + color: var(--cc-primary-color) !important; +} + +#cc-main .cm__footer { + color: var(--cc-primary-color) !important; +} + +#cc-main .cm__footer-links a, +#cc-main .cm__link { + color: var(--cc-primary-color) !important; +} \ No newline at end of file diff --git a/app/core/src/main/resources/static/manifest.json b/app/core/src/main/resources/static/manifest.json index 8ca262b47..039dc00dc 100644 --- a/app/core/src/main/resources/static/manifest.json +++ b/app/core/src/main/resources/static/manifest.json @@ -1,20 +1,25 @@ { - "name": "Stirling-PDF", - "short_name": "Stirling-PDF", + "short_name": "Stirling PDF", + "name": "Stirling PDF", "icons": [ { - "src": "/android-chrome-192x192.png", - "sizes": "192x192", - "type": "image/png" + "src": "modern-logo/favicon.ico", + "sizes": "64x64 32x32 24x24 16x16", + "type": "image/x-icon" }, { - "src": "/android-chrome-512x512.png", - "sizes": "512x512", - "type": "image/png" + "src": "modern-logo/logo192.png", + "type": "image/png", + "sizes": "192x192" + }, + { + "src": "modern-logo/logo512.png", + "type": "image/png", + "sizes": "512x512" } ], - "start_url": "/", + "start_url": ".", "display": "standalone", - "background_color": "#ffffff", - "theme_color": "#000000" + "theme_color": "#000000", + "background_color": "#ffffff" } diff --git a/frontend/package-lock.json b/frontend/package-lock.json index f788474bb..2d7d40d1d 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -54,11 +54,14 @@ "globals": "^16.4.0", "i18next": "^25.5.2", "i18next-browser-languagedetector": "^8.2.0", + "jscanify": "^1.4.0", "jszip": "^3.10.1", "license-report": "^6.8.0", "pdf-lib": "^1.17.1", "pdfjs-dist": "^5.4.149", + "peerjs": "^1.5.5", "posthog-js": "^1.268.0", + "qrcode.react": "^4.1.0", "react": "^19.1.1", "react-dom": "^19.1.1", "react-i18next": "^15.7.3", @@ -373,7 +376,6 @@ "version": "5.1.0", "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz", "integrity": "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==", - "dev": true, "funding": [ { "type": "github", @@ -393,7 +395,6 @@ "version": "2.1.4", "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz", "integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==", - "dev": true, "funding": [ { "type": "github", @@ -417,7 +418,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz", "integrity": "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==", - "dev": true, "funding": [ { "type": "github", @@ -445,7 +445,6 @@ "version": "3.0.5", "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz", "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==", - "dev": true, "funding": [ { "type": "github", @@ -457,7 +456,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" }, @@ -489,7 +487,6 @@ "version": "3.0.4", "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz", "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==", - "dev": true, "funding": [ { "type": "github", @@ -501,7 +498,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" } @@ -582,7 +578,6 @@ "resolved": "https://registry.npmjs.org/@embedpdf/core/-/core-1.5.0.tgz", "integrity": "sha512-Yrh9XoVaT8cUgzgqpJ7hx5wg6BqQrCFirqqlSwVb+Ly9oNn4fZbR9GycIWmzJOU5XBnaOJjXfQSaDyoNP0woNA==", "license": "MIT", - "peer": true, "dependencies": { "@embedpdf/engines": "1.5.0", "@embedpdf/models": "1.5.0" @@ -682,7 +677,6 @@ "resolved": "https://registry.npmjs.org/@embedpdf/plugin-history/-/plugin-history-1.5.0.tgz", "integrity": "sha512-p7PTNNaIr4gH3jLwX+eLJe1DeUXgi21kVGN6SRx/pocH8esg4jqoOeD/YiRRZoZnPOiy0jBXVhkPkwSmY7a2hQ==", "license": "MIT", - "peer": true, "dependencies": { "@embedpdf/models": "1.5.0" }, @@ -699,7 +693,6 @@ "resolved": "https://registry.npmjs.org/@embedpdf/plugin-interaction-manager/-/plugin-interaction-manager-1.5.0.tgz", "integrity": "sha512-ckHgTfvkW6c5Ta7Mc+Dl9C2foVnvEpqEJ84wyBnqrU0OWbe/jsiPhyKBVeartMGqNI/kVfaQTXupyrKhekAVmg==", "license": "MIT", - "peer": true, "dependencies": { "@embedpdf/models": "1.5.0" }, @@ -717,7 +710,6 @@ "resolved": "https://registry.npmjs.org/@embedpdf/plugin-loader/-/plugin-loader-1.5.0.tgz", "integrity": "sha512-P4YpIZfaW69etYIjphyaL4cGl2pB14h3OdTE0tRQ2pZYZHFLTvlt4q9B3PVSdhlSrHK5nob7jfLGon2U7xCslg==", "license": "MIT", - "peer": true, "dependencies": { "@embedpdf/models": "1.5.0" }, @@ -771,7 +763,6 @@ "resolved": "https://registry.npmjs.org/@embedpdf/plugin-render/-/plugin-render-1.5.0.tgz", "integrity": "sha512-ywwSj0ByrlkvrJIHKRzqxARkOZriki8VJUC+T4MV8fGyF4CzvCRJyKlPktahFz+VxhoodqTh7lBCib68dH+GvA==", "license": "MIT", - "peer": true, "dependencies": { "@embedpdf/models": "1.5.0" }, @@ -806,7 +797,6 @@ "resolved": "https://registry.npmjs.org/@embedpdf/plugin-scroll/-/plugin-scroll-1.5.0.tgz", "integrity": "sha512-RNmTZCZ8X1mA8cw9M7TMDuhO9GtkOalGha2bBL3En3D1IlDRS7PzNNMSMV7eqT7OQICSTltlpJ8p8Qi5esvL/Q==", "license": "MIT", - "peer": true, "dependencies": { "@embedpdf/models": "1.5.0" }, @@ -843,7 +833,6 @@ "resolved": "https://registry.npmjs.org/@embedpdf/plugin-selection/-/plugin-selection-1.5.0.tgz", "integrity": "sha512-zrxLBAZQoPswDuf9q9DrYaQc6B0Ysc2U1hueTjNH/4+ydfl0BFXZkKR63C2e3YmWtXvKjkoIj0GyPzsiBORLUw==", "license": "MIT", - "peer": true, "dependencies": { "@embedpdf/models": "1.5.0" }, @@ -919,7 +908,6 @@ "resolved": "https://registry.npmjs.org/@embedpdf/plugin-viewport/-/plugin-viewport-1.5.0.tgz", "integrity": "sha512-G8GDyYRhfehw72+r4qKkydnA5+AU8qH67g01Y12b0DzI0VIzymh/05Z4dK8DsY3jyWPXJfw2hlg5+KDHaMBHgQ==", "license": "MIT", - "peer": true, "dependencies": { "@embedpdf/models": "1.5.0" }, @@ -1075,7 +1063,6 @@ "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz", "integrity": "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==", "license": "MIT", - "peer": true, "dependencies": { "@babel/runtime": "^7.18.3", "@emotion/babel-plugin": "^11.13.5", @@ -1119,7 +1106,6 @@ "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.14.1.tgz", "integrity": "sha512-qEEJt42DuToa3gurlH4Qqc1kVpNq8wO8cJtDzU46TjlzWjDlsVyevtYCRijVq3SrHsROS+gVQ8Fnea108GnKzw==", "license": "MIT", - "peer": true, "dependencies": { "@babel/runtime": "^7.18.3", "@emotion/babel-plugin": "^11.13.5", @@ -1979,7 +1965,6 @@ "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", - "dev": true, "license": "ISC", "dependencies": { "string-width": "^5.1.2", @@ -1997,7 +1982,6 @@ "version": "6.2.2", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", - "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -2010,7 +1994,6 @@ "version": "6.2.3", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", - "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -2023,14 +2006,12 @@ "version": "9.2.2", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true, "license": "MIT" }, "node_modules/@isaacs/cliui/node_modules/string-width": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "dev": true, "license": "MIT", "dependencies": { "eastasianwidth": "^0.2.0", @@ -2048,7 +2029,6 @@ "version": "7.1.2", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", - "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^6.0.1" @@ -2064,7 +2044,6 @@ "version": "8.1.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", - "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^6.1.0", @@ -2150,7 +2129,6 @@ "resolved": "https://registry.npmjs.org/@mantine/core/-/core-8.3.6.tgz", "integrity": "sha512-paTl+0x+O/QtgMtqVJaG8maD8sfiOdgPmLOyG485FmeGZ1L3KMdEkhxZtmdGlDFsLXhmMGQ57ducT90bvhXX5A==", "license": "MIT", - "peer": true, "dependencies": { "@floating-ui/react": "^0.27.16", "clsx": "^2.1.1", @@ -2201,7 +2179,6 @@ "resolved": "https://registry.npmjs.org/@mantine/hooks/-/hooks-8.3.6.tgz", "integrity": "sha512-liHfaWXHAkLjJy+Bkr29UsCwAoDQ/a64WrM67lksx8F0qqyjR5RQH8zVlhuOjdpQnwtlUkE/YiTvbJiPcoI0bw==", "license": "MIT", - "peer": true, "peerDependencies": { "react": "^18.x || ^19.x" } @@ -2228,6 +2205,15 @@ "@types/gapi.client.discovery-v1": "*" } }, + "node_modules/@msgpack/msgpack": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@msgpack/msgpack/-/msgpack-2.8.0.tgz", + "integrity": "sha512-h9u4u/jiIRKbq25PM+zymTyW6bhTzELvOoUd+AvYriWOAKpLGnIamaET3pnHYoI5iYphAHBI4ayx0MehR+VVPQ==", + "license": "ISC", + "engines": { + "node": ">= 10" + } + }, "node_modules/@mui/core-downloads-tracker": { "version": "7.3.5", "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-7.3.5.tgz", @@ -2269,7 +2255,6 @@ "resolved": "https://registry.npmjs.org/@mui/material/-/material-7.3.5.tgz", "integrity": "sha512-8VVxFmp1GIm9PpmnQoCoYo0UWHoOrdA57tDL62vkpzEgvb/d71Wsbv4FRg7r1Gyx7PuSo0tflH34cdl/NvfHNQ==", "license": "MIT", - "peer": true, "dependencies": { "@babel/runtime": "^7.28.4", "@mui/core-downloads-tracker": "^7.3.5", @@ -2719,7 +2704,6 @@ "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", - "dev": true, "license": "MIT", "optional": true, "engines": { @@ -3202,7 +3186,6 @@ "resolved": "https://registry.npmjs.org/@stripe/stripe-js/-/stripe-js-7.9.0.tgz", "integrity": "sha512-ggs5k+/0FUJcIgNY08aZTqpBTtbExkJMYMLSMwyucrhtWexVOEY1KJmhBsxf+E/Q15f5rbwBpj+t0t2AW2oCsQ==", "license": "MIT", - "peer": true, "engines": { "node": ">=12.16" } @@ -3321,6 +3304,7 @@ "resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.6.tgz", "integrity": "sha512-4awhxtMh4cx9blePWl10HRHj8Iivtqj+2QdDCSMDzxG+XKa9+VCNupQuCuvzEhYPzZSrX+0gC+0lHA/0fFKKQQ==", "license": "MIT", + "peer": true, "peerDependencies": { "acorn": "^8.9.0" } @@ -4097,7 +4081,6 @@ "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", @@ -4426,7 +4409,6 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz", "integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==", "license": "MIT", - "peer": true, "dependencies": { "csstype": "^3.0.2" } @@ -4437,7 +4419,6 @@ "integrity": "sha512-9KQPoO6mZCi7jcIStSnlOWn2nEF3mNmyr3rIAsGnAbQKYbRLyqmeSc39EVgtxXVia+LMT8j3knZLAZAh+xLmrw==", "dev": true, "license": "MIT", - "peer": true, "peerDependencies": { "@types/react": "^19.2.0" } @@ -4507,7 +4488,6 @@ "integrity": "sha512-6m1I5RmHBGTnUGS113G04DMu3CpSdxCAU/UvtjNWL4Nuf3MW9tQhiJqRlHzChIkhy6kZSAQmc+I1bcGjE3yNKg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.46.3", "@typescript-eslint/types": "8.46.3", @@ -5221,6 +5201,7 @@ "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.24.tgz", "integrity": "sha512-BM8kBhtlkkbnyl4q+HiF5R5BL0ycDPfihowulm02q3WYp2vxgPcJuZO866qa/0u3idbMntKEtVNuAUp5bw4teg==", "license": "MIT", + "peer": true, "dependencies": { "@vue/shared": "3.5.24" } @@ -5230,6 +5211,7 @@ "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.24.tgz", "integrity": "sha512-RYP/byyKDgNIqfX/gNb2PB55dJmM97jc9wyF3jK7QUInYKypK2exmZMNwnjueWwGceEkP6NChd3D2ZVEp9undQ==", "license": "MIT", + "peer": true, "dependencies": { "@vue/reactivity": "3.5.24", "@vue/shared": "3.5.24" @@ -5240,6 +5222,7 @@ "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.24.tgz", "integrity": "sha512-Z8ANhr/i0XIluonHVjbUkjvn+CyrxbXRIxR7wn7+X7xlcb7dJsfITZbkVOeJZdP8VZwfrWRsWdShH6pngMxRjw==", "license": "MIT", + "peer": true, "dependencies": { "@vue/reactivity": "3.5.24", "@vue/runtime-core": "3.5.24", @@ -5252,6 +5235,7 @@ "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.24.tgz", "integrity": "sha512-Yh2j2Y4G/0/4z/xJ1Bad4mxaAk++C2v4kaa8oSYTMJBJ00/ndPuxCnWeot0/7/qafQFLh5pr6xeV6SdMcE/G1w==", "license": "MIT", + "peer": true, "dependencies": { "@vue/compiler-ssr": "3.5.24", "@vue/shared": "3.5.24" @@ -5278,7 +5262,6 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -5300,7 +5283,6 @@ "version": "7.1.4", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", - "dev": true, "license": "MIT", "engines": { "node": ">= 14" @@ -5327,7 +5309,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -5337,7 +5318,6 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -5381,7 +5361,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true, "license": "Python-2.0" }, "node_modules/aria-query": { @@ -5686,6 +5665,7 @@ "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", "license": "Apache-2.0", + "peer": true, "engines": { "node": ">= 0.4" } @@ -5724,7 +5704,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, "license": "MIT" }, "node_modules/bare-events": { @@ -5828,7 +5807,6 @@ "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "dev": true, "funding": [ { "type": "github", @@ -5897,7 +5875,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", - "dev": true, "license": "MIT", "dependencies": { "buffer": "^5.5.0", @@ -5909,7 +5886,6 @@ "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "dev": true, "license": "MIT", "dependencies": { "inherits": "^2.0.3", @@ -5924,7 +5900,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", - "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" @@ -5943,6 +5918,12 @@ "node": ">=8" } }, + "node_modules/browser-stdout": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", + "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", + "license": "ISC" + }, "node_modules/browserslist": { "version": "4.27.0", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.27.0.tgz", @@ -5962,7 +5943,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.19", "caniuse-lite": "^1.0.30001751", @@ -5981,7 +5961,6 @@ "version": "5.7.1", "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "dev": true, "funding": [ { "type": "github", @@ -6143,6 +6122,18 @@ "node": ">=6" } }, + "node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/camelcase-css": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", @@ -6173,6 +6164,20 @@ ], "license": "CC-BY-4.0" }, + "node_modules/canvas": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/canvas/-/canvas-3.2.0.tgz", + "integrity": "sha512-jk0GxrLtUEmW/TmFsk2WghvgHe8B0pxGilqCL21y8lHkPUGa6FTsnCNtHPOzT8O3y+N+m3espawV80bbBlgfTA==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "node-addon-api": "^7.0.0", + "prebuild-install": "^7.1.3" + }, + "engines": { + "node": "^18.12.0 || >= 20.9.0" + } + }, "node_modules/chai": { "version": "5.3.3", "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", @@ -6194,7 +6199,6 @@ "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^4.1.0", @@ -6261,6 +6265,12 @@ "node": ">= 6" } }, + "node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "license": "ISC" + }, "node_modules/chromium-bidi": { "version": "10.5.1", "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-10.5.1.tgz", @@ -6305,7 +6315,6 @@ "version": "8.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", - "dev": true, "license": "ISC", "dependencies": { "string-width": "^4.2.0", @@ -6320,7 +6329,6 @@ "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", @@ -6335,7 +6343,6 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^4.0.0", @@ -6372,7 +6379,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, "license": "MIT", "dependencies": { "color-name": "~1.1.4" @@ -6385,7 +6391,6 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, "license": "MIT" }, "node_modules/combined-stream": { @@ -6670,11 +6675,22 @@ "node": "*" } }, + "node_modules/decamelize": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", + "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/decimal.js": { "version": "10.6.0", "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", - "dev": true, "license": "MIT" }, "node_modules/decompress-response": { @@ -7010,8 +7026,7 @@ "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1521046.tgz", "integrity": "sha512-vhE6eymDQSKWUXwwA37NtTTVEzjtGVfDr3pRbsWEQ5onH/Snp2c+2xZHWJJawG/0hCCJLRGt4xVtEVUVILol4w==", "dev": true, - "license": "BSD-3-Clause", - "peer": true + "license": "BSD-3-Clause" }, "node_modules/dezalgo": { "version": "1.0.4", @@ -7024,6 +7039,15 @@ "wrappy": "1" } }, + "node_modules/diff": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-7.0.0.tgz", + "integrity": "sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, "node_modules/doctrine": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", @@ -7072,7 +7096,6 @@ "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", - "dev": true, "license": "MIT" }, "node_modules/electron-to-chromium": { @@ -7085,14 +7108,12 @@ "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, "license": "MIT" }, "node_modules/end-of-stream": { "version": "1.4.5", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", - "dev": true, "license": "MIT", "dependencies": { "once": "^1.4.0" @@ -7115,7 +7136,6 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", - "dev": true, "license": "BSD-2-Clause", "engines": { "node": ">=0.12" @@ -7406,7 +7426,6 @@ "integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -7577,7 +7596,6 @@ "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -7744,7 +7762,8 @@ "version": "1.2.2", "resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.2.tgz", "integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/espree": { "version": "10.4.0", @@ -7809,6 +7828,7 @@ "resolved": "https://registry.npmjs.org/esrap/-/esrap-2.1.2.tgz", "integrity": "sha512-DgvlIQeowRNyvLPWW4PT7Gu13WznY288Du086E751mwwbsgr29ytBiYeLzAGIo0qk3Ujob0SDk8TiSaM5WQzNg==", "license": "MIT", + "peer": true, "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.15" } @@ -7856,6 +7876,12 @@ "node": ">=0.10.0" } }, + "node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "license": "MIT" + }, "node_modules/events-universal": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz", @@ -7866,6 +7892,15 @@ "bare-events": "^2.7.0" } }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "license": "(MIT OR WTFPL)", + "engines": { + "node": ">=6" + } + }, "node_modules/expect-type": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.2.2.tgz", @@ -8122,7 +8157,6 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "dev": true, "license": "MIT", "dependencies": { "locate-path": "^6.0.0", @@ -8147,6 +8181,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/flat": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", + "license": "BSD-3-Clause", + "bin": { + "flat": "cli.js" + } + }, "node_modules/flat-cache": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", @@ -8208,7 +8251,6 @@ "version": "3.3.1", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", - "dev": true, "license": "ISC", "dependencies": { "cross-spawn": "^7.0.6", @@ -8225,7 +8267,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "dev": true, "license": "ISC", "engines": { "node": ">=14" @@ -8272,6 +8313,12 @@ "url": "https://github.com/sponsors/rawify" } }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "license": "MIT" + }, "node_modules/fs-extra": { "version": "11.3.2", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.2.tgz", @@ -8377,7 +8424,6 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true, "license": "ISC", "engines": { "node": "6.* || 8.* || >= 10.*" @@ -8510,6 +8556,12 @@ "node": ">= 14" } }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "license": "MIT" + }, "node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", @@ -8708,7 +8760,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -8782,6 +8833,15 @@ "node": ">= 0.4" } }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "license": "MIT", + "bin": { + "he": "bin/he" + } + }, "node_modules/hoist-non-react-statics": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", @@ -8808,7 +8868,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", - "dev": true, "license": "MIT", "dependencies": { "whatwg-encoding": "^3.1.1" @@ -8843,7 +8902,6 @@ "version": "7.0.2", "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", - "dev": true, "license": "MIT", "dependencies": { "agent-base": "^7.1.0", @@ -8870,7 +8928,6 @@ "version": "7.0.6", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", - "dev": true, "license": "MIT", "dependencies": { "agent-base": "^7.1.2", @@ -8899,7 +8956,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "@babel/runtime": "^7.27.6" }, @@ -8925,7 +8981,6 @@ "version": "0.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "dev": true, "license": "MIT", "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" @@ -8938,7 +8993,6 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "dev": true, "funding": [ { "type": "github", @@ -9252,7 +9306,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -9364,11 +9417,28 @@ "node": ">=0.10.0" } }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-plain-obj": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", + "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/is-potential-custom-element-name": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", - "dev": true, "license": "MIT" }, "node_modules/is-reference": { @@ -9376,6 +9446,7 @@ "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz", "integrity": "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==", "license": "MIT", + "peer": true, "dependencies": { "@types/estree": "^1.0.6" } @@ -9505,7 +9576,6 @@ "version": "0.1.0", "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", - "dev": true, "license": "MIT", "engines": { "node": ">=10" @@ -9650,7 +9720,6 @@ "version": "3.4.3", "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", - "dev": true, "license": "BlueOak-1.0.0", "dependencies": { "@isaacs/cliui": "^8.0.2" @@ -9681,7 +9750,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dev": true, "license": "MIT", "dependencies": { "argparse": "^2.0.1" @@ -9690,13 +9758,183 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jscanify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/jscanify/-/jscanify-1.4.0.tgz", + "integrity": "sha512-5wVTrZfQOBoxNeHcP5n971xHGm61126XvurKNPZs8bFg07z2KDp32fVyGk1bOFWDNn526ZP3fc7bYlm/Oiwz8w==", + "license": "MIT", + "dependencies": { + "canvas": "^3.1.0", + "jsdom": "^26.0.0", + "mocha": "^11.1.0" + } + }, + "node_modules/jscanify/node_modules/@asamuzakjp/css-color": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz", + "integrity": "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==", + "license": "MIT", + "dependencies": { + "@csstools/css-calc": "^2.1.3", + "@csstools/css-color-parser": "^3.0.9", + "@csstools/css-parser-algorithms": "^3.0.4", + "@csstools/css-tokenizer": "^3.0.3", + "lru-cache": "^10.4.3" + } + }, + "node_modules/jscanify/node_modules/cssstyle": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.6.0.tgz", + "integrity": "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==", + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^3.2.0", + "rrweb-cssom": "^0.8.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/jscanify/node_modules/data-urls": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz", + "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==", + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/jscanify/node_modules/jsdom": { + "version": "26.1.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-26.1.0.tgz", + "integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==", + "license": "MIT", + "dependencies": { + "cssstyle": "^4.2.1", + "data-urls": "^5.0.0", + "decimal.js": "^10.5.0", + "html-encoding-sniffer": "^4.0.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.6", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.16", + "parse5": "^7.2.1", + "rrweb-cssom": "^0.8.0", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^5.1.1", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^3.1.1", + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.1.1", + "ws": "^8.18.0", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jscanify/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "license": "ISC" + }, + "node_modules/jscanify/node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/jscanify/node_modules/tldts": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz", + "integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==", + "license": "MIT", + "dependencies": { + "tldts-core": "^6.1.86" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/jscanify/node_modules/tldts-core": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz", + "integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==", + "license": "MIT" + }, + "node_modules/jscanify/node_modules/tough-cookie": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz", + "integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==", + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^6.1.32" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/jscanify/node_modules/tr46": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", + "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/jscanify/node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/jscanify/node_modules/whatwg-url": { + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", + "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", + "license": "MIT", + "dependencies": { + "tr46": "^5.1.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/jsdom": { "version": "27.1.0", "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-27.1.0.tgz", "integrity": "sha512-Pcfm3eZ+eO4JdZCXthW9tCDT3nF4K+9dmeZ+5X39n+Kqz0DDIABRP5CAEOHRFZk8RGuC2efksTJxrjp8EXCunQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@acemir/cssom": "^0.9.19", "@asamuzakjp/dom-selector": "^6.7.3", @@ -10283,13 +10521,13 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz", "integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "dev": true, "license": "MIT", "dependencies": { "p-locate": "^5.0.0" @@ -10312,7 +10550,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", - "dev": true, "license": "MIT", "dependencies": { "chalk": "^4.1.0", @@ -10549,7 +10786,6 @@ "version": "9.0.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "dev": true, "license": "ISC", "dependencies": { "brace-expansion": "^2.0.1" @@ -10574,7 +10810,6 @@ "version": "7.1.2", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", - "dev": true, "license": "ISC", "engines": { "node": ">=16 || 14 >=14.17" @@ -10600,6 +10835,12 @@ "mkdirp": "bin/cmd.js" } }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "license": "MIT" + }, "node_modules/mlly": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.0.tgz", @@ -10632,6 +10873,105 @@ "pathe": "^2.0.1" } }, + "node_modules/mocha": { + "version": "11.7.5", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-11.7.5.tgz", + "integrity": "sha512-mTT6RgopEYABzXWFx+GcJ+ZQ32kp4fMf0xvpZIIfSq9Z8lC/++MtcCnQ9t5FP2veYEP95FIYSvW+U9fV4xrlig==", + "license": "MIT", + "dependencies": { + "browser-stdout": "^1.3.1", + "chokidar": "^4.0.1", + "debug": "^4.3.5", + "diff": "^7.0.0", + "escape-string-regexp": "^4.0.0", + "find-up": "^5.0.0", + "glob": "^10.4.5", + "he": "^1.2.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "log-symbols": "^4.1.0", + "minimatch": "^9.0.5", + "ms": "^2.1.3", + "picocolors": "^1.1.1", + "serialize-javascript": "^6.0.2", + "strip-json-comments": "^3.1.1", + "supports-color": "^8.1.1", + "workerpool": "^9.2.0", + "yargs": "^17.7.2", + "yargs-parser": "^21.1.1", + "yargs-unparser": "^2.0.0" + }, + "bin": { + "_mocha": "bin/_mocha", + "mocha": "bin/mocha.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/mocha/node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/mocha/node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/mocha/node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/mocha/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, "node_modules/module-definition": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/module-definition/-/module-definition-6.0.1.tgz", @@ -10702,6 +11042,12 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", + "license": "MIT" + }, "node_modules/napi-postinstall": { "version": "0.3.4", "resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.4.tgz", @@ -10735,6 +11081,24 @@ "node": ">= 0.4.0" } }, + "node_modules/node-abi": { + "version": "3.85.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.85.0.tgz", + "integrity": "sha512-zsFhmbkAzwhTft6nd3VxcG0cvJsT70rL+BIGHWVq5fi6MwGrHwzqKaxXE+Hl2GmnGItnDKPPkO5/LQqjVkIdFg==", + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "license": "MIT" + }, "node_modules/node-releases": { "version": "2.0.27", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", @@ -10829,6 +11193,12 @@ "dev": true, "license": "ISC" }, + "node_modules/nwsapi": { + "version": "2.2.23", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.23.tgz", + "integrity": "sha512-7wfH4sLbt4M0gCDzGE6vzQBo0bfTKjU7Sfpqy/7gs1qBfYz2vEJH6vXcBKpO3+6Yu1telwd0t9HpyOoLEQQbIQ==", + "license": "MIT" + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -10939,7 +11309,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, "license": "ISC", "dependencies": { "wrappy": "1" @@ -11066,7 +11435,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, "license": "MIT", "dependencies": { "yocto-queue": "^0.1.0" @@ -11082,7 +11450,6 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", - "dev": true, "license": "MIT", "dependencies": { "p-limit": "^3.0.2" @@ -11145,7 +11512,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", - "dev": true, "license": "BlueOak-1.0.0" }, "node_modules/package-manager-detector": { @@ -11218,7 +11584,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -11253,7 +11618,6 @@ "version": "1.11.1", "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", - "dev": true, "license": "BlueOak-1.0.0", "dependencies": { "lru-cache": "^10.2.0", @@ -11270,7 +11634,6 @@ "version": "10.4.3", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "dev": true, "license": "ISC" }, "node_modules/path-type": { @@ -11323,6 +11686,38 @@ "@napi-rs/canvas": "^0.1.81" } }, + "node_modules/peerjs": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/peerjs/-/peerjs-1.5.5.tgz", + "integrity": "sha512-viMUCPDL6CSfOu0ZqVcFqbWRXNHIbv2lPqNbrBIjbFYrflebOjItJ4hPfhjnuUCstqciHVu9vVJ7jFqqKi/EuQ==", + "license": "MIT", + "dependencies": { + "@msgpack/msgpack": "^2.8.0", + "eventemitter3": "^4.0.7", + "peerjs-js-binarypack": "^2.1.0", + "webrtc-adapter": "^9.0.0" + }, + "engines": { + "node": ">= 14" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/peer" + } + }, + "node_modules/peerjs-js-binarypack": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/peerjs-js-binarypack/-/peerjs-js-binarypack-2.1.0.tgz", + "integrity": "sha512-YIwCC+pTzp3Bi8jPI9UFKO0t0SLo6xALnHkiNt/iUFmUUZG0fEEmEyFKvjsDKweiFitzHRyhuh6NvyJZ4nNxMg==", + "license": "MIT", + "engines": { + "node": ">= 14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/peer" + } + }, "node_modules/pend": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", @@ -11442,7 +11837,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -11722,12 +12116,79 @@ "resolved": "https://registry.npmjs.org/preact/-/preact-10.27.2.tgz", "integrity": "sha512-5SYSgFKSyhCbk6SrXyMpqjb5+MQBgfvEKE/OC+PujcY34sOpqtr+0AZQtPYx5IA6VxynQ7rUPCtKzyovpj9Bpg==", "license": "MIT", - "peer": true, "funding": { "type": "opencollective", "url": "https://opencollective.com/preact" } }, + "node_modules/prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/prebuild-install/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/prebuild-install/node_modules/tar-fs": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", + "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", + "license": "MIT", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/prebuild-install/node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/precinct": { "version": "12.2.0", "resolved": "https://registry.npmjs.org/precinct/-/precinct-12.2.0.tgz", @@ -11912,7 +12373,6 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz", "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==", - "dev": true, "license": "MIT", "dependencies": { "end-of-stream": "^1.1.0", @@ -11923,7 +12383,6 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -11997,6 +12456,15 @@ } } }, + "node_modules/qrcode.react": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/qrcode.react/-/qrcode.react-4.2.0.tgz", + "integrity": "sha512-QpgqWi8rD9DsS9EP3z7BT+5lY5SFhsqGjpgW5DY/i3mK4M9DTBNz3ErMi8BWYEfI3L0d8GIbGmcdFAS1uIRGjA==", + "license": "ISC", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/quansync": { "version": "0.2.11", "resolved": "https://registry.npmjs.org/quansync/-/quansync-0.2.11.tgz", @@ -12060,6 +12528,15 @@ "integrity": "sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ==", "license": "MIT" }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, "node_modules/rc": { "version": "1.2.8", "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", @@ -12105,7 +12582,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz", "integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -12115,7 +12591,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz", "integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==", "license": "MIT", - "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -12541,7 +13016,6 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -12728,6 +13202,12 @@ "fsevents": "~2.3.2" } }, + "node_modules/rrweb-cssom": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz", + "integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==", + "license": "MIT" + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -12831,7 +13311,6 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true, "license": "MIT" }, "node_modules/sass-lookup": { @@ -12865,7 +13344,6 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", - "dev": true, "license": "ISC", "dependencies": { "xmlchars": "^2.2.0" @@ -12880,6 +13358,12 @@ "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", "license": "MIT" }, + "node_modules/sdp": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/sdp/-/sdp-3.2.1.tgz", + "integrity": "sha512-lwsAIzOPlH8/7IIjjz3K0zYBk7aBVVcvjMwt3M4fLxpjMYyy7i3I97SLHebgn4YBjirkzfp3RvRDWSKsh/+WFw==", + "license": "MIT" + }, "node_modules/semver": { "version": "7.7.3", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", @@ -12892,6 +13376,15 @@ "node": ">=10" } }, + "node_modules/serialize-javascript": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", + "license": "BSD-3-Clause", + "dependencies": { + "randombytes": "^2.1.0" + } + }, "node_modules/set-cookie-parser": { "version": "2.7.2", "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", @@ -13070,6 +13563,78 @@ "integrity": "sha512-BT5JJygS5BS0oV+tffPRorIud6q17bM7v/1LdQwd0o6mTqGoI25yY1NjSL99OqkekWltS4uon6p52Y8j1Zqu7g==", "license": "MIT" }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "node_modules/simple-get/node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/simple-get/node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/slash": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/slash/-/slash-5.1.0.tgz", @@ -13321,7 +13886,6 @@ "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", @@ -13437,7 +14001,6 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -13451,7 +14014,6 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -13487,7 +14049,6 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -13575,7 +14136,6 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, "license": "MIT", "dependencies": { "has-flag": "^4.0.0" @@ -13627,6 +14187,7 @@ "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", "license": "Apache-2.0", + "peer": true, "engines": { "node": ">= 0.4" } @@ -13635,7 +14196,6 @@ "version": "3.2.4", "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", - "dev": true, "license": "MIT" }, "node_modules/tabbable": { @@ -13835,7 +14395,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -14021,6 +14580,18 @@ "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", "license": "0BSD" }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -14137,7 +14708,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "devOptional": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -14219,7 +14789,6 @@ "dev": true, "hasInstallScript": true, "license": "MIT", - "peer": true, "dependencies": { "napi-postinstall": "^0.3.0" }, @@ -14424,7 +14993,6 @@ "integrity": "sha512-BxAKBWmIbrDgrokdGZH1IgkIk/5mMHDreLDmCJ0qpyJaAteP8NvMhkwr/ZCQNqNH97bw/dANTE9PDzqwJghfMQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", @@ -14595,7 +15163,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -14609,7 +15176,6 @@ "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/chai": "^5.2.2", "@vitest/expect": "3.2.4", @@ -14732,7 +15298,6 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", - "dev": true, "license": "MIT", "dependencies": { "xml-name-validator": "^5.0.0" @@ -14784,11 +15349,23 @@ "node": ">=20" } }, + "node_modules/webrtc-adapter": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/webrtc-adapter/-/webrtc-adapter-9.0.3.tgz", + "integrity": "sha512-5fALBcroIl31OeXAdd1YUntxiZl1eHlZZWzNg3U4Fn+J9/cGL3eT80YlrsWGvj2ojuz1rZr2OXkgCzIxAZ7vRQ==", + "license": "BSD-3-Clause", + "dependencies": { + "sdp": "^3.2.0" + }, + "engines": { + "node": ">=6.0.0", + "npm": ">=3.10.0" + } + }, "node_modules/whatwg-encoding": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", - "dev": true, "license": "MIT", "dependencies": { "iconv-lite": "0.6.3" @@ -14801,7 +15378,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", - "dev": true, "license": "MIT", "engines": { "node": ">=18" @@ -14968,6 +15544,12 @@ "node": ">=12.17" } }, + "node_modules/workerpool": { + "version": "9.3.4", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-9.3.4.tgz", + "integrity": "sha512-TmPRQYYSAnnDiEB0P/Ytip7bFGvqnSU6I2BcuSw7Hx+JSg/DsUi5ebYfc8GYaSdpuvOcEs6dXxPurOYpe9QFwg==", + "license": "Apache-2.0" + }, "node_modules/wrap-ansi": { "version": "9.0.2", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", @@ -14990,7 +15572,6 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^4.0.0", @@ -15008,7 +15589,6 @@ "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", @@ -15085,7 +15665,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true, "license": "ISC" }, "node_modules/ws": { @@ -15113,7 +15692,6 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", - "dev": true, "license": "Apache-2.0", "engines": { "node": ">=18" @@ -15123,14 +15701,12 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", - "dev": true, "license": "MIT" }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "dev": true, "license": "ISC", "engines": { "node": ">=10" @@ -15153,7 +15729,6 @@ "version": "17.7.2", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", - "dev": true, "license": "MIT", "dependencies": { "cliui": "^8.0.1", @@ -15172,17 +15747,30 @@ "version": "21.1.1", "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", - "dev": true, "license": "ISC", "engines": { "node": ">=12" } }, + "node_modules/yargs-unparser": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz", + "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==", + "license": "MIT", + "dependencies": { + "camelcase": "^6.0.0", + "decamelize": "^4.0.0", + "flat": "^5.0.2", + "is-plain-obj": "^2.1.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/yargs/node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", @@ -15208,7 +15796,6 @@ "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "dev": true, "license": "MIT", "engines": { "node": ">=10" @@ -15221,7 +15808,8 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.4.tgz", "integrity": "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/zod": { "version": "3.25.76", diff --git a/frontend/package.json b/frontend/package.json index 914b7bb1f..a2529a5f0 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -50,11 +50,14 @@ "globals": "^16.4.0", "i18next": "^25.5.2", "i18next-browser-languagedetector": "^8.2.0", + "jscanify": "^1.4.0", "jszip": "^3.10.1", "license-report": "^6.8.0", "pdf-lib": "^1.17.1", "pdfjs-dist": "^5.4.149", + "peerjs": "^1.5.5", "posthog-js": "^1.268.0", + "qrcode.react": "^4.1.0", "react": "^19.1.1", "react-dom": "^19.1.1", "react-i18next": "^15.7.3", diff --git a/frontend/public/locales/en-GB/translation.toml b/frontend/public/locales/en-GB/translation.toml index 4f81d64a3..b78bc01f9 100644 --- a/frontend/public/locales/en-GB/translation.toml +++ b/frontend/public/locales/en-GB/translation.toml @@ -4805,6 +4805,8 @@ googleDriveShort = "Drive" myFiles = "My Files" noRecentFiles = "No recent files found" googleDriveNotAvailable = "Google Drive integration not available" +mobileUpload = "Mobile Upload" +mobileShort = "Mobile" downloadSelected = "Download Selected" saveSelected = "Save Selected" openFiles = "Open Files" @@ -6254,3 +6256,32 @@ title = "Add Text Results" [addText.error] failed = "An error occurred while adding text to the PDF." + +[mobileUpload] +title = "Upload from Mobile" +description = "Scan this QR code with your mobile device to upload photos directly to this page." +error = "Connection Error" +sessionId = "Session ID" +connected = "Mobile device connected" +instructions = "Open the camera app on your phone and scan this code. Files will be transferred directly between devices." + +[mobileScanner] +title = "Mobile Scanner" +noSession = "Invalid Session" +noSessionMessage = "Please scan a valid QR code to access this page." +uploadSuccess = "Upload Successful!" +uploadSuccessMessage = "Your images have been transferred." +connected = "Connected" +connecting = "Connecting..." +camera = "Camera" +fileUpload = "File Upload" +cameraAccessDenied = "Camera access denied. Please enable camera access." +capture = "Capture Photo" +selectImage = "Select Image" +preview = "Preview" +retake = "Retake" +addToBatch = "Add to Batch" +upload = "Upload" +batchImages = "Batch" +clearBatch = "Clear" +uploadAll = "Upload All" diff --git a/frontend/src/core/App.tsx b/frontend/src/core/App.tsx index be5e561e5..972a8eb02 100644 --- a/frontend/src/core/App.tsx +++ b/frontend/src/core/App.tsx @@ -1,8 +1,13 @@ import { Suspense } from "react"; +import { Routes, Route } from "react-router-dom"; +import { MantineProvider } from "@mantine/core"; import { AppProviders } from "@app/components/AppProviders"; import { AppLayout } from "@app/components/AppLayout"; import { LoadingFallback } from "@app/components/shared/LoadingFallback"; +import { RainbowThemeProvider } from "@app/components/shared/RainbowThemeProvider"; +import { PreferencesProvider } from "@app/contexts/PreferencesContext"; import HomePage from "@app/pages/HomePage"; +import MobileScannerPage from "@app/pages/MobileScannerPage"; import Onboarding from "@app/components/onboarding/Onboarding"; // Import global styles @@ -13,15 +18,44 @@ import "@app/styles/index.css"; // Import file ID debugging helpers (development only) import "@app/utils/fileIdSafety"; +// Minimal providers for mobile scanner - no API calls, no authentication +function MobileScannerProviders({ children }: { children: React.ReactNode }) { + return ( + + + {children} + + + ); +} + export default function App() { return ( }> - - - - - - + + {/* Mobile scanner route - no backend needed, pure P2P WebRTC */} + + + + } + /> + + {/* All other routes need AppProviders for backend integration */} + + + + + + + } + /> + ); } diff --git a/frontend/src/core/components/fileManager/FileSourceButtons.tsx b/frontend/src/core/components/fileManager/FileSourceButtons.tsx index 8a076a758..0979637c6 100644 --- a/frontend/src/core/components/fileManager/FileSourceButtons.tsx +++ b/frontend/src/core/components/fileManager/FileSourceButtons.tsx @@ -1,12 +1,14 @@ -import React from 'react'; +import React, { useState } from 'react'; import { Stack, Text, Button, Group } from '@mantine/core'; import HistoryIcon from '@mui/icons-material/History'; import CloudIcon from '@mui/icons-material/Cloud'; +import PhonelinkIcon from '@mui/icons-material/Phonelink'; import { useTranslation } from 'react-i18next'; import { useFileManagerContext } from '@app/contexts/FileManagerContext'; import { useGoogleDrivePicker } from '@app/hooks/useGoogleDrivePicker'; import { useFileActionTerminology } from '@app/hooks/useFileActionTerminology'; import { useFileActionIcons } from '@app/hooks/useFileActionIcons'; +import MobileUploadModal from '@app/components/shared/MobileUploadModal'; interface FileSourceButtonsProps { horizontal?: boolean; @@ -15,12 +17,13 @@ interface FileSourceButtonsProps { const FileSourceButtons: React.FC = ({ horizontal = false }) => { - const { activeSource, onSourceChange, onLocalFileClick, onGoogleDriveSelect } = useFileManagerContext(); + const { activeSource, onSourceChange, onLocalFileClick, onGoogleDriveSelect, onNewFilesSelect } = useFileManagerContext(); const { t } = useTranslation(); const { isEnabled: isGoogleDriveEnabled, openPicker: openGoogleDrivePicker } = useGoogleDrivePicker(); const terminology = useFileActionTerminology(); const icons = useFileActionIcons(); const UploadIcon = icons.upload; + const [mobileUploadModalOpen, setMobileUploadModalOpen] = useState(false); const handleGoogleDriveClick = async () => { try { @@ -33,6 +36,16 @@ const FileSourceButtons: React.FC = ({ } }; + const handleMobileUploadClick = () => { + setMobileUploadModalOpen(true); + }; + + const handleFilesReceivedFromMobile = (files: File[]) => { + if (files.length > 0) { + onNewFilesSelect(files); + } + }; + const buttonProps = { variant: (source: string) => activeSource === source ? 'filled' : 'subtle', getColor: (source: string) => activeSource === source ? 'var(--mantine-color-gray-2)' : undefined, @@ -105,24 +118,59 @@ const FileSourceButtons: React.FC = ({ > {horizontal ? t('fileManager.googleDriveShort', 'Drive') : t('fileManager.googleDrive', 'Google Drive')} + + ); if (horizontal) { return ( - - {buttons} - + <> + + {buttons} + + setMobileUploadModalOpen(false)} + onFilesReceived={handleFilesReceivedFromMobile} + /> + ); } return ( - - - {t('fileManager.myFiles', 'My Files')} - - {buttons} - + <> + + + {t('fileManager.myFiles', 'My Files')} + + {buttons} + + setMobileUploadModalOpen(false)} + onFilesReceived={handleFilesReceivedFromMobile} + /> + ); }; diff --git a/frontend/src/core/components/shared/LandingPage.tsx b/frontend/src/core/components/shared/LandingPage.tsx index 68ee35537..00e805de1 100644 --- a/frontend/src/core/components/shared/LandingPage.tsx +++ b/frontend/src/core/components/shared/LandingPage.tsx @@ -1,5 +1,5 @@ import React, { useEffect } from 'react'; -import { Container, Button, Group, useMantineColorScheme } from '@mantine/core'; +import { Container, Button, Group, useMantineColorScheme, ActionIcon, Tooltip } from '@mantine/core'; import { Dropzone } from '@mantine/dropzone'; import LocalIcon from '@app/components/shared/LocalIcon'; import { useTranslation } from 'react-i18next'; @@ -11,6 +11,8 @@ import { useLogoVariant } from '@app/hooks/useLogoVariant'; import { useFileManager } from '@app/hooks/useFileManager'; import { useFileActionTerminology } from '@app/hooks/useFileActionTerminology'; import { useFileActionIcons } from '@app/hooks/useFileActionIcons'; +import MobileUploadModal from '@app/components/shared/MobileUploadModal'; +import PhonelinkIcon from '@mui/icons-material/Phonelink'; const LandingPage = () => { const { addFiles } = useFileHandler(); @@ -24,6 +26,7 @@ const LandingPage = () => { const { wordmark } = useLogoAssets(); const { loadRecentFiles } = useFileManager(); const [hasRecents, setHasRecents] = React.useState(false); + const [mobileUploadModalOpen, setMobileUploadModalOpen] = React.useState(false); const terminology = useFileActionTerminology(); const icons = useFileActionIcons(); @@ -48,6 +51,16 @@ const LandingPage = () => { event.target.value = ''; }; + const handleMobileUploadClick = () => { + setMobileUploadModalOpen(true); + }; + + const handleFilesReceivedFromMobile = async (files: File[]) => { + if (files.length > 0) { + await addFiles(files); + } + }; + // Determine if the user has any recent files (same source as File Manager) useEffect(() => { let isMounted = true; @@ -202,32 +215,58 @@ const LandingPage = () => { )} + + + + + )} {!hasRecents && ( - + <> + + + + + + + )} @@ -251,6 +290,11 @@ const LandingPage = () => { + setMobileUploadModalOpen(false)} + onFilesReceived={handleFilesReceivedFromMobile} + /> ); }; diff --git a/frontend/src/core/components/shared/MobileUploadModal.tsx b/frontend/src/core/components/shared/MobileUploadModal.tsx new file mode 100644 index 000000000..e7659594d --- /dev/null +++ b/frontend/src/core/components/shared/MobileUploadModal.tsx @@ -0,0 +1,208 @@ +import { useEffect, useCallback, useState, useRef } from 'react'; +import { Modal, Stack, Text, Badge, Box, Group, Alert } from '@mantine/core'; +import { QRCodeSVG } from 'qrcode.react'; +import { useTranslation } from 'react-i18next'; +import { useFrontendUrl } from '@app/hooks/useFrontendUrl'; +import InfoRoundedIcon from '@mui/icons-material/InfoRounded'; +import ErrorRoundedIcon from '@mui/icons-material/ErrorRounded'; +import CheckRoundedIcon from '@mui/icons-material/CheckRounded'; +import { Z_INDEX_OVER_FILE_MANAGER_MODAL } from '@app/styles/zIndex'; +import { withBasePath } from '@app/constants/app'; + +interface MobileUploadModalProps { + opened: boolean; + onClose: () => void; + onFilesReceived: (files: File[]) => void; +} + +// Generate a UUID-like session ID +function generateSessionId(): string { + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => { + const r = (Math.random() * 16) | 0; + const v = c === 'x' ? r : (r & 0x3) | 0x8; + return v.toString(16); + }); +} + +/** + * MobileUploadModal + * + * Displays a QR code that mobile devices can scan to upload files via backend server. + * Files are temporarily stored on server and retrieved by desktop. + */ +export default function MobileUploadModal({ opened, onClose, onFilesReceived }: MobileUploadModalProps) { + const { t } = useTranslation(); + const frontendUrl = useFrontendUrl(); + + const [sessionId] = useState(() => generateSessionId()); + const [filesReceived, setFilesReceived] = useState(0); + const [error, setError] = useState(null); + const pollIntervalRef = useRef(null); + const processedFiles = useRef>(new Set()); + + // Use configured frontendUrl if set, otherwise use current origin + // Combine with base path and mobile-scanner route + const mobileUrl = `${frontendUrl}${withBasePath('/mobile-scanner')}?session=${sessionId}`; + + const pollForFiles = useCallback(async () => { + if (!opened) return; + + try { + const response = await fetch(`/api/v1/mobile-scanner/files/${sessionId}`); + if (!response.ok) { + throw new Error('Failed to check for files'); + } + + const data = await response.json(); + const files = data.files || []; + + // Download only files we haven't processed yet + const newFiles = files.filter((f: any) => !processedFiles.current.has(f.filename)); + + if (newFiles.length > 0) { + for (const fileMetadata of newFiles) { + try { + const downloadResponse = await fetch( + `/api/v1/mobile-scanner/download/${sessionId}/${fileMetadata.filename}` + ); + + if (downloadResponse.ok) { + const blob = await downloadResponse.blob(); + const file = new File([blob], fileMetadata.filename, { + type: fileMetadata.contentType || 'image/jpeg' + }); + + processedFiles.current.add(fileMetadata.filename); + setFilesReceived((prev) => prev + 1); + onFilesReceived([file]); + } + } catch (err) { + console.error('Failed to download file:', fileMetadata.filename, err); + } + } + + // Delete the entire session immediately after downloading all files + // This ensures files are only on server for ~1 second + try { + await fetch(`/api/v1/mobile-scanner/session/${sessionId}`, { method: 'DELETE' }); + console.log('Session cleaned up after file download'); + } catch (cleanupErr) { + console.warn('Failed to cleanup session after download:', cleanupErr); + } + } + } catch (err) { + console.error('Error polling for files:', err); + setError(t('mobileUpload.pollingError', 'Error checking for files')); + } + }, [opened, sessionId, onFilesReceived, t]); + + // Start polling when modal opens + useEffect(() => { + if (opened) { + setFilesReceived(0); + setError(null); + processedFiles.current.clear(); + + // Poll every 2 seconds + pollIntervalRef.current = window.setInterval(pollForFiles, 2000); + + // Initial poll + pollForFiles(); + } else { + // Stop polling when modal closes + if (pollIntervalRef.current) { + clearInterval(pollIntervalRef.current); + pollIntervalRef.current = null; + } + } + + return () => { + if (pollIntervalRef.current) { + clearInterval(pollIntervalRef.current); + } + }; + }, [opened, pollForFiles]); + + return ( + + + } + color="blue" + variant="light" + > + + {t( + 'mobileUpload.description', + 'Scan this QR code with your mobile device to upload photos directly to this page.' + )} + + + + {error && ( + } + title={t('mobileUpload.error', 'Connection Error')} + color="red" + > + {error} + + )} + + + + + + + + + {t('mobileUpload.sessionId', 'Session ID')}: + + + {sessionId} + + + + {filesReceived > 0 && ( + }> + {t('mobileUpload.filesReceived', '{{count}} file(s) received', { count: filesReceived })} + + )} + + + {t( + 'mobileUpload.instructions', + 'Open the camera app on your phone and scan this code. Files will be uploaded through the server.' + )} + + + + {mobileUrl} + + + + + ); +} diff --git a/frontend/src/core/hooks/useFrontendUrl.ts b/frontend/src/core/hooks/useFrontendUrl.ts new file mode 100644 index 000000000..b4b97223a --- /dev/null +++ b/frontend/src/core/hooks/useFrontendUrl.ts @@ -0,0 +1,36 @@ +import { useState, useEffect } from 'react'; +import apiClient from '@app/services/apiClient'; + +interface AppConfig { + frontendUrl?: string; + [key: string]: unknown; +} + +/** + * Hook to get the configured frontend URL from backend app-config. + * Falls back to window.location.origin if not configured. + */ +export const useFrontendUrl = (): string => { + const [frontendUrl, setFrontendUrl] = useState(window.location.origin); + + useEffect(() => { + const fetchFrontendUrl = async () => { + try { + const response = await apiClient.get('/api/v1/app-config'); + const configuredUrl = response.data.frontendUrl; + + // Use configured URL if not empty, otherwise keep window.location.origin + if (configuredUrl && configuredUrl.trim() !== '') { + setFrontendUrl(configuredUrl); + } + } catch (error) { + console.warn('Failed to fetch app config, using window.location.origin:', error); + // Keep the default window.location.origin on error + } + }; + + fetchFrontendUrl(); + }, []); + + return frontendUrl; +}; diff --git a/frontend/src/core/pages/MobileScannerPage.tsx b/frontend/src/core/pages/MobileScannerPage.tsx new file mode 100644 index 000000000..8008d295f --- /dev/null +++ b/frontend/src/core/pages/MobileScannerPage.tsx @@ -0,0 +1,910 @@ +import { useState, useRef, useEffect, useCallback } from 'react'; +import { useSearchParams, useNavigate } from 'react-router-dom'; +import { Box, Button, Stack, Text, Group, Alert, Tabs, Progress, Switch, useMantineColorScheme } from '@mantine/core'; +import { useTranslation } from 'react-i18next'; +import { useLogoPath } from '@app/hooks/useLogoPath'; +import { useLogoAssets } from '@app/hooks/useLogoAssets'; +import ErrorRoundedIcon from '@mui/icons-material/ErrorRounded'; +import InfoRoundedIcon from '@mui/icons-material/InfoRounded'; +import PhotoCameraRoundedIcon from '@mui/icons-material/PhotoCameraRounded'; +import UploadRoundedIcon from '@mui/icons-material/UploadRounded'; +import AddPhotoAlternateRoundedIcon from '@mui/icons-material/AddPhotoAlternateRounded'; +import CheckCircleRoundedIcon from '@mui/icons-material/CheckCircleRounded'; +// @ts-ignore - jscanify doesn't have TypeScript definitions +import jscanify from 'jscanify/src/jscanify.js'; + +/** + * MobileScannerPage + * + * Mobile-friendly page for capturing photos and uploading them to the backend server. + * Accessed by scanning QR code from desktop. + */ +export default function MobileScannerPage() { + const { t } = useTranslation(); + const [searchParams] = useSearchParams(); + const navigate = useNavigate(); + const sessionId = searchParams.get('session'); + const { colorScheme } = useMantineColorScheme(); + const brandIconSrc = useLogoPath(); + const { wordmark } = useLogoAssets(); + const brandTextSrc = colorScheme === 'dark' ? wordmark.white : wordmark.black; + + const [activeTab, setActiveTab] = useState('camera'); + const [capturedImages, setCapturedImages] = useState([]); + const [currentPreview, setCurrentPreview] = useState(null); + const [isUploading, setIsUploading] = useState(false); + const [uploadProgress, setUploadProgress] = useState(0); + const [uploadSuccess, setUploadSuccess] = useState(false); + const [uploadError, setUploadError] = useState(null); + const [cameraError, setCameraError] = useState(null); + const [autoEnhance, setAutoEnhance] = useState(true); + const [showLiveDetection, setShowLiveDetection] = useState(true); // On by default with adaptive performance + const [isProcessing, setIsProcessing] = useState(false); + const [openCvReady, setOpenCvReady] = useState(false); + const [torchEnabled, setTorchEnabled] = useState(false); + const [torchSupported, setTorchSupported] = useState(false); + + const videoRef = useRef(null); + const canvasRef = useRef(null); + const highlightCanvasRef = useRef(null); + const detectionCanvasRef = useRef(null); // Low-res canvas for detection only + const streamRef = useRef(null); + const fileInputRef = useRef(null); + const scannerRef = useRef(null); + const highlightIntervalRef = useRef(null); + + // Detection resolution - extremely low for mobile performance + const DETECTION_WIDTH = 160; // Ultra-low for real-time mobile detection + + // Initialize jscanify scanner and wait for OpenCV + useEffect(() => { + let script: HTMLScriptElement | null = null; + + const loadOpenCV = () => { + // Check if OpenCV is already loaded + if ((window as any).cv && (window as any).cv.Mat) { + initScanner(); + return; + } + + // Check if script already exists + const existingScript = document.querySelector('script[src*="opencv.js"]'); + if (existingScript) { + // Script exists, wait for it to load + (window as any).onOpenCvReady = initScanner; + return; + } + + // Load OpenCV.js from CDN + script = document.createElement('script'); + script.src = 'https://docs.opencv.org/4.7.0/opencv.js'; + script.async = true; + script.onload = () => { + console.log('OpenCV.js loaded'); + // OpenCV needs a moment to initialize after script loads + setTimeout(initScanner, 100); + }; + script.onerror = () => { + console.error('Failed to load OpenCV.js'); + }; + document.head.appendChild(script); + }; + + const initScanner = () => { + // Verify OpenCV is actually ready + if (!(window as any).cv || !(window as any).cv.Mat) { + console.warn('OpenCV not ready yet, retrying...'); + setTimeout(initScanner, 100); + return; + } + + try { + scannerRef.current = new jscanify(); + setOpenCvReady(true); + console.log('jscanify initialized with OpenCV'); + } catch (err) { + console.error('Failed to initialize jscanify:', err); + } + }; + + // Start loading process + if (document.readyState === 'complete') { + loadOpenCV(); + } else { + window.addEventListener('load', loadOpenCV); + } + + return () => { + window.removeEventListener('load', loadOpenCV); + // Don't remove script on unmount as other components might use it + }; + }, []); + + // Initialize camera + useEffect(() => { + if (activeTab === 'camera' && !cameraError && !currentPreview) { + // Check if mediaDevices API is available (requires HTTPS or localhost) + if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) { + console.error('MediaDevices API not available - requires HTTPS or localhost'); + setCameraError( + t( + 'mobileScanner.httpsRequired', + 'Camera access requires HTTPS or localhost. Please use HTTPS or access via localhost.' + ) + ); + setActiveTab('file'); + return; + } + + navigator.mediaDevices + .getUserMedia({ + video: { + facingMode: 'environment', + // Request 1080p - good quality without going overboard + width: { ideal: 1920, max: 1920 }, + height: { ideal: 1080, max: 1080 }, + }, + audio: false, + }) + .then(async (stream) => { + streamRef.current = stream; + if (videoRef.current) { + videoRef.current.srcObject = stream; + + // Log actual resolution we got + const videoTrack = stream.getVideoTracks()[0]; + const settings = videoTrack.getSettings(); + console.log('Camera resolution:', settings.width, 'x', settings.height); + + // Configure camera capabilities for document scanning + try { + const capabilities = videoTrack.getCapabilities(); + const constraints: any = { advanced: [] }; + + // 1. Enable continuous autofocus + if (capabilities.focusMode && capabilities.focusMode.includes('continuous')) { + constraints.advanced.push({ focusMode: 'continuous' }); + console.log('✓ Continuous autofocus enabled'); + } + + // 2. Enable continuous auto-exposure for varying lighting + if (capabilities.exposureMode && capabilities.exposureMode.includes('continuous')) { + constraints.advanced.push({ exposureMode: 'continuous' }); + console.log('✓ Auto-exposure enabled'); + } + + // 3. Check if torch/flashlight is supported + if (capabilities.torch) { + setTorchSupported(true); + console.log('✓ Torch/flashlight available'); + } + + // Apply all constraints + if (constraints.advanced.length > 0) { + await videoTrack.applyConstraints(constraints); + } + } catch (err) { + console.log('Could not configure camera features:', err); + } + } + }) + .catch((err) => { + console.error('Camera error:', err); + setCameraError(t('mobileScanner.cameraAccessDenied', 'Camera access denied. Please enable camera access.')); + // Auto-switch to file upload if camera fails + setActiveTab('file'); + }); + } + + return () => { + // Clean up stream when switching away from camera or showing preview + if (streamRef.current) { + streamRef.current.getTracks().forEach((track) => track.stop()); + streamRef.current = null; + } + // Stop highlighting when camera is stopped + if (highlightIntervalRef.current) { + clearInterval(highlightIntervalRef.current); + highlightIntervalRef.current = null; + } + }; + }, [activeTab, cameraError, currentPreview, t]); + + // Real-time document highlighting on camera feed + useEffect(() => { + console.log(`[Mobile Scanner] Effect triggered: activeTab=${activeTab}, showLiveDetection=${showLiveDetection}, openCvReady=${openCvReady}, currentPreview=${currentPreview}`); + + if (activeTab === 'camera' && showLiveDetection && openCvReady && scannerRef.current && !currentPreview) { + const startHighlighting = () => { + if (!videoRef.current || !highlightCanvasRef.current) return; + if (!videoRef.current.videoWidth || !videoRef.current.videoHeight) return; + + const video = videoRef.current; + const highlightCanvas = highlightCanvasRef.current; + + // Create low-res detection canvas with optimized context for frequent pixel reading + const detectionCanvas = document.createElement('canvas'); + const detectionCtx = detectionCanvas.getContext('2d', { willReadFrequently: true }); + if (!detectionCtx) return; + + // Calculate scaled dimensions for detection (160px wide max) + const scale = DETECTION_WIDTH / video.videoWidth; + detectionCanvas.width = DETECTION_WIDTH; + detectionCanvas.height = Math.round(video.videoHeight * scale); + + // CRITICAL FIX: Make highlight canvas ALSO low-res (CSS will scale it visually) + // Drawing to a 4K canvas is what was causing the lag! + highlightCanvas.width = DETECTION_WIDTH; + highlightCanvas.height = Math.round(video.videoHeight * scale); + + console.log(`[Mobile Scanner] Video: ${video.videoWidth}x${video.videoHeight}`); + console.log(`[Mobile Scanner] Detection: ${detectionCanvas.width}x${detectionCanvas.height} (${Math.round(scale * 100)}%)`); + console.log(`[Mobile Scanner] Highlight canvas: ${highlightCanvas.width}x${highlightCanvas.height}`); + console.log(`[Mobile Scanner] Starting interval at 1 FPS`); + + // Set highlight canvas to match video for vector drawing + highlightCanvas.width = video.videoWidth; + highlightCanvas.height = video.videoHeight; + const highlightCtx = highlightCanvas.getContext('2d', { willReadFrequently: true }); + if (!highlightCtx) return; + + // Use requestAnimationFrame with adaptive throttle based on device performance + let frameCount = 0; + const frameTimes: number[] = []; + let lastDetectionTime = 0; + let detectionInterval = 333; // Start at 3 FPS (333ms) + const detectionTimings: number[] = []; // Track last 10 detection times + const MAX_TIMINGS = 10; + + const runDetection = () => { + const now = performance.now(); + + // Only run detection every second + if (now - lastDetectionTime >= detectionInterval) { + lastDetectionTime = now; + const startTime = performance.now(); + + try { + // Step 1: Copy video to low-res detection canvas + const copyStart = performance.now(); + detectionCtx.drawImage(video, 0, 0, detectionCanvas.width, detectionCanvas.height); + const copyTime = performance.now() - copyStart; + + // Step 2: Run detection on low-res to get corner points + const detectionStart = performance.now(); + const mat = (window as any).cv.imread(detectionCanvas); + const contour = scannerRef.current.findPaperContour(mat); + let corners = null; + + if (contour) { + // Validate contour area (reject if too small or too large) + const contourArea = (window as any).cv.contourArea(contour); + const frameArea = detectionCanvas.width * detectionCanvas.height; + const areaPercent = (contourArea / frameArea) * 100; + + // Only accept if contour is 15-85% of frame (filters out noise and frame edges) + if (areaPercent >= 15 && areaPercent <= 85) { + corners = scannerRef.current.getCornerPoints(contour); + } + } + mat.delete(); + const detectionTime = performance.now() - detectionStart; + + // Step 3: Draw ONLY the corner lines on full-res canvas (super fast!) + const drawStart = performance.now(); + highlightCtx.clearRect(0, 0, highlightCanvas.width, highlightCanvas.height); + + // Validate we have all 4 corners (no triangles!) + if ( + corners && + corners.topLeftCorner && + corners.topRightCorner && + corners.bottomLeftCorner && + corners.bottomRightCorner + ) { + // Scale corner points from low-res to full-res + const scaleFactor = video.videoWidth / detectionCanvas.width; + const tl = { x: corners.topLeftCorner.x * scaleFactor, y: corners.topLeftCorner.y * scaleFactor }; + const tr = { x: corners.topRightCorner.x * scaleFactor, y: corners.topRightCorner.y * scaleFactor }; + const br = { x: corners.bottomRightCorner.x * scaleFactor, y: corners.bottomRightCorner.y * scaleFactor }; + const bl = { x: corners.bottomLeftCorner.x * scaleFactor, y: corners.bottomLeftCorner.y * scaleFactor }; + + // Validation 1: Minimum distance between corners + const minDistance = 50; + const distances = [ + Math.hypot(tr.x - tl.x, tr.y - tl.y), + Math.hypot(br.x - tr.x, br.y - tr.y), + Math.hypot(bl.x - br.x, bl.y - br.y), + Math.hypot(tl.x - bl.x, tl.y - bl.y), + ]; + + const allCornersSpaced = distances.every((d) => d > minDistance); + + // Validation 2: Aspect ratio (documents are ~1.0 to 1.5 ratio) + const width1 = Math.hypot(tr.x - tl.x, tr.y - tl.y); + const width2 = Math.hypot(br.x - bl.x, br.y - bl.y); + const height1 = Math.hypot(bl.x - tl.x, bl.y - tl.y); + const height2 = Math.hypot(br.x - tr.x, br.y - tr.y); + + const avgWidth = (width1 + width2) / 2; + const avgHeight = (height1 + height2) / 2; + const aspectRatio = Math.max(avgWidth, avgHeight) / Math.min(avgWidth, avgHeight); + + // Accept aspect ratios from 1:1 (square) to 1:2 (elongated document) + const goodAspectRatio = aspectRatio >= 1.0 && aspectRatio <= 2.0; + + if (allCornersSpaced && goodAspectRatio) { + // Draw lines connecting corners (vector graphics - super lightweight!) + highlightCtx.strokeStyle = '#00FF00'; + highlightCtx.lineWidth = 4; + highlightCtx.beginPath(); + highlightCtx.moveTo(tl.x, tl.y); + highlightCtx.lineTo(tr.x, tr.y); + highlightCtx.lineTo(br.x, br.y); + highlightCtx.lineTo(bl.x, bl.y); + highlightCtx.lineTo(tl.x, tl.y); + highlightCtx.stroke(); + } + } + const drawTime = performance.now() - drawStart; + + const totalTime = performance.now() - startTime; + frameCount++; + frameTimes.push(totalTime); + + // Track detection timings for adaptive performance + detectionTimings.push(totalTime); + if (detectionTimings.length > MAX_TIMINGS) { + detectionTimings.shift(); // Keep only last 10 + } + + // Adaptive performance adjustment (after warmup period) + if (frameCount > 5 && detectionTimings.length >= 5) { + const avgTime = detectionTimings.reduce((a, b) => a + b, 0) / detectionTimings.length; + + // Adjust detection interval based on average performance + if (avgTime < 20) { + // Very fast device: 5 FPS (200ms) + detectionInterval = 200; + } else if (avgTime < 40) { + // Fast device: 3 FPS (333ms) + detectionInterval = 333; + } else if (avgTime < 80) { + // Medium device: 2 FPS (500ms) + detectionInterval = 500; + } else { + // Slower device: 1 FPS (1000ms) + detectionInterval = 1000; + } + } + + if (frameCount <= 10) { + console.log(`[Mobile Scanner] Frame ${frameCount}: ${Math.round(totalTime)}ms total (copy: ${Math.round(copyTime)}ms, detect: ${Math.round(detectionTime)}ms, draw: ${Math.round(drawTime)}ms) - interval: ${detectionInterval}ms`); + } + + if (frameCount === 10) { + const avg = frameTimes.reduce((a, b) => a + b, 0) / frameTimes.length; + console.log(`[Mobile Scanner] Average of first 10 frames: ${Math.round(avg)}ms - Adaptive rate: ${Math.round(1000/detectionInterval)} FPS`); + } + } catch (err) { + console.error('[Mobile Scanner] Detection error:', err); + } + } + + // Continue animation loop + highlightIntervalRef.current = requestAnimationFrame(runDetection); + }; + + // Start the animation loop + highlightIntervalRef.current = requestAnimationFrame(runDetection); + }; + + // Wait for video to be ready + if (videoRef.current && videoRef.current.readyState >= 2) { + startHighlighting(); + } else if (videoRef.current) { + videoRef.current.addEventListener('loadedmetadata', startHighlighting); + } + + return () => { + if (highlightIntervalRef.current) { + console.log('[Mobile Scanner] Stopping detection'); + cancelAnimationFrame(highlightIntervalRef.current); + highlightIntervalRef.current = null; + } + }; + } + }, [activeTab, showLiveDetection, openCvReady, currentPreview]); + + const captureImage = useCallback(async () => { + if (!videoRef.current || !canvasRef.current) return; + + setIsProcessing(true); + + try { + const video = videoRef.current; + const canvas = canvasRef.current; + const context = canvas.getContext('2d'); + + if (!context) return; + + // Capture raw image from video at full resolution + canvas.width = video.videoWidth; + canvas.height = video.videoHeight; + context.drawImage(video, 0, 0, canvas.width, canvas.height); + + let finalDataUrl: string; + + // Apply jscanify processing if enabled and available + if (autoEnhance && scannerRef.current && openCvReady) { + try { + // Create low-res canvas for detection (faster processing) + const detectionCanvas = document.createElement('canvas'); + const detectionCtx = detectionCanvas.getContext('2d', { willReadFrequently: true }); + if (!detectionCtx) throw new Error('Cannot create detection context'); + + const scale = DETECTION_WIDTH / video.videoWidth; + detectionCanvas.width = DETECTION_WIDTH; + detectionCanvas.height = Math.round(video.videoHeight * scale); + + // Draw downscaled image for detection + detectionCtx.drawImage(video, 0, 0, detectionCanvas.width, detectionCanvas.height); + + // Run detection on low-res image + const mat = (window as any).cv.imread(detectionCanvas); + const contour = scannerRef.current.findPaperContour(mat); + + if (contour) { + const cornerPoints = scannerRef.current.getCornerPoints(contour); + + // Scale corner points back to full resolution + if (cornerPoints) { + const scaleFactor = 1 / scale; + const scaledCorners = { + topLeftCorner: { x: cornerPoints.topLeftCorner.x * scaleFactor, y: cornerPoints.topLeftCorner.y * scaleFactor }, + topRightCorner: { x: cornerPoints.topRightCorner.x * scaleFactor, y: cornerPoints.topRightCorner.y * scaleFactor }, + bottomLeftCorner: { x: cornerPoints.bottomLeftCorner.x * scaleFactor, y: cornerPoints.bottomLeftCorner.y * scaleFactor }, + bottomRightCorner: { x: cornerPoints.bottomRightCorner.x * scaleFactor, y: cornerPoints.bottomRightCorner.y * scaleFactor }, + }; + + // Use scaled corners for validation and extraction + const { topLeftCorner, topRightCorner, bottomLeftCorner, bottomRightCorner } = scaledCorners; + + // Validate corner points are reasonable (minimum distance in full-res) + const minDistance = 100; // Minimum pixels between corners at full resolution + const distances = [ + Math.hypot(topRightCorner.x - topLeftCorner.x, topRightCorner.y - topLeftCorner.y), + Math.hypot(bottomRightCorner.x - topRightCorner.x, bottomRightCorner.y - topRightCorner.y), + Math.hypot(bottomLeftCorner.x - bottomRightCorner.x, bottomLeftCorner.y - bottomRightCorner.y), + Math.hypot(topLeftCorner.x - bottomLeftCorner.x, topLeftCorner.y - bottomLeftCorner.y), + ]; + + const isValidDetection = distances.every((d) => d > minDistance); + + if (!isValidDetection) { + console.warn('Detected corners are too close together, using original image'); + finalDataUrl = canvas.toDataURL('image/jpeg', 0.95); + return; + } + + console.log('Valid document detected at full resolution:', { + corners: scaledCorners, + distances: distances.map((d) => Math.round(d)), + }); + + // Calculate width and height of the document + const topWidth = Math.hypot(topRightCorner.x - topLeftCorner.x, topRightCorner.y - topLeftCorner.y); + const bottomWidth = Math.hypot(bottomRightCorner.x - bottomLeftCorner.x, bottomRightCorner.y - bottomLeftCorner.y); + const leftHeight = Math.hypot(bottomLeftCorner.x - topLeftCorner.x, bottomLeftCorner.y - topLeftCorner.y); + const rightHeight = Math.hypot(bottomRightCorner.x - topRightCorner.x, bottomRightCorner.y - topRightCorner.y); + + // Use average dimensions to maintain proper aspect ratio + const docWidth = Math.round((topWidth + bottomWidth) / 2); + const docHeight = Math.round((leftHeight + rightHeight) / 2); + + // Extract paper from full-resolution canvas with scaled corner points + const resultCanvas = scannerRef.current.extractPaper(canvas, docWidth, docHeight, scaledCorners); + + // Use high quality JPEG compression to preserve image quality + finalDataUrl = resultCanvas.toDataURL('image/jpeg', 0.95); + } else { + console.log('No corners detected, using original'); + mat.delete(); + finalDataUrl = canvas.toDataURL('image/jpeg', 0.95); + } + } else { + console.log('No contour detected, using original'); + mat.delete(); + finalDataUrl = canvas.toDataURL('image/jpeg', 0.95); + } + } catch (err) { + console.warn('jscanify processing failed, using original image:', err); + finalDataUrl = canvas.toDataURL('image/jpeg', 0.95); + } + } else { + // Auto-enhance disabled or jscanify not available - use original at high quality + finalDataUrl = canvas.toDataURL('image/jpeg', 0.95); + } + + setCurrentPreview(finalDataUrl); + } finally { + setIsProcessing(false); + } + }, [autoEnhance, openCvReady]); + + const handleFileSelect = useCallback((e: React.ChangeEvent) => { + const files = e.target.files; + if (!files || files.length === 0) return; + + const file = files[0]; + const reader = new FileReader(); + + reader.onload = (event) => { + if (event.target?.result) { + setCurrentPreview(event.target.result as string); + } + }; + + reader.readAsDataURL(file); + }, []); + + const addToBatch = useCallback(() => { + if (currentPreview) { + setCapturedImages((prev) => [...prev, currentPreview]); + setCurrentPreview(null); + } + }, [currentPreview]); + + const uploadImages = useCallback(async () => { + const imagesToUpload = currentPreview ? [currentPreview, ...capturedImages] : capturedImages; + + if (imagesToUpload.length === 0) return; + if (!sessionId) return; + + setIsUploading(true); + setUploadError(null); + setUploadProgress(0); + + try { + // Convert data URLs to File objects + const files: File[] = []; + for (let i = 0; i < imagesToUpload.length; i++) { + const dataUrl = imagesToUpload[i]; + const response = await fetch(dataUrl); + const blob = await response.blob(); + const file = new File([blob], `scan-${Date.now()}-${i}.jpg`, { type: 'image/jpeg' }); + files.push(file); + setUploadProgress(((i + 1) / (imagesToUpload.length + 1)) * 50); // 0-50% for conversion + } + + // Upload to backend + const formData = new FormData(); + files.forEach((file) => { + formData.append('files', file); + }); + + const uploadResponse = await fetch(`/api/v1/mobile-scanner/upload/${sessionId}`, { + method: 'POST', + body: formData, + }); + + if (!uploadResponse.ok) { + throw new Error('Upload failed'); + } + + setUploadProgress(100); + setUploadSuccess(true); + + // Close the mobile tab after successful upload + setTimeout(() => { + window.close(); + // Fallback if window.close() doesn't work (some browsers block it) + if (!window.closed) { + navigate('/'); + } + }, 1500); + } catch (err) { + console.error('Upload failed:', err); + setUploadError(t('mobileScanner.uploadFailed', 'Upload failed. Please try again.')); + } finally { + setIsUploading(false); + } + }, [currentPreview, capturedImages, sessionId, navigate, t]); + + const retake = useCallback(() => { + setCurrentPreview(null); + }, []); + + const clearBatch = useCallback(() => { + setCapturedImages([]); + }, []); + + const toggleTorch = useCallback(async () => { + if (!streamRef.current) return; + + try { + const videoTrack = streamRef.current.getVideoTracks()[0]; + await videoTrack.applyConstraints({ + advanced: [{ torch: !torchEnabled }], + }); + setTorchEnabled(!torchEnabled); + console.log('Torch:', !torchEnabled ? 'ON' : 'OFF'); + } catch (err) { + console.error('Failed to toggle torch:', err); + } + }, [torchEnabled]); + + if (!sessionId) { + return ( + + + {t('mobileScanner.noSessionMessage', 'Please scan a valid QR code to access this page.')} + + + ); + } + + if (uploadSuccess) { + return ( + + + + {t('mobileScanner.uploadSuccess', 'Upload Successful!')} + + + {t('mobileScanner.uploadSuccessMessage', 'Your images have been transferred.')} + + + ); + } + + return ( + + {/* Header */} + + + {t('home.mobile.brandAlt', + Stirling PDF + + + + {uploadError && ( + + } onClose={() => setUploadError(null)} withCloseButton> + {uploadError} + + + )} + + {isUploading && ( + + + {t('mobileScanner.uploading', 'Uploading...')} + + + + )} + + {cameraError && ( + + }> + {cameraError} + + + )} + + {!currentPreview && ( + + + }> + {t('mobileScanner.camera', 'Camera')} + + }> + {t('mobileScanner.fileUpload', 'File Upload')} + + + + + + + + + + + {t('mobileScanner.liveDetection', 'Live Detection')} + + setShowLiveDetection(e.currentTarget.checked)} + disabled={!openCvReady} + /> + + + + {t('mobileScanner.autoEnhance', 'Auto-enhance')} + + setAutoEnhance(e.currentTarget.checked)} + disabled={!openCvReady} + /> + + {torchSupported && ( + + + {t('mobileScanner.flashlight', 'Flashlight')} + + + + )} + + + {autoEnhance && openCvReady && ( + + {t( + 'mobileScanner.autoEnhanceInfo', + 'Document edges will be automatically detected and perspective corrected' + )} + + )} + + + + + + + + + + + )} + + {currentPreview && ( + + + {t('mobileScanner.preview', 'Preview')} + + + Preview + + + + + + + + )} + + {capturedImages.length > 0 && ( + + + + {t('mobileScanner.batchImages', 'Batch')} ({capturedImages.length}) + + + + + + + + {capturedImages.map((img, idx) => ( + + {`Capture + + ))} + + + )} + + ); +} diff --git a/frontend/src/proprietary/App.tsx b/frontend/src/proprietary/App.tsx index 52a246fc6..8a03293d8 100644 --- a/frontend/src/proprietary/App.tsx +++ b/frontend/src/proprietary/App.tsx @@ -3,11 +3,14 @@ import { Routes, Route } from "react-router-dom"; import { AppProviders } from "@app/components/AppProviders"; import { AppLayout } from "@app/components/AppLayout"; import { LoadingFallback } from "@app/components/shared/LoadingFallback"; +import { PreferencesProvider } from "@app/contexts/PreferencesContext"; +import { RainbowThemeProvider } from "@app/components/shared/RainbowThemeProvider"; import Landing from "@app/routes/Landing"; import Login from "@app/routes/Login"; import Signup from "@app/routes/Signup"; import AuthCallback from "@app/routes/AuthCallback"; import InviteAccept from "@app/routes/InviteAccept"; +import MobileScannerPage from "@app/pages/MobileScannerPage"; import Onboarding from "@app/components/onboarding/Onboarding"; // Import global styles @@ -19,24 +22,53 @@ import "@app/styles/auth-theme.css"; // Import file ID debugging helpers (development only) import "@app/utils/fileIdSafety"; +// Minimal providers for mobile scanner - no API calls, no authentication +function MobileScannerProviders({ children }: { children: React.ReactNode }) { + return ( + + + {children} + + + ); +} + export default function App() { return ( }> - - - - {/* Auth routes - no nested providers needed */} - } /> - } /> - } /> - } /> + + {/* Mobile scanner route - no backend needed, pure P2P WebRTC */} + + + + } + /> - {/* Main app routes - Landing handles auth logic */} - } /> - - - - + {/* All other routes need AppProviders for backend integration */} + + + + {/* Auth routes - no nested providers needed */} + } /> + } /> + } /> + } /> + + {/* Main app routes - Landing handles auth logic */} + } /> + + + + + } + /> + ); }