From 5c3b0ca23057302e6df71a45f618ffd8fda2bdc1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C6=90=C9=94=C4=B1s3?= <8407711+w3spi5@users.noreply.github.com> Date: Wed, 14 Jan 2026 18:16:16 +0100 Subject: [PATCH 01/22] Add acknowledgments section in readme according to reddit comment (https://redd.it/1qc9fgt) --- README.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/README.md b/README.md index a5d5d9d..a2e28b8 100644 --- a/README.md +++ b/README.md @@ -489,3 +489,20 @@ If uploading large files (>500MB) fails with HTTP 500 error but smaller files wo

BigDump Screenshot

+ +--- + +## Acknowledgments + +This project is maintained by [w3spi5](https://github.com/w3spi5) with AI-assisted development using Claude. + +AI assists with: +- Code implementation and help to refactoring +- Documentation drafting +- Test coverage + +Human-driven: +- Refactoring +- Architecture decisions +- Feature prioritization +- Code review and quality control From 63e2018bb128a6f55e8de4da417ed4feaf96e9a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C6=90=C9=94=C4=B1s3?= <8407711+w3spi5@users.noreply.github.com> Date: Wed, 28 Jan 2026 00:18:34 +0100 Subject: [PATCH 02/22] Add continue_on_error option to complete imports despite SQL errors --- CHANGELOG.md | 87 +++++++++++++++++++++++ README.md | 41 ++++++++++- config/config.example.php | 17 +++++ dist/bigdump-config.example.php | 7 ++ src/Controllers/BigDumpController.php | 21 ++++-- src/Models/ImportSession.php | 96 ++++++++++++++++++++++++++ src/Services/AjaxService.php | 75 ++++++++++++++++++-- src/Services/ImportHistoryService.php | 5 +- src/Services/ImportService.php | 18 ++++- uploads/2026-01-27_23h31_56.png | Bin 0 -> 115937 bytes 10 files changed, 354 insertions(+), 13 deletions(-) create mode 100644 uploads/2026-01-27_23h31_56.png diff --git a/CHANGELOG.md b/CHANGELOG.md index 2ad2c7a..5bebab8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,93 @@ All notable changes to BigDump are documented in this file. > **Note**: BigDump was originally created by Alexey Ozerov in 2003. Version 2.x is a complete MVC refactoring by w3spi5 (2025). +## [2.28] - 2026-01-28 - Continue On Error Mode + +### Added in 2.28 + +- **Continue On Error Mode**: Option to continue import despite SQL errors + - New `continue_on_error` config option (default: false for safety) + - SQL errors collected as warnings instead of stopping the import + - Warnings displayed in collapsible orange section after successful import + - Maximum 100 warnings stored (bounded buffer to prevent memory issues) + - Warning format includes line number and SQL error message + - Useful for large imports where some statements may fail (e.g., duplicate keys, missing tables) + - Import history tracks warnings count + +### Changed in 2.28 + +- **ImportSession**: Added warnings system + - New `$warnings` array property with `MAX_WARNINGS` constant (100) + - Methods: `addWarning()`, `getWarnings()`, `hasWarnings()`, `getWarningsCount()`, `isWarningsLimitReached()` + - Warnings persisted across SSE sessions + - Statistics include `warnings_count` + +- **ImportService**: Modified error handling in `executeQueryDirect()` + - Checks `continue_on_error` config before throwing exception + - Logs warnings with `error_log()` for server-side tracking + +- **SSE Complete Event**: Extended payload + - Includes `warnings`, `warningsCount`, `warningsLimitReached`, `maxWarnings` when applicable + - Success message shows with orange warning section if warnings exist + +- **AjaxService**: Enhanced `displaySuccessInPage()` JavaScript + - Accepts warnings parameters + - Renders collapsible amber/orange warning section with Tailwind styling + - Shows individual warnings grouped by line number + - Animations still trigger even with warnings + +### Config Example + +```php +/** + * Continue import despite SQL errors (v2.28+). + * When enabled, SQL errors are collected as warnings instead of + * stopping the import. Useful for large imports where some statements + * may fail (e.g., duplicate keys, missing tables). + * Default: false (stop on first error - safest behavior) + */ +'continue_on_error' => false, +``` + +### Files Modified in 2.28 + +| File | Change | +|------|--------| +| `config/config.example.php` | Added `continue_on_error` option | +| `dist/bigdump-config.example.php` | Added `continue_on_error` option | +| `src/Models/ImportSession.php` | Added warnings system (array, methods, MAX_WARNINGS) | +| `src/Services/ImportService.php` | Modified `executeQueryDirect()` for continue_on_error | +| `src/Controllers/BigDumpController.php` | Extended SSE complete event with warnings | +| `src/Services/AjaxService.php` | Enhanced `displaySuccessInPage()` with warnings UI | +| `src/Services/ImportHistoryService.php` | Added `warningsCount` parameter to `addEntry()` | + +--- + +## [2.27] - 2025-01-13 - Celebration Effects & Visual Enhancements + +### Added in 2.27 + +- **Celebration Effects**: Success triggers visual celebrations + - Fireworks: 20 seconds of colorful particle explosions with trails and glow + - Confetti: Infinite falling 3D confetti with realistic physics + - Side confetti cannons bursting every 2 seconds + +- **Visual Enhancements**: + - Animated favicon with bouncing arrow during import + - Skeleton loaders with shimmer animation for loading states + - Progress bar glow: Green pulsing effect on completion + - Improved dark mode contrast for success statistics + +- **French Localization**: Translated labels ("Import terminé!") + +### Fixed in 2.27 + +- **Progress Bar Completion**: Progress bar now correctly reaches 100% +- **SSE Redirect Issue**: Success now displays in-page instead of redirecting to error page +- **CI/CD Workflow**: Compatibility fixed for protected branches + +--- + ## [2.26] - 2025-01-13 - Clean URLs & Bug Fixes ### Added in 2.26 diff --git a/README.md b/README.md index a2e28b8..493bbf6 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ -# BigDump 2.26 - Staggered MySQL Dump Importer +# BigDump 2.28 - Staggered MySQL Dump Importer [![PHP Version](https://img.shields.io/badge/php-8.1+-yellow.svg)](https://php.net/) [![License](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE) -[![Package Version](https://img.shields.io/badge/version-2.26-blue.svg)](https://php.net/) +[![Package Version](https://img.shields.io/badge/version-2.28-blue.svg)](https://php.net/) [![Build Assets](https://img.shields.io/badge/build-GitHub_Actions-2088FF.svg)](https://github.com/w3spi5/bigdump/actions) [![PHAR](https://img.shields.io/badge/PHAR-single--file-purple.svg)](https://github.com/w3spi5/bigdump/releases) @@ -89,6 +89,43 @@ For dedicated servers with 128MB+ memory, providing +20-30% throughput improveme **Note:** Aggressive mode automatically falls back to conservative if PHP `memory_limit` is below 128MB. +## Continue On Error Mode (v2.28) + +BigDump 2.27 introduces a **continue on error** option that allows imports to complete even when some SQL statements fail. This is useful for: + +- Large imports where some statements may fail (duplicate keys, missing tables) +- Recovering as much data as possible from partially corrupted dumps +- Imports where you expect certain errors (e.g., re-importing existing data) + +### Enable Continue On Error + +```php +'continue_on_error' => true, // Default: false (stop on first error) +``` + +### How It Works + +When enabled: +1. SQL errors are **collected as warnings** instead of stopping the import +2. Import continues processing remaining statements +3. After completion, a **success message with warning section** is displayed +4. Warning section shows line numbers and error messages (collapsible) +5. Maximum **100 warnings** stored to prevent memory issues + +### Example Output + +``` +✅ Import completed successfully! + Queries: 15,432 | Lines: 89,234 | Duration: 2m 34s + +⚠️ 3 warning(s) during import [Show details] + - Line 45,231: Duplicate entry '123' for key 'PRIMARY' + - Line 67,892: Table 'temp_backup' doesn't exist + - Line 78,001: Data truncated for column 'description' +``` + +**Important:** Default is `false` (stop on first error) for safety. Only enable when you understand the implications of ignoring SQL errors. + ## CLI SQL Optimizer (v2.22) BigDump includes a **standalone CLI tool** for optimizing SQL dump files without requiring a database connection. It rewrites dumps with INSERT batching for faster imports. diff --git a/config/config.example.php b/config/config.example.php index 45caf9f..12def1b 100644 --- a/config/config.example.php +++ b/config/config.example.php @@ -44,6 +44,23 @@ */ 'db_connection_charset' => 'utf8mb4', + /** + * Continue import despite SQL errors (v2.28+). + * + * When enabled, SQL errors are collected as warnings instead of + * stopping the import. Useful for large imports where some statements + * may fail (e.g., duplicate keys, missing tables) but you want to + * import as much data as possible. + * + * After import, a success message is shown with a warning section + * listing all errors that occurred. + * + * Maximum 100 warnings are collected to prevent memory issues. + * + * Default: false (stop on first error - safest behavior) + */ + 'continue_on_error' => false, + /** * Persistent database connections (v2.25+). * diff --git a/dist/bigdump-config.example.php b/dist/bigdump-config.example.php index 5b142be..aee9729 100644 --- a/dist/bigdump-config.example.php +++ b/dist/bigdump-config.example.php @@ -67,6 +67,13 @@ // IMPORT CONFIGURATION // ========================================================================= + /** + * Continue import despite SQL errors. + * When enabled, SQL errors are collected as warnings instead of stopping. + * Useful for large imports where some statements may fail. + */ + 'continue_on_error' => false, + /** * AJAX mode (true = no page refresh during import). */ diff --git a/src/Controllers/BigDumpController.php b/src/Controllers/BigDumpController.php index 243dcc5..5a6a35c 100644 --- a/src/Controllers/BigDumpController.php +++ b/src/Controllers/BigDumpController.php @@ -527,7 +527,7 @@ public function sseImport(): void // Without this, "Back to Home" → "Import" sees invalid session and fails silently $this->clearSessionDirect($sessionFile); } else { - // Log successful import to history + // Log successful import to history (include warnings count if any) $this->historyService->addEntry( $session->getFilename(), $stats['queries_done'] ?? 0, @@ -535,12 +535,24 @@ public function sseImport(): void $stats['bytes_done'] ?? 0, true, null, - 0.0 + 0.0, + $session->getWarningsCount() ); - $sseService->sendEvent('complete', [ + // Build complete event data with warnings if present + $completeData = [ 'stats' => $stats, - ]); + ]; + + // Include warnings in the response if any were collected + if ($session->hasWarnings()) { + $completeData['warnings'] = $session->getWarnings(); + $completeData['warningsCount'] = $session->getWarningsCount(); + $completeData['warningsLimitReached'] = $session->isWarningsLimitReached(); + $completeData['maxWarnings'] = ImportSession::getMaxWarnings(); + } + + $sseService->sendEvent('complete', $completeData); } // Exit to prevent Application::run() from calling Response::send() @@ -572,6 +584,7 @@ private function saveSessionDirect(ImportSession $session, string $sessionFile): 'file_size' => $session->getFileSize(), 'frozen_lines_total' => $session->getFrozenLinesTotal(), 'frozen_queries_total' => $session->getFrozenQueriesTotal(), + 'warnings' => $session->getWarnings(), 'active' => true, ]; diff --git a/src/Models/ImportSession.php b/src/Models/ImportSession.php index b7b2207..4d1ddb9 100644 --- a/src/Models/ImportSession.php +++ b/src/Models/ImportSession.php @@ -151,6 +151,18 @@ class ImportSession */ private ?array $fileAnalysisData = null; + /** + * SQL warnings collected during import (when continue_on_error is enabled) + * @var array + */ + private array $warnings = []; + + /** + * Maximum number of warnings to collect (memory protection) + * @var int + */ + private const MAX_WARNINGS = 100; + /** * Creates a new session from request parameters * @@ -684,6 +696,80 @@ public function getTargetRamUsage(): ?float return $this->fileAnalysisData['target_ram_usage'] ?? null; } + /** + * Adds a warning (non-fatal SQL error) to the session. + * + * Warnings are collected when continue_on_error is enabled. + * Maximum MAX_WARNINGS warnings are stored to prevent memory issues. + * + * @param string $message Error message + * @param int $line Line number where error occurred + * @return self + */ + public function addWarning(string $message, int $line): self + { + if (count($this->warnings) < self::MAX_WARNINGS) { + $this->warnings[] = [ + 'line' => $line, + 'message' => $message, + ]; + } + return $this; + } + + /** + * Gets all collected warnings. + * + * @return array Warnings array + */ + public function getWarnings(): array + { + return $this->warnings; + } + + /** + * Checks if any warnings were collected. + * + * @return bool True if warnings exist + */ + public function hasWarnings(): bool + { + return count($this->warnings) > 0; + } + + /** + * Gets the total number of warnings collected. + * + * Note: This returns the actual count, which may be less than + * the total errors encountered if MAX_WARNINGS was reached. + * + * @return int Number of warnings + */ + public function getWarningsCount(): int + { + return count($this->warnings); + } + + /** + * Checks if warnings limit was reached. + * + * @return bool True if MAX_WARNINGS was reached + */ + public function isWarningsLimitReached(): bool + { + return count($this->warnings) >= self::MAX_WARNINGS; + } + + /** + * Gets the maximum warnings limit. + * + * @return int Maximum warnings allowed + */ + public static function getMaxWarnings(): int + { + return self::MAX_WARNINGS; + } + /** * Calculates session statistics * @@ -818,6 +904,10 @@ public function getStatistics(): array 'file_category_label' => $this->getFileCategoryLabel(), 'is_bulk_insert' => $this->isBulkInsert(), 'target_ram_usage' => $this->getTargetRamUsage(), + + // Warnings (continue_on_error mode) + 'warnings_count' => $this->getWarningsCount(), + 'warnings_limit_reached' => $this->isWarningsLimitReached(), ]; } @@ -859,6 +949,7 @@ public function toSession(): void 'frozen_lines_total' => $this->frozenLinesTotal, 'frozen_queries_total' => $this->frozenQueriesTotal, 'file_analysis_data' => $this->fileAnalysisData, + 'warnings' => $this->warnings, 'active' => true, ]; } @@ -899,6 +990,11 @@ public static function fromSession(): ?self $session->fileAnalysisData = $data['file_analysis_data']; } + // Restore warnings if available + if (isset($data['warnings']) && is_array($data['warnings'])) { + $session->warnings = $data['warnings']; + } + return $session; } diff --git a/src/Services/AjaxService.php b/src/Services/AjaxService.php index 3d84edb..1e1e7cc 100644 --- a/src/Services/AjaxService.php +++ b/src/Services/AjaxService.php @@ -160,16 +160,73 @@ function displayErrorInPage(message, stats) { /** * Displays an import success message directly in the page. * Creates a styled success container matching the error display pattern. + * Optionally displays warnings in an orange section below the success message. * * @param {object} stats Statistics object + * @param {array} warnings Array of warning objects (optional) + * @param {number} warningsCount Total warnings count (may exceed warnings.length) + * @param {boolean} warningsLimitReached Whether max warnings limit was reached + * @param {number} maxWarnings Maximum warnings limit */ - function displaySuccessInPage(stats) { + function displaySuccessInPage(stats, warnings, warningsCount, warningsLimitReached, maxWarnings) { var queriesDone = stats && stats.queries_done ? stats.queries_done.toLocaleString() : '0'; var linesDone = stats && stats.lines_done ? stats.lines_done.toLocaleString() : '0'; var bytesDone = stats && stats.bytes_done ? formatBytes(stats.bytes_done) : '0 B'; var elapsed = document.getElementById('elapsedTime'); var elapsedTime = elapsed ? elapsed.textContent : '00:00:00'; + // Determine if we have warnings to display + var hasWarnings = warnings && warnings.length > 0; + var displayWarningsCount = warningsCount || (warnings ? warnings.length : 0); + + // Build warnings section HTML if needed + var warningsHtml = ''; + if (hasWarnings) { + var warningItems = warnings.map(function(w) { + return '
  • ' + + 'Line ' + w.line + '' + + '' + escapeHtml(w.message.split('\\n')[0]) + '' + + '
  • '; + }).join(''); + + var limitNotice = ''; + if (warningsLimitReached) { + limitNotice = '
    ' + + '⚠️ Warning limit reached (' + maxWarnings + '). Additional errors may have occurred but were not recorded.' + + '
    '; + } + + warningsHtml = '
    ' + + '
    ' + + '
    ' + + '' + + '' + + '' + + '
    ' + + '
    ' + + '

    ' + displayWarningsCount + ' Warning' + (displayWarningsCount !== 1 ? 's' : '') + '

    ' + + '
    Some SQL statements failed but import continued (continue_on_error enabled).
    ' + + '
    ' + + '
    ' + + '
    ' + + '' + + '' + + '' + + '' + + 'Show Warning Details' + + '' + + '
      ' + warningItems + '
    ' + + limitNotice + + '
    ' + + '
    '; + } + + // Adjust success message based on warnings presence + var successTitle = hasWarnings ? 'Import Complete (with Warnings)' : 'Import Complete!'; + var successSubtitle = hasWarnings + ? 'File ' + escapeHtml(filename) + ' has been imported with ' + displayWarningsCount + ' warning' + (displayWarningsCount !== 1 ? 's' : '') + '.' + : 'File ' + escapeHtml(filename) + ' has been successfully imported.'; + // Create success HTML with Tailwind classes var successHtml = '' + '
    ' + @@ -202,6 +259,7 @@ function displaySuccessInPage(stats) { '
    ' + '' + '' + + warningsHtml + '
    ' + '' + 'Back to Home' + @@ -701,10 +759,17 @@ function createConnection() { source.close(); try { var data = JSON.parse(e.data); - displaySuccessInPage(data.stats); + // Pass warnings data if available + displaySuccessInPage( + data.stats, + data.warnings || [], + data.warningsCount || 0, + data.warningsLimitReached || false, + data.maxWarnings || 100 + ); } catch (err) { // Fallback: show basic success without stats - displaySuccessInPage(null); + displaySuccessInPage(null, [], 0, false, 100); } }); diff --git a/src/Services/ImportHistoryService.php b/src/Services/ImportHistoryService.php index 76628c9..1130bdc 100644 --- a/src/Services/ImportHistoryService.php +++ b/src/Services/ImportHistoryService.php @@ -45,6 +45,7 @@ public function __construct(string $uploadDir, int $maxEntries = 50) * @param bool $success Whether import was successful * @param string|null $error Error message if failed * @param float $duration Duration in seconds + * @param int $warningsCount Number of warnings collected (continue_on_error mode) * @return void */ public function addEntry( @@ -54,7 +55,8 @@ public function addEntry( int $bytesProcessed, bool $success, ?string $error = null, - float $duration = 0.0 + float $duration = 0.0, + int $warningsCount = 0 ): void { $history = $this->loadHistory(); @@ -71,6 +73,7 @@ public function addEntry( 'error' => $error, 'duration' => round($duration, 2), 'duration_formatted' => $this->formatDuration($duration), + 'warnings_count' => $warningsCount, ]; // Add to beginning (most recent first) diff --git a/src/Services/ImportService.php b/src/Services/ImportService.php index d3311d8..636519f 100644 --- a/src/Services/ImportService.php +++ b/src/Services/ImportService.php @@ -609,10 +609,13 @@ private function checkBatchCommit(): void /** * Executes a SQL query directly (bypasses batching). * + * If continue_on_error is enabled in config, SQL errors are collected + * as warnings and execution continues. Otherwise, a RuntimeException is thrown. + * * @param ImportSession $session Import session * @param string $query SQL query * @return void - * @throws RuntimeException In case of SQL error + * @throws RuntimeException In case of SQL error (when continue_on_error is disabled) */ private function executeQueryDirect(ImportSession $session, string $query): void { @@ -629,6 +632,19 @@ private function executeQueryDirect(ImportSession $session, string $query): void ? substr($query, 0, 500) . '...' : $query; + // Check if continue_on_error is enabled + if ($this->config->get('continue_on_error', false)) { + // Collect as warning, continue execution + $warningMessage = "SQL Error at line {$lineNum}: {$error}\nQuery: {$displayQuery}"; + $session->addWarning($warningMessage, $lineNum); + + // Log the warning for debugging + error_log("BigDump: continue_on_error - Line {$lineNum}: {$error}"); + + return; // Continue to next query + } + + // Original behavior: throw exception throw new RuntimeException( "SQL Error at line {$lineNum}:\n" . "Query: {$displayQuery}\n" . diff --git a/uploads/2026-01-27_23h31_56.png b/uploads/2026-01-27_23h31_56.png new file mode 100644 index 0000000000000000000000000000000000000000..9be3d98832cb0e95904b2912af448c4fd3372a28 GIT binary patch literal 115937 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

    voE)IoHRPffvL>eUT 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∾3=D$-v;Cb@eSWe`kyJxmjZ@13@mgM>^ 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­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 OQ^#$8EBe*#*8c%?tUP}J literal 0 HcmV?d00001 From 0a4667e6cf41714d3c87d50032ea113ca00de00f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C6=90=C9=94=C4=B1s3?= <8407711+w3spi5@users.noreply.github.com> Date: Wed, 28 Jan 2026 00:24:44 +0100 Subject: [PATCH 03/22] Suppression screenshot --- uploads/2026-01-27_23h31_56.png | Bin 115937 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 uploads/2026-01-27_23h31_56.png diff --git a/uploads/2026-01-27_23h31_56.png b/uploads/2026-01-27_23h31_56.png deleted file mode 100644 index 9be3d98832cb0e95904b2912af448c4fd3372a28..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 115937 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

    voE)IoHRPffvL>eUT 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∾3=D$-v;Cb@eSWe`kyJxmjZ@13@mgM>^ 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­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 OQ^#$8EBe*#*8c%?tUP}J From 9569a56ef5cc4a24beb8eb0e19cd52a3b0408a6b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C6=90=C9=94=C4=B1s3?= <8407711+w3spi5@users.noreply.github.com> Date: Wed, 28 Jan 2026 14:18:11 +0100 Subject: [PATCH 04/22] Some design and bugs corrections --- assets/dist/bigdump.min.js | 2 +- assets/dist/celebration.min.js | 466 ++++++++++++++++++++++++++++ assets/dist/favicon-animator.min.js | 145 +++++++++ assets/dist/filepolling.min.js | 2 +- assets/js/bigdump.js | 43 +++ assets/js/celebration.js | 466 ++++++++++++++++++++++++++++ assets/js/favicon-animator.js | 145 +++++++++ assets/js/filepolling.js | 21 +- assets/src/js/bigdump.js | 43 +++ assets/src/js/celebration.js | 466 ++++++++++++++++++++++++++++ assets/src/js/favicon-animator.js | 145 +++++++++ assets/src/js/filepolling.js | 21 +- src/Core/Application.php | 2 +- src/Core/View.php | 2 + src/Models/FileHandler.php | 10 +- src/Services/AjaxService.php | 46 +-- templates/error.php | 6 +- templates/home.php | 2 +- templates/import.php | 9 +- templates/layout.php | 2 + 20 files changed, 2010 insertions(+), 34 deletions(-) create mode 100644 assets/dist/celebration.min.js create mode 100644 assets/dist/favicon-animator.min.js create mode 100644 assets/js/celebration.js create mode 100644 assets/js/favicon-animator.js create mode 100644 assets/src/js/celebration.js create mode 100644 assets/src/js/favicon-animator.js diff --git a/assets/dist/bigdump.min.js b/assets/dist/bigdump.min.js index bc05420..98fa28c 100644 --- a/assets/dist/bigdump.min.js +++ b/assets/dist/bigdump.min.js @@ -1 +1 @@ -(function(){"use strict";function o(){var e=document.getElementById("loadingOverlay");e&&document.querySelectorAll("form").forEach(function(t){t.querySelector('input[name="fn"]')&&t.addEventListener("submit",function(i){e.classList.add("active");var n=t.querySelector('input[name="fn"]');if(n){var u=n.value,r=e.querySelector(".loading-subtext");r&&(r.textContent="Loading "+u)}})})}function d(){var e=document.getElementById("darkModeToggle");e&&e.addEventListener("click",function(){var t=document.documentElement,i=t.getAttribute("data-theme")||"light",n=i==="dark"?"light":"dark";t.setAttribute("data-theme",n),localStorage.setItem("bigdump-theme",n)})}function a(){o(),d()}document.readyState==="loading"?document.addEventListener("DOMContentLoaded",a):a()})(); +(function(){"use strict";function o(){var e=document.getElementById("loadingOverlay");e&&document.querySelectorAll("form").forEach(function(t){t.querySelector('input[name="fn"]')&&t.addEventListener("submit",function(i){e.classList.add("active");var n=t.querySelector('input[name="fn"]');if(n){var u=n.value,r=e.querySelector(".loading-subtext");r&&(r.textContent="Loading "+u)}})})}function d(){var e=document.getElementById("darkModeToggle");e&&e.addEventListener("click",function(){var t=document.documentElement,i=t.getAttribute("data-theme")||"light",n=i==="dark"?"light":"dark";t.setAttribute("data-theme",n),localStorage.setItem("bigdump-theme",n)})}function c(e,t){e.disabled=!0,e.classList.add("opacity-50","cursor-not-allowed"),e.innerHTML=''+t}function b(){document.querySelectorAll("form").forEach(function(e){var t=e.querySelector('input[name="fn"]'),i=e.querySelector('input[name="action"]'),n=e.querySelector('button[type="submit"]');n&&(t&&(!i||i.value!=="delete")&&e.addEventListener("submit",function(){c(n,"Importing...")}),i&&i.value==="delete"&&e.addEventListener("submit",function(u){c(n,"Deleting...")}))})}function a(){o(),d(),b()}document.readyState==="loading"?document.addEventListener("DOMContentLoaded",a):a()})(); diff --git a/assets/dist/celebration.min.js b/assets/dist/celebration.min.js new file mode 100644 index 0000000..5d80a74 --- /dev/null +++ b/assets/dist/celebration.min.js @@ -0,0 +1,466 @@ +/** + * BigDump - Celebration Effects Module (Enhanced) + * Creates realistic fireworks and confetti animations on import completion. + * Features: particle trails, glow effects, 3D confetti rotation, air resistance. + */ +(function() { + 'use strict'; + + var canvas = null; + var ctx = null; + var animationId = null; + var particles = []; + var confetti = []; + var trails = []; + var isRunning = false; + var startTime = 0; + var fireworksDuration = 20000; // 20 seconds of fireworks + var confettiForever = true; // Confetti falls indefinitely + + // Vibrant colors for fireworks + var fireworkPalettes = [ + ['#FF6B6B', '#FF8E8E', '#FFB4B4'], // Red + ['#4ECDC4', '#7EDCD6', '#A8EBE7'], // Teal + ['#45B7D1', '#6FC9DE', '#99DBEB'], // Blue + ['#FFD93D', '#FFE469', '#FFEF9A'], // Yellow + ['#6BCB77', '#8DD896', '#AFE5B5'], // Green + ['#FF8C00', '#FFA533', '#FFBE66'], // Orange + ['#DA70D6', '#E494E0', '#EEB8EA'], // Orchid + ['#9B59B6', '#B07CC6', '#C59FD6'], // Purple + ['#E74C3C', '#ED7669', '#F3A096'], // Coral + ['#3498DB', '#5DADE2', '#85C1E9'] // Sky blue + ]; + + var confettiColors = [ + '#FF6B6B', '#4ECDC4', '#45B7D1', '#FFD93D', '#6BCB77', + '#FF8C00', '#DA70D6', '#9B59B6', '#E74C3C', '#3498DB', + '#F39C12', '#1ABC9C', '#E91E63', '#00BCD4', '#8BC34A' + ]; + + /** + * Enhanced Particle class with trails and glow + */ + function Particle(x, y, color, velocity, options) { + options = options || {}; + this.x = x; + this.y = y; + this.color = color; + this.velocity = velocity; + this.gravity = options.gravity || 0.06; + this.decay = options.decay || 0.015; + this.alpha = 1; + this.size = options.size || 3; + this.hasTrail = options.hasTrail !== false; + this.glowSize = options.glowSize || this.size * 2; + this.friction = options.friction || 0.98; + this.trail = []; + this.maxTrailLength = options.trailLength || 5; + } + + Particle.prototype.update = function() { + // Store trail position + if (this.hasTrail && this.alpha > 0.3) { + this.trail.push({ x: this.x, y: this.y, alpha: this.alpha }); + if (this.trail.length > this.maxTrailLength) { + this.trail.shift(); + } + } + + this.velocity.x *= this.friction; + this.velocity.y *= this.friction; + this.velocity.y += this.gravity; + this.x += this.velocity.x; + this.y += this.velocity.y; + this.alpha -= this.decay; + return this.alpha > 0; + }; + + Particle.prototype.draw = function(ctx) { + // Draw trail + if (this.hasTrail && this.trail.length > 0) { + for (var i = 0; i < this.trail.length; i++) { + var t = this.trail[i]; + var trailAlpha = (i / this.trail.length) * this.alpha * 0.5; + var trailSize = this.size * (0.3 + (i / this.trail.length) * 0.5); + ctx.save(); + ctx.globalAlpha = trailAlpha; + ctx.beginPath(); + ctx.arc(t.x, t.y, trailSize, 0, Math.PI * 2); + ctx.fillStyle = this.color; + ctx.fill(); + ctx.restore(); + } + } + + // Draw glow + ctx.save(); + ctx.globalAlpha = this.alpha * 0.4; + ctx.beginPath(); + ctx.arc(this.x, this.y, this.glowSize, 0, Math.PI * 2); + var gradient = ctx.createRadialGradient(this.x, this.y, 0, this.x, this.y, this.glowSize); + gradient.addColorStop(0, this.color); + gradient.addColorStop(1, 'transparent'); + ctx.fillStyle = gradient; + ctx.fill(); + ctx.restore(); + + // Draw core + ctx.save(); + ctx.globalAlpha = this.alpha; + ctx.beginPath(); + ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2); + ctx.fillStyle = this.color; + ctx.fill(); + + // Bright center + ctx.beginPath(); + ctx.arc(this.x, this.y, this.size * 0.5, 0, Math.PI * 2); + ctx.fillStyle = '#FFFFFF'; + ctx.globalAlpha = this.alpha * 0.8; + ctx.fill(); + ctx.restore(); + }; + + /** + * Enhanced Confetti with 3D rotation and air resistance + */ + function ConfettiPiece(x, y, color) { + this.x = x; + this.y = y; + this.color = color; + this.width = Math.random() * 12 + 6; + this.height = Math.random() * 8 + 4; + this.velocity = { + x: (Math.random() - 0.5) * 8, + y: Math.random() * 2 + 1 + }; + // 3D rotation angles + this.rotationX = Math.random() * 360; + this.rotationY = Math.random() * 360; + this.rotationZ = Math.random() * 360; + this.rotationSpeedX = (Math.random() - 0.5) * 8; + this.rotationSpeedY = (Math.random() - 0.5) * 12; + this.rotationSpeedZ = (Math.random() - 0.5) * 6; + // Oscillation (swaying) + this.oscillationSpeed = Math.random() * 0.08 + 0.03; + this.oscillationDistance = Math.random() * 60 + 30; + this.initialX = x; + this.time = Math.random() * 100; + // Air resistance + this.drag = 0.98 + Math.random() * 0.015; + this.terminalVelocity = 4 + Math.random() * 2; + } + + ConfettiPiece.prototype.update = function(canvasHeight) { + this.time += this.oscillationSpeed; + + // Horizontal oscillation (swaying in wind) + this.x = this.initialX + Math.sin(this.time) * this.oscillationDistance; + this.initialX += this.velocity.x * 0.1; // Drift + + // Apply drag + this.velocity.x *= this.drag; + this.velocity.y += 0.08; // Gravity + + // Terminal velocity + if (this.velocity.y > this.terminalVelocity) { + this.velocity.y = this.terminalVelocity; + } + + this.y += this.velocity.y; + + // Tumbling rotation + this.rotationX += this.rotationSpeedX; + this.rotationY += this.rotationSpeedY; + this.rotationZ += this.rotationSpeedZ; + + // Slow down rotation over time + this.rotationSpeedX *= 0.999; + this.rotationSpeedY *= 0.999; + + return this.y < canvasHeight + 50; + }; + + ConfettiPiece.prototype.draw = function(ctx) { + ctx.save(); + ctx.translate(this.x, this.y); + + // Apply 3D-like rotation (fake perspective) + var scaleX = Math.cos(this.rotationY * Math.PI / 180); + var scaleY = Math.cos(this.rotationX * Math.PI / 180); + ctx.rotate(this.rotationZ * Math.PI / 180); + ctx.scale(scaleX, scaleY); + + // Draw confetti piece with slight shadow + ctx.shadowColor = 'rgba(0,0,0,0.2)'; + ctx.shadowBlur = 2; + ctx.shadowOffsetY = 1; + + ctx.fillStyle = this.color; + ctx.fillRect(-this.width / 2, -this.height / 2, this.width, this.height); + + // Add shine effect + ctx.fillStyle = 'rgba(255,255,255,0.3)'; + ctx.fillRect(-this.width / 2, -this.height / 2, this.width * 0.4, this.height); + + ctx.restore(); + }; + + /** + * Create an enhanced firework explosion + */ + function createFirework(x, y) { + var palette = fireworkPalettes[Math.floor(Math.random() * fireworkPalettes.length)]; + var particleCount = 60 + Math.floor(Math.random() * 40); + var burstSpeed = 4 + Math.random() * 3; + + // Main explosion particles + for (var i = 0; i < particleCount; i++) { + var angle = (Math.PI * 2 / particleCount) * i + (Math.random() - 0.5) * 0.2; + var speed = burstSpeed * (0.6 + Math.random() * 0.8); + var colorIndex = Math.floor(Math.random() * palette.length); + var velocity = { + x: Math.cos(angle) * speed, + y: Math.sin(angle) * speed + }; + particles.push(new Particle(x, y, palette[colorIndex], velocity, { + gravity: 0.05, + decay: 0.010 + Math.random() * 0.008, + size: 2 + Math.random() * 2, + trailLength: 4, + glowSize: 6 + })); + } + + // Inner ring (brighter, faster decay) + var innerCount = 20 + Math.floor(Math.random() * 15); + for (var j = 0; j < innerCount; j++) { + var innerAngle = Math.random() * Math.PI * 2; + var innerSpeed = burstSpeed * 1.3 * Math.random(); + var innerVelocity = { + x: Math.cos(innerAngle) * innerSpeed, + y: Math.sin(innerAngle) * innerSpeed + }; + particles.push(new Particle(x, y, '#FFFFFF', innerVelocity, { + gravity: 0.04, + decay: 0.025, + size: 1.5, + trailLength: 3, + glowSize: 4, + hasTrail: true + })); + } + + // Sparkle particles (twinkle effect) + var sparkleCount = 15 + Math.floor(Math.random() * 10); + for (var k = 0; k < sparkleCount; k++) { + var sparkAngle = Math.random() * Math.PI * 2; + var sparkSpeed = burstSpeed * 0.3 + Math.random() * burstSpeed * 0.8; + var sparkVelocity = { + x: Math.cos(sparkAngle) * sparkSpeed, + y: Math.sin(sparkAngle) * sparkSpeed + }; + particles.push(new Particle(x, y, '#FFFACD', sparkVelocity, { + gravity: 0.08, + decay: 0.03 + Math.random() * 0.02, + size: 1, + glowSize: 3, + hasTrail: false + })); + } + } + + /** + * Create confetti burst from top + */ + function createConfettiBurst(count) { + if (!canvas) return; + + for (var i = 0; i < count; i++) { + var x = Math.random() * canvas.width; + var color = confettiColors[Math.floor(Math.random() * confettiColors.length)]; + confetti.push(new ConfettiPiece(x, -20 - Math.random() * 50, color)); + } + } + + /** + * Create confetti cannon from sides + */ + function createConfettiCannon(fromLeft) { + if (!canvas) return; + + var startX = fromLeft ? -10 : canvas.width + 10; + var angle = fromLeft ? -Math.PI / 4 : -Math.PI * 3 / 4; + + for (var i = 0; i < 30; i++) { + var spread = (Math.random() - 0.5) * 0.5; + var speed = 8 + Math.random() * 6; + var color = confettiColors[Math.floor(Math.random() * confettiColors.length)]; + var piece = new ConfettiPiece(startX, canvas.height * 0.7, color); + piece.velocity.x = Math.cos(angle + spread) * speed; + piece.velocity.y = Math.sin(angle + spread) * speed; + piece.initialX = startX; + confetti.push(piece); + } + } + + /** + * Initialize canvas + */ + function init() { + if (canvas) return; + + canvas = document.createElement('canvas'); + canvas.id = 'celebration-canvas'; + canvas.style.cssText = 'position: fixed; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; z-index: 9999;'; + document.body.appendChild(canvas); + + ctx = canvas.getContext('2d'); + resizeCanvas(); + + window.addEventListener('resize', resizeCanvas); + } + + /** + * Resize canvas to match window + */ + function resizeCanvas() { + if (!canvas) return; + canvas.width = window.innerWidth; + canvas.height = window.innerHeight; + } + + /** + * Animation loop + */ + function animate() { + if (!isRunning) return; + + var elapsed = Date.now() - startTime; + + // Clear canvas with slight fade for trail effect + ctx.fillStyle = 'rgba(0, 0, 0, 0.1)'; + ctx.fillRect(0, 0, canvas.width, canvas.height); + ctx.clearRect(0, 0, canvas.width, canvas.height); + + // Update and draw particles + particles = particles.filter(function(p) { + var alive = p.update(); + if (alive) p.draw(ctx); + return alive; + }); + + // Update and draw confetti + confetti = confetti.filter(function(c) { + var alive = c.update(canvas.height); + if (alive) c.draw(ctx); + return alive; + }); + + // Create new fireworks periodically for 20 seconds + if (elapsed < fireworksDuration) { + // More frequent fireworks at start, gradually decreasing + var fireworkChance = elapsed < 5000 ? 0.08 : (elapsed < 10000 ? 0.05 : 0.03); + if (Math.random() < fireworkChance) { + var x = Math.random() * canvas.width * 0.7 + canvas.width * 0.15; + var y = Math.random() * canvas.height * 0.4 + canvas.height * 0.08; + createFirework(x, y); + } + } + + // Add confetti continuously forever (or until stopped) + if (confettiForever && Math.random() < 0.15) { + createConfettiBurst(1); + } + + // Confetti cannons at specific intervals during first 15 seconds + if (elapsed < 15000) { + var cannonInterval = 2000; // Every 2 seconds + var cannonWindow = 30; + if (elapsed % cannonInterval < cannonWindow && Math.random() < 0.5) { + createConfettiCannon(Math.random() < 0.5); + } + } + + // Continue animation as long as running (confetti forever!) + if (isRunning) { + animationId = requestAnimationFrame(animate); + } else { + cleanup(); + } + } + + /** + * Clean up canvas and reset state + */ + function cleanup() { + isRunning = false; + if (animationId) { + cancelAnimationFrame(animationId); + animationId = null; + } + if (canvas && canvas.parentNode) { + canvas.parentNode.removeChild(canvas); + } + canvas = null; + ctx = null; + particles = []; + confetti = []; + trails = []; + window.removeEventListener('resize', resizeCanvas); + } + + /** + * Start the celebration effects + */ + function start() { + if (isRunning) return; + + init(); + isRunning = true; + startTime = Date.now(); + particles = []; + confetti = []; + trails = []; + + // Initial burst of fireworks (staggered) + var fireworkPositions = [ + { x: 0.3, y: 0.25, delay: 0 }, + { x: 0.7, y: 0.2, delay: 150 }, + { x: 0.5, y: 0.15, delay: 300 }, + { x: 0.25, y: 0.3, delay: 450 }, + { x: 0.75, y: 0.28, delay: 600 } + ]; + + fireworkPositions.forEach(function(pos) { + setTimeout(function() { + if (!isRunning || !canvas) return; + createFirework(canvas.width * pos.x, canvas.height * pos.y); + }, pos.delay); + }); + + // Initial confetti burst + createConfettiBurst(80); + + animate(); + console.log('Celebration started!'); + } + + /** + * Stop the celebration effects immediately + */ + function stop() { + if (!isRunning) return; + cleanup(); + console.log('Celebration stopped'); + } + + // Expose API + window.BigDump = window.BigDump || {}; + window.BigDump.celebration = { + start: start, + stop: stop, + isRunning: function() { return isRunning; } + }; + +})(); diff --git a/assets/dist/favicon-animator.min.js b/assets/dist/favicon-animator.min.js new file mode 100644 index 0000000..7a03b6c --- /dev/null +++ b/assets/dist/favicon-animator.min.js @@ -0,0 +1,145 @@ +/** + * BigDump - Animated Favicon Module + * Creates a bouncing arrow animation during import process. + * Uses Canvas API to dynamically generate favicon frames. + */ +(function() { + 'use strict'; + + var canvas = null; + var ctx = null; + var animationId = null; + var originalFavicon = null; + var linkElement = null; + var frame = 0; + var isAnimating = false; + + // Animation settings + var SIZE = 32; // Favicon size + var ARROW_COLOR = '#3B82F6'; // Blue-500 + var BG_COLOR = '#1E3A5F'; // Dark blue background + var BOUNCE_HEIGHT = 6; // Pixels to bounce + var FRAME_RATE = 60; // ms between frames + + /** + * Initialize canvas and get original favicon + */ + function init() { + if (canvas) return; + + canvas = document.createElement('canvas'); + canvas.width = SIZE; + canvas.height = SIZE; + ctx = canvas.getContext('2d'); + + // Find and store original favicon + linkElement = document.querySelector('link[rel="icon"][type="image/png"]') || + document.querySelector('link[rel="icon"]'); + + if (linkElement) { + originalFavicon = linkElement.href; + } + } + + /** + * Draw a frame of the bouncing arrow animation + * @param {number} offset - Vertical bounce offset + */ + function drawFrame(offset) { + // Clear canvas + ctx.clearRect(0, 0, SIZE, SIZE); + + // Draw circular background + ctx.beginPath(); + ctx.arc(SIZE / 2, SIZE / 2, SIZE / 2 - 1, 0, Math.PI * 2); + ctx.fillStyle = BG_COLOR; + ctx.fill(); + + // Calculate arrow position with bounce + var centerX = SIZE / 2; + var centerY = SIZE / 2 + offset; + + // Draw downward arrow + ctx.beginPath(); + ctx.strokeStyle = ARROW_COLOR; + ctx.fillStyle = ARROW_COLOR; + ctx.lineWidth = 3; + ctx.lineCap = 'round'; + ctx.lineJoin = 'round'; + + // Arrow shaft + ctx.moveTo(centerX, centerY - 8); + ctx.lineTo(centerX, centerY + 4); + ctx.stroke(); + + // Arrow head (triangle) + ctx.beginPath(); + ctx.moveTo(centerX - 6, centerY); + ctx.lineTo(centerX, centerY + 8); + ctx.lineTo(centerX + 6, centerY); + ctx.closePath(); + ctx.fill(); + + // Update favicon + if (linkElement) { + linkElement.href = canvas.toDataURL('image/png'); + } + } + + /** + * Animation loop using eased bounce + */ + function animate() { + if (!isAnimating) return; + + // Eased bounce: sin wave for smooth up/down motion + var bounce = Math.sin(frame * 0.15) * BOUNCE_HEIGHT; + drawFrame(bounce); + + frame++; + animationId = setTimeout(animate, FRAME_RATE); + } + + /** + * Start the favicon animation + */ + function start() { + if (isAnimating) return; + + init(); + isAnimating = true; + frame = 0; + animate(); + console.log('Favicon animation started'); + } + + /** + * Stop the animation and restore original favicon + */ + function stop() { + if (!isAnimating) return; + + isAnimating = false; + + if (animationId) { + clearTimeout(animationId); + animationId = null; + } + + // Restore original favicon + if (linkElement && originalFavicon) { + linkElement.href = originalFavicon; + } + + console.log('Favicon animation stopped'); + } + + // Expose API + window.BigDump = window.BigDump || {}; + window.BigDump.faviconAnimator = { + start: start, + stop: stop, + isAnimating: function() { return isAnimating; } + }; + +})(); diff --git a/assets/dist/filepolling.min.js b/assets/dist/filepolling.min.js index 93eb086..aaca51e 100644 --- a/assets/dist/filepolling.min.js +++ b/assets/dist/filepolling.min.js @@ -1 +1 @@ -(function(){"use strict";var C=document.getElementById("bigdump-config");if(!C)return;var B=JSON.parse(C.dataset.dbConfigured||"false"),M=JSON.parse(C.dataset.gzipSupported||"false"),G=4e3,p=null,g=new Set,f=new Set;function O(e){switch(e){case"SQL":return"bg-blue-100 dark:bg-blue-900/50 text-blue-800 dark:text-blue-200";case"GZip":return"bg-purple-100 dark:bg-purple-900/50 text-purple-800 dark:text-purple-200";case"CSV":return"bg-green-100 dark:bg-green-900/50 text-green-800 dark:text-green-200";default:return"bg-blue-100 dark:bg-blue-900/50 text-blue-800 dark:text-blue-200"}}function Z(e){var t=e.split(".").pop().toLowerCase();return t==="sql"?"SQL":t==="gz"?"GZip":t==="csv"?"CSV":"SQL"}function R(){var e=document.getElementById("previewModal"),t=document.getElementById("historyModal");return e&&!e.classList.contains("hidden")||t&&!t.classList.contains("hidden")}function z(e){var t=document.getElementById("noFilesMessage"),r=document.getElementById("filesTableContainer");!t||!r||(e?(t.classList.remove("hidden"),r.classList.add("hidden")):(t.classList.add("hidden"),r.classList.remove("hidden")))}function T(e,t,r){var n=document.createElement("tr");n.className="hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors duration-150",n.setAttribute("data-filename",e.name),t&&(n.classList.add("bg-green-50","dark:bg-green-900/20","animate-pulse"),setTimeout(function(){n.classList.remove("bg-green-50","dark:bg-green-900/20","animate-pulse")},3e3)),r&&n.classList.add("bg-amber-50","dark:bg-amber-900/20");var u=B&&(e.type!=="GZip"||M),a="px-4 py-3 border-b border-gray-200 dark:border-gray-700 text-gray-900 dark:text-gray-100",i=document.createElement("td");i.className=a;var l=document.createElement("strong");if(l.textContent=e.name,i.appendChild(l),r){var v=document.createElement("span");v.className="ml-2 text-amber-600 dark:text-amber-400 text-xs";var x=document.createElementNS("http://www.w3.org/2000/svg","svg");x.setAttribute("class","icon w-4 h-4 fill-current inline-block animate-spin");var P=document.createElementNS("http://www.w3.org/2000/svg","use");P.setAttributeNS("http://www.w3.org/1999/xlink","href","assets/icons.svg#spinner"),x.appendChild(P),v.appendChild(x),v.appendChild(document.createTextNode(" Uploading...")),i.appendChild(v)}else if(t){var E=document.createElement("span");E.className="ml-2 px-1.5 py-0.5 bg-green-500 text-white text-xs rounded font-medium",E.textContent="NEW",i.appendChild(E)}n.appendChild(i);var y=document.createElement("td");y.className=a,y.textContent=r?"--":e.sizeFormatted,n.appendChild(y);var N=document.createElement("td");N.className=a,N.textContent=r?"--":e.date,n.appendChild(N);var F=document.createElement("td");F.className=a;var k=document.createElement("span");k.className="px-2 py-1 rounded text-xs font-medium "+O(e.type),k.textContent=e.type,F.appendChild(k),n.appendChild(F);var d=document.createElement("td");if(d.className=a+" text-center",r){var S=document.createElement("span");S.className="text-gray-400",S.textContent="--",d.appendChild(S)}else{if(u){var o=document.createElement("button");o.type="button",o.className="btn btn-icon btn-purple",o.title="Preview SQL content",o.onclick=function(){window.previewFile(e.name)};var L=document.createElementNS("http://www.w3.org/2000/svg","svg");L.setAttribute("class","icon w-4 h-4 fill-current");var A=document.createElementNS("http://www.w3.org/2000/svg","use");A.setAttributeNS("http://www.w3.org/1999/xlink","href","assets/icons.svg#eye"),L.appendChild(A),o.appendChild(L),d.appendChild(o),d.appendChild(document.createTextNode(" "));var s=document.createElement("form");s.method="post",s.action="",s.style.display="inline";var b=document.createElement("input");b.type="hidden",b.name="fn",b.value=e.name,s.appendChild(b);var w=document.createElement("button");w.type="submit",w.className="btn btn-green",w.textContent="Import",s.appendChild(w),d.appendChild(s),d.appendChild(document.createTextNode(" "))}else if(e.type==="GZip"){var D=document.createElement("span");D.className="text-muted",D.textContent="GZip not supported",d.appendChild(D),d.appendChild(document.createTextNode(" "))}var m=document.createElement("a");m.href="?delete="+encodeURIComponent(e.name),m.className="btn btn-red",m.textContent="Delete",m.onclick=function(){return confirm("Delete "+e.name+"?")},d.appendChild(m)}return n.appendChild(d),n}function Q(e,t){var r=document.getElementById("fileTableBody");if(r){var n=new Set(t.map(function(a){return a.name})),u=document.createDocumentFragment();f.forEach(function(a){if(!e.find(function(l){return l.name===a})){var i={name:a,type:Z(a),sizeFormatted:"--",date:"--"};u.appendChild(T(i,!1,!0))}}),e.forEach(function(a){var i=n.has(a.name),l=f.has(a.name);u.appendChild(T(a,i,l))}),r.replaceChildren(u)}}function _(){document.querySelectorAll("#fileTableBody tr[data-filename]").forEach(function(e){g.add(e.getAttribute("data-filename"))})}function h(){_(),p=setInterval(c,G)}function I(){p&&(clearInterval(p),p=null)}function c(){R()||fetch("/files/list").then(function(e){return e.json()}).then(function(e){if(e.success){var t=new Set(e.files.map(function(n){return n.name})),r=e.files.filter(function(n){return!g.has(n.name)});Q(e.files,r),g=t,z(e.files.length===0)}}).catch(function(e){console.error("File polling error:",e)})}function U(e){f.add(e),c()}function V(e){f.delete(e),g.add(e),c()}document.addEventListener("visibilitychange",function(){document.hidden?I():h()}),document.readyState==="loading"?document.addEventListener("DOMContentLoaded",h):h(),window.BigDump=window.BigDump||{},window.BigDump.refreshFileList=c,window.BigDump.trackUploadStart=U,window.BigDump.trackUploadEnd=V,window.BigDump.startFilePolling=h,window.BigDump.stopFilePolling=I,window.refreshFileList=c})(); +(function(){"use strict";var C=document.getElementById("bigdump-config");if(!C)return;var B=JSON.parse(C.dataset.dbConfigured||"false"),M=JSON.parse(C.dataset.gzipSupported||"false"),G=4e3,p=null,g=new Set,f=new Set;function O(e){switch(e){case"SQL":return"bg-blue-100 dark:bg-blue-900/50 text-blue-800 dark:text-blue-200";case"GZip":return"bg-purple-100 dark:bg-purple-900/50 text-purple-800 dark:text-purple-200";case"CSV":return"bg-green-100 dark:bg-green-900/50 text-green-800 dark:text-green-200";default:return"bg-blue-100 dark:bg-blue-900/50 text-blue-800 dark:text-blue-200"}}function Z(e){var t=e.split(".").pop().toLowerCase();return t==="sql"?"SQL":t==="gz"?"GZip":t==="csv"?"CSV":"SQL"}function R(){var e=document.getElementById("previewModal"),t=document.getElementById("historyModal");return e&&!e.classList.contains("hidden")||t&&!t.classList.contains("hidden")}function z(e){var t=document.getElementById("noFilesMessage"),r=document.getElementById("filesTableContainer");!t||!r||(e?(t.classList.remove("hidden"),r.classList.add("hidden")):(t.classList.add("hidden"),r.classList.remove("hidden")))}function T(e,t,r){var n=document.createElement("tr");n.className="hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors duration-150",n.setAttribute("data-filename",e.name),t&&(n.classList.add("bg-green-50","dark:bg-green-900/20","animate-pulse"),setTimeout(function(){n.classList.remove("bg-green-50","dark:bg-green-900/20","animate-pulse")},3e3)),r&&n.classList.add("bg-amber-50","dark:bg-amber-900/20");var u=B&&(e.type!=="GZip"||M),a="px-4 py-3 border-b border-gray-200 dark:border-gray-700 text-gray-900 dark:text-gray-100",i=document.createElement("td");i.className=a;var l=document.createElement("strong");if(l.textContent=e.name,i.appendChild(l),r){var v=document.createElement("span");v.className="ml-2 text-amber-600 dark:text-amber-400 text-xs";var x=document.createElementNS("http://www.w3.org/2000/svg","svg");x.setAttribute("class","icon w-4 h-4 fill-current inline-block animate-spin");var P=document.createElementNS("http://www.w3.org/2000/svg","use");P.setAttributeNS("http://www.w3.org/1999/xlink","href","assets/icons.svg#spinner"),x.appendChild(P),v.appendChild(x),v.appendChild(document.createTextNode(" Uploading...")),i.appendChild(v)}else if(t){var E=document.createElement("span");E.className="ml-2 px-1.5 py-0.5 bg-green-500 text-white text-xs rounded font-medium",E.textContent="NEW",i.appendChild(E)}n.appendChild(i);var y=document.createElement("td");y.className=a,y.textContent=r?"--":e.sizeFormatted,n.appendChild(y);var N=document.createElement("td");N.className=a,N.textContent=r?"--":e.date,n.appendChild(N);var F=document.createElement("td");F.className=a;var k=document.createElement("span");k.className="px-2 py-1 rounded text-xs font-medium "+O(e.type),k.textContent=e.type,F.appendChild(k),n.appendChild(F);var d=document.createElement("td");if(d.className=a+" text-center",r){var S=document.createElement("span");S.className="text-gray-400",S.textContent="--",d.appendChild(S)}else{if(u){var o=document.createElement("button");o.type="button",o.className="btn btn-icon btn-purple",o.title="Preview SQL content",o.onclick=function(){window.previewFile(e.name)};var L=document.createElementNS("http://www.w3.org/2000/svg","svg");L.setAttribute("class","icon w-4 h-4 fill-current");var A=document.createElementNS("http://www.w3.org/2000/svg","use");A.setAttributeNS("http://www.w3.org/1999/xlink","href","assets/icons.svg#eye"),L.appendChild(A),o.appendChild(L),d.appendChild(o),d.appendChild(document.createTextNode(" "));var s=document.createElement("form");s.method="post",s.action="",s.style.display="inline";var b=document.createElement("input");b.type="hidden",b.name="fn",b.value=e.name,s.appendChild(b);var w=document.createElement("button");w.type="submit",w.className="btn btn-green",w.textContent="Import",s.onsubmit=function(){return w.disabled=!0,w.classList.add("opacity-50","cursor-not-allowed"),w.innerHTML='Importing...',!0},s.appendChild(w),d.appendChild(s),d.appendChild(document.createTextNode(" "))}else if(e.type==="GZip"){var D=document.createElement("span");D.className="text-muted",D.textContent="GZip not supported",d.appendChild(D),d.appendChild(document.createTextNode(" "))}var m=document.createElement("a");m.href="?delete="+encodeURIComponent(e.name),m.className="btn btn-red",m.textContent="Delete",m.onclick=function(j){return m.classList.contains("disabled")?(j.preventDefault(),!1):confirm("Delete "+e.name+"?")?(m.classList.add("disabled","opacity-50","cursor-not-allowed","pointer-events-none"),m.innerHTML='Deleting...',!0):!1},d.appendChild(m)}return n.appendChild(d),n}function Q(e,t){var r=document.getElementById("fileTableBody");if(r){var n=new Set(t.map(function(a){return a.name})),u=document.createDocumentFragment();f.forEach(function(a){if(!e.find(function(l){return l.name===a})){var i={name:a,type:Z(a),sizeFormatted:"--",date:"--"};u.appendChild(T(i,!1,!0))}}),e.forEach(function(a){var i=n.has(a.name),l=f.has(a.name);u.appendChild(T(a,i,l))}),r.replaceChildren(u)}}function _(){document.querySelectorAll("#fileTableBody tr[data-filename]").forEach(function(e){g.add(e.getAttribute("data-filename"))})}function h(){_(),p=setInterval(c,G)}function I(){p&&(clearInterval(p),p=null)}function c(){R()||fetch("/files/list").then(function(e){return e.json()}).then(function(e){if(e.success){var t=new Set(e.files.map(function(n){return n.name})),r=e.files.filter(function(n){return!g.has(n.name)});Q(e.files,r),g=t,z(e.files.length===0)}}).catch(function(e){console.error("File polling error:",e)})}function U(e){f.add(e),c()}function V(e){f.delete(e),g.add(e),c()}document.addEventListener("visibilitychange",function(){document.hidden?I():h()}),document.readyState==="loading"?document.addEventListener("DOMContentLoaded",h):h(),window.BigDump=window.BigDump||{},window.BigDump.refreshFileList=c,window.BigDump.trackUploadStart=U,window.BigDump.trackUploadEnd=V,window.BigDump.startFilePolling=h,window.BigDump.stopFilePolling=I,window.refreshFileList=c})(); diff --git a/assets/js/bigdump.js b/assets/js/bigdump.js index a9252a7..f556a4c 100644 --- a/assets/js/bigdump.js +++ b/assets/js/bigdump.js @@ -34,6 +34,48 @@ }); } + /** + * Disable a button with loading state to prevent double-click + * @param {HTMLElement} button - The button element + * @param {string} loadingText - Text to show while loading + */ + function disableButtonWithLoading(button, loadingText) { + button.disabled = true; + button.classList.add('opacity-50', 'cursor-not-allowed'); + button.innerHTML = '' + loadingText; + } + + /** + * Initialize button disable behavior for import and delete forms + * Prevents double-click by disabling buttons after first click + */ + function initButtonDisable() { + // Handle import forms (forms with 'fn' input but no 'action' input) + document.querySelectorAll('form').forEach(function(form) { + var fnInput = form.querySelector('input[name="fn"]'); + var actionInput = form.querySelector('input[name="action"]'); + var submitBtn = form.querySelector('button[type="submit"]'); + + if (!submitBtn) return; + + // Import forms: have 'fn' but no 'action=delete' + if (fnInput && (!actionInput || actionInput.value !== 'delete')) { + form.addEventListener('submit', function() { + disableButtonWithLoading(submitBtn, 'Importing...'); + }); + } + + // Delete forms: have 'action=delete' + if (actionInput && actionInput.value === 'delete') { + form.addEventListener('submit', function(e) { + // The confirm is already handled by onsubmit in HTML + // If we reach here, user confirmed, so disable button + disableButtonWithLoading(submitBtn, 'Deleting...'); + }); + } + }); + } + /** * Initialize dark mode toggle */ @@ -56,6 +98,7 @@ function init() { initLoadingOverlay(); initDarkModeToggle(); + initButtonDisable(); } // Initialize when DOM is ready diff --git a/assets/js/celebration.js b/assets/js/celebration.js new file mode 100644 index 0000000..5d80a74 --- /dev/null +++ b/assets/js/celebration.js @@ -0,0 +1,466 @@ +/** + * BigDump - Celebration Effects Module (Enhanced) + * Creates realistic fireworks and confetti animations on import completion. + * Features: particle trails, glow effects, 3D confetti rotation, air resistance. + */ +(function() { + 'use strict'; + + var canvas = null; + var ctx = null; + var animationId = null; + var particles = []; + var confetti = []; + var trails = []; + var isRunning = false; + var startTime = 0; + var fireworksDuration = 20000; // 20 seconds of fireworks + var confettiForever = true; // Confetti falls indefinitely + + // Vibrant colors for fireworks + var fireworkPalettes = [ + ['#FF6B6B', '#FF8E8E', '#FFB4B4'], // Red + ['#4ECDC4', '#7EDCD6', '#A8EBE7'], // Teal + ['#45B7D1', '#6FC9DE', '#99DBEB'], // Blue + ['#FFD93D', '#FFE469', '#FFEF9A'], // Yellow + ['#6BCB77', '#8DD896', '#AFE5B5'], // Green + ['#FF8C00', '#FFA533', '#FFBE66'], // Orange + ['#DA70D6', '#E494E0', '#EEB8EA'], // Orchid + ['#9B59B6', '#B07CC6', '#C59FD6'], // Purple + ['#E74C3C', '#ED7669', '#F3A096'], // Coral + ['#3498DB', '#5DADE2', '#85C1E9'] // Sky blue + ]; + + var confettiColors = [ + '#FF6B6B', '#4ECDC4', '#45B7D1', '#FFD93D', '#6BCB77', + '#FF8C00', '#DA70D6', '#9B59B6', '#E74C3C', '#3498DB', + '#F39C12', '#1ABC9C', '#E91E63', '#00BCD4', '#8BC34A' + ]; + + /** + * Enhanced Particle class with trails and glow + */ + function Particle(x, y, color, velocity, options) { + options = options || {}; + this.x = x; + this.y = y; + this.color = color; + this.velocity = velocity; + this.gravity = options.gravity || 0.06; + this.decay = options.decay || 0.015; + this.alpha = 1; + this.size = options.size || 3; + this.hasTrail = options.hasTrail !== false; + this.glowSize = options.glowSize || this.size * 2; + this.friction = options.friction || 0.98; + this.trail = []; + this.maxTrailLength = options.trailLength || 5; + } + + Particle.prototype.update = function() { + // Store trail position + if (this.hasTrail && this.alpha > 0.3) { + this.trail.push({ x: this.x, y: this.y, alpha: this.alpha }); + if (this.trail.length > this.maxTrailLength) { + this.trail.shift(); + } + } + + this.velocity.x *= this.friction; + this.velocity.y *= this.friction; + this.velocity.y += this.gravity; + this.x += this.velocity.x; + this.y += this.velocity.y; + this.alpha -= this.decay; + return this.alpha > 0; + }; + + Particle.prototype.draw = function(ctx) { + // Draw trail + if (this.hasTrail && this.trail.length > 0) { + for (var i = 0; i < this.trail.length; i++) { + var t = this.trail[i]; + var trailAlpha = (i / this.trail.length) * this.alpha * 0.5; + var trailSize = this.size * (0.3 + (i / this.trail.length) * 0.5); + ctx.save(); + ctx.globalAlpha = trailAlpha; + ctx.beginPath(); + ctx.arc(t.x, t.y, trailSize, 0, Math.PI * 2); + ctx.fillStyle = this.color; + ctx.fill(); + ctx.restore(); + } + } + + // Draw glow + ctx.save(); + ctx.globalAlpha = this.alpha * 0.4; + ctx.beginPath(); + ctx.arc(this.x, this.y, this.glowSize, 0, Math.PI * 2); + var gradient = ctx.createRadialGradient(this.x, this.y, 0, this.x, this.y, this.glowSize); + gradient.addColorStop(0, this.color); + gradient.addColorStop(1, 'transparent'); + ctx.fillStyle = gradient; + ctx.fill(); + ctx.restore(); + + // Draw core + ctx.save(); + ctx.globalAlpha = this.alpha; + ctx.beginPath(); + ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2); + ctx.fillStyle = this.color; + ctx.fill(); + + // Bright center + ctx.beginPath(); + ctx.arc(this.x, this.y, this.size * 0.5, 0, Math.PI * 2); + ctx.fillStyle = '#FFFFFF'; + ctx.globalAlpha = this.alpha * 0.8; + ctx.fill(); + ctx.restore(); + }; + + /** + * Enhanced Confetti with 3D rotation and air resistance + */ + function ConfettiPiece(x, y, color) { + this.x = x; + this.y = y; + this.color = color; + this.width = Math.random() * 12 + 6; + this.height = Math.random() * 8 + 4; + this.velocity = { + x: (Math.random() - 0.5) * 8, + y: Math.random() * 2 + 1 + }; + // 3D rotation angles + this.rotationX = Math.random() * 360; + this.rotationY = Math.random() * 360; + this.rotationZ = Math.random() * 360; + this.rotationSpeedX = (Math.random() - 0.5) * 8; + this.rotationSpeedY = (Math.random() - 0.5) * 12; + this.rotationSpeedZ = (Math.random() - 0.5) * 6; + // Oscillation (swaying) + this.oscillationSpeed = Math.random() * 0.08 + 0.03; + this.oscillationDistance = Math.random() * 60 + 30; + this.initialX = x; + this.time = Math.random() * 100; + // Air resistance + this.drag = 0.98 + Math.random() * 0.015; + this.terminalVelocity = 4 + Math.random() * 2; + } + + ConfettiPiece.prototype.update = function(canvasHeight) { + this.time += this.oscillationSpeed; + + // Horizontal oscillation (swaying in wind) + this.x = this.initialX + Math.sin(this.time) * this.oscillationDistance; + this.initialX += this.velocity.x * 0.1; // Drift + + // Apply drag + this.velocity.x *= this.drag; + this.velocity.y += 0.08; // Gravity + + // Terminal velocity + if (this.velocity.y > this.terminalVelocity) { + this.velocity.y = this.terminalVelocity; + } + + this.y += this.velocity.y; + + // Tumbling rotation + this.rotationX += this.rotationSpeedX; + this.rotationY += this.rotationSpeedY; + this.rotationZ += this.rotationSpeedZ; + + // Slow down rotation over time + this.rotationSpeedX *= 0.999; + this.rotationSpeedY *= 0.999; + + return this.y < canvasHeight + 50; + }; + + ConfettiPiece.prototype.draw = function(ctx) { + ctx.save(); + ctx.translate(this.x, this.y); + + // Apply 3D-like rotation (fake perspective) + var scaleX = Math.cos(this.rotationY * Math.PI / 180); + var scaleY = Math.cos(this.rotationX * Math.PI / 180); + ctx.rotate(this.rotationZ * Math.PI / 180); + ctx.scale(scaleX, scaleY); + + // Draw confetti piece with slight shadow + ctx.shadowColor = 'rgba(0,0,0,0.2)'; + ctx.shadowBlur = 2; + ctx.shadowOffsetY = 1; + + ctx.fillStyle = this.color; + ctx.fillRect(-this.width / 2, -this.height / 2, this.width, this.height); + + // Add shine effect + ctx.fillStyle = 'rgba(255,255,255,0.3)'; + ctx.fillRect(-this.width / 2, -this.height / 2, this.width * 0.4, this.height); + + ctx.restore(); + }; + + /** + * Create an enhanced firework explosion + */ + function createFirework(x, y) { + var palette = fireworkPalettes[Math.floor(Math.random() * fireworkPalettes.length)]; + var particleCount = 60 + Math.floor(Math.random() * 40); + var burstSpeed = 4 + Math.random() * 3; + + // Main explosion particles + for (var i = 0; i < particleCount; i++) { + var angle = (Math.PI * 2 / particleCount) * i + (Math.random() - 0.5) * 0.2; + var speed = burstSpeed * (0.6 + Math.random() * 0.8); + var colorIndex = Math.floor(Math.random() * palette.length); + var velocity = { + x: Math.cos(angle) * speed, + y: Math.sin(angle) * speed + }; + particles.push(new Particle(x, y, palette[colorIndex], velocity, { + gravity: 0.05, + decay: 0.010 + Math.random() * 0.008, + size: 2 + Math.random() * 2, + trailLength: 4, + glowSize: 6 + })); + } + + // Inner ring (brighter, faster decay) + var innerCount = 20 + Math.floor(Math.random() * 15); + for (var j = 0; j < innerCount; j++) { + var innerAngle = Math.random() * Math.PI * 2; + var innerSpeed = burstSpeed * 1.3 * Math.random(); + var innerVelocity = { + x: Math.cos(innerAngle) * innerSpeed, + y: Math.sin(innerAngle) * innerSpeed + }; + particles.push(new Particle(x, y, '#FFFFFF', innerVelocity, { + gravity: 0.04, + decay: 0.025, + size: 1.5, + trailLength: 3, + glowSize: 4, + hasTrail: true + })); + } + + // Sparkle particles (twinkle effect) + var sparkleCount = 15 + Math.floor(Math.random() * 10); + for (var k = 0; k < sparkleCount; k++) { + var sparkAngle = Math.random() * Math.PI * 2; + var sparkSpeed = burstSpeed * 0.3 + Math.random() * burstSpeed * 0.8; + var sparkVelocity = { + x: Math.cos(sparkAngle) * sparkSpeed, + y: Math.sin(sparkAngle) * sparkSpeed + }; + particles.push(new Particle(x, y, '#FFFACD', sparkVelocity, { + gravity: 0.08, + decay: 0.03 + Math.random() * 0.02, + size: 1, + glowSize: 3, + hasTrail: false + })); + } + } + + /** + * Create confetti burst from top + */ + function createConfettiBurst(count) { + if (!canvas) return; + + for (var i = 0; i < count; i++) { + var x = Math.random() * canvas.width; + var color = confettiColors[Math.floor(Math.random() * confettiColors.length)]; + confetti.push(new ConfettiPiece(x, -20 - Math.random() * 50, color)); + } + } + + /** + * Create confetti cannon from sides + */ + function createConfettiCannon(fromLeft) { + if (!canvas) return; + + var startX = fromLeft ? -10 : canvas.width + 10; + var angle = fromLeft ? -Math.PI / 4 : -Math.PI * 3 / 4; + + for (var i = 0; i < 30; i++) { + var spread = (Math.random() - 0.5) * 0.5; + var speed = 8 + Math.random() * 6; + var color = confettiColors[Math.floor(Math.random() * confettiColors.length)]; + var piece = new ConfettiPiece(startX, canvas.height * 0.7, color); + piece.velocity.x = Math.cos(angle + spread) * speed; + piece.velocity.y = Math.sin(angle + spread) * speed; + piece.initialX = startX; + confetti.push(piece); + } + } + + /** + * Initialize canvas + */ + function init() { + if (canvas) return; + + canvas = document.createElement('canvas'); + canvas.id = 'celebration-canvas'; + canvas.style.cssText = 'position: fixed; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; z-index: 9999;'; + document.body.appendChild(canvas); + + ctx = canvas.getContext('2d'); + resizeCanvas(); + + window.addEventListener('resize', resizeCanvas); + } + + /** + * Resize canvas to match window + */ + function resizeCanvas() { + if (!canvas) return; + canvas.width = window.innerWidth; + canvas.height = window.innerHeight; + } + + /** + * Animation loop + */ + function animate() { + if (!isRunning) return; + + var elapsed = Date.now() - startTime; + + // Clear canvas with slight fade for trail effect + ctx.fillStyle = 'rgba(0, 0, 0, 0.1)'; + ctx.fillRect(0, 0, canvas.width, canvas.height); + ctx.clearRect(0, 0, canvas.width, canvas.height); + + // Update and draw particles + particles = particles.filter(function(p) { + var alive = p.update(); + if (alive) p.draw(ctx); + return alive; + }); + + // Update and draw confetti + confetti = confetti.filter(function(c) { + var alive = c.update(canvas.height); + if (alive) c.draw(ctx); + return alive; + }); + + // Create new fireworks periodically for 20 seconds + if (elapsed < fireworksDuration) { + // More frequent fireworks at start, gradually decreasing + var fireworkChance = elapsed < 5000 ? 0.08 : (elapsed < 10000 ? 0.05 : 0.03); + if (Math.random() < fireworkChance) { + var x = Math.random() * canvas.width * 0.7 + canvas.width * 0.15; + var y = Math.random() * canvas.height * 0.4 + canvas.height * 0.08; + createFirework(x, y); + } + } + + // Add confetti continuously forever (or until stopped) + if (confettiForever && Math.random() < 0.15) { + createConfettiBurst(1); + } + + // Confetti cannons at specific intervals during first 15 seconds + if (elapsed < 15000) { + var cannonInterval = 2000; // Every 2 seconds + var cannonWindow = 30; + if (elapsed % cannonInterval < cannonWindow && Math.random() < 0.5) { + createConfettiCannon(Math.random() < 0.5); + } + } + + // Continue animation as long as running (confetti forever!) + if (isRunning) { + animationId = requestAnimationFrame(animate); + } else { + cleanup(); + } + } + + /** + * Clean up canvas and reset state + */ + function cleanup() { + isRunning = false; + if (animationId) { + cancelAnimationFrame(animationId); + animationId = null; + } + if (canvas && canvas.parentNode) { + canvas.parentNode.removeChild(canvas); + } + canvas = null; + ctx = null; + particles = []; + confetti = []; + trails = []; + window.removeEventListener('resize', resizeCanvas); + } + + /** + * Start the celebration effects + */ + function start() { + if (isRunning) return; + + init(); + isRunning = true; + startTime = Date.now(); + particles = []; + confetti = []; + trails = []; + + // Initial burst of fireworks (staggered) + var fireworkPositions = [ + { x: 0.3, y: 0.25, delay: 0 }, + { x: 0.7, y: 0.2, delay: 150 }, + { x: 0.5, y: 0.15, delay: 300 }, + { x: 0.25, y: 0.3, delay: 450 }, + { x: 0.75, y: 0.28, delay: 600 } + ]; + + fireworkPositions.forEach(function(pos) { + setTimeout(function() { + if (!isRunning || !canvas) return; + createFirework(canvas.width * pos.x, canvas.height * pos.y); + }, pos.delay); + }); + + // Initial confetti burst + createConfettiBurst(80); + + animate(); + console.log('Celebration started!'); + } + + /** + * Stop the celebration effects immediately + */ + function stop() { + if (!isRunning) return; + cleanup(); + console.log('Celebration stopped'); + } + + // Expose API + window.BigDump = window.BigDump || {}; + window.BigDump.celebration = { + start: start, + stop: stop, + isRunning: function() { return isRunning; } + }; + +})(); diff --git a/assets/js/favicon-animator.js b/assets/js/favicon-animator.js new file mode 100644 index 0000000..7a03b6c --- /dev/null +++ b/assets/js/favicon-animator.js @@ -0,0 +1,145 @@ +/** + * BigDump - Animated Favicon Module + * Creates a bouncing arrow animation during import process. + * Uses Canvas API to dynamically generate favicon frames. + */ +(function() { + 'use strict'; + + var canvas = null; + var ctx = null; + var animationId = null; + var originalFavicon = null; + var linkElement = null; + var frame = 0; + var isAnimating = false; + + // Animation settings + var SIZE = 32; // Favicon size + var ARROW_COLOR = '#3B82F6'; // Blue-500 + var BG_COLOR = '#1E3A5F'; // Dark blue background + var BOUNCE_HEIGHT = 6; // Pixels to bounce + var FRAME_RATE = 60; // ms between frames + + /** + * Initialize canvas and get original favicon + */ + function init() { + if (canvas) return; + + canvas = document.createElement('canvas'); + canvas.width = SIZE; + canvas.height = SIZE; + ctx = canvas.getContext('2d'); + + // Find and store original favicon + linkElement = document.querySelector('link[rel="icon"][type="image/png"]') || + document.querySelector('link[rel="icon"]'); + + if (linkElement) { + originalFavicon = linkElement.href; + } + } + + /** + * Draw a frame of the bouncing arrow animation + * @param {number} offset - Vertical bounce offset + */ + function drawFrame(offset) { + // Clear canvas + ctx.clearRect(0, 0, SIZE, SIZE); + + // Draw circular background + ctx.beginPath(); + ctx.arc(SIZE / 2, SIZE / 2, SIZE / 2 - 1, 0, Math.PI * 2); + ctx.fillStyle = BG_COLOR; + ctx.fill(); + + // Calculate arrow position with bounce + var centerX = SIZE / 2; + var centerY = SIZE / 2 + offset; + + // Draw downward arrow + ctx.beginPath(); + ctx.strokeStyle = ARROW_COLOR; + ctx.fillStyle = ARROW_COLOR; + ctx.lineWidth = 3; + ctx.lineCap = 'round'; + ctx.lineJoin = 'round'; + + // Arrow shaft + ctx.moveTo(centerX, centerY - 8); + ctx.lineTo(centerX, centerY + 4); + ctx.stroke(); + + // Arrow head (triangle) + ctx.beginPath(); + ctx.moveTo(centerX - 6, centerY); + ctx.lineTo(centerX, centerY + 8); + ctx.lineTo(centerX + 6, centerY); + ctx.closePath(); + ctx.fill(); + + // Update favicon + if (linkElement) { + linkElement.href = canvas.toDataURL('image/png'); + } + } + + /** + * Animation loop using eased bounce + */ + function animate() { + if (!isAnimating) return; + + // Eased bounce: sin wave for smooth up/down motion + var bounce = Math.sin(frame * 0.15) * BOUNCE_HEIGHT; + drawFrame(bounce); + + frame++; + animationId = setTimeout(animate, FRAME_RATE); + } + + /** + * Start the favicon animation + */ + function start() { + if (isAnimating) return; + + init(); + isAnimating = true; + frame = 0; + animate(); + console.log('Favicon animation started'); + } + + /** + * Stop the animation and restore original favicon + */ + function stop() { + if (!isAnimating) return; + + isAnimating = false; + + if (animationId) { + clearTimeout(animationId); + animationId = null; + } + + // Restore original favicon + if (linkElement && originalFavicon) { + linkElement.href = originalFavicon; + } + + console.log('Favicon animation stopped'); + } + + // Expose API + window.BigDump = window.BigDump || {}; + window.BigDump.faviconAnimator = { + start: start, + stop: stop, + isAnimating: function() { return isAnimating; } + }; + +})(); diff --git a/assets/js/filepolling.js b/assets/js/filepolling.js index 6f76eb1..b8587da 100644 --- a/assets/js/filepolling.js +++ b/assets/js/filepolling.js @@ -197,6 +197,13 @@ importBtn.type = 'submit'; importBtn.className = 'btn btn-green'; importBtn.textContent = 'Import'; + // Disable button on click to prevent double submission + form.onsubmit = function() { + importBtn.disabled = true; + importBtn.classList.add('opacity-50', 'cursor-not-allowed'); + importBtn.innerHTML = 'Importing...'; + return true; // Allow form submission + }; form.appendChild(importBtn); td5.appendChild(form); td5.appendChild(document.createTextNode(' ')); @@ -213,7 +220,19 @@ deleteLink.href = '?delete=' + encodeURIComponent(file.name); deleteLink.className = 'btn btn-red'; deleteLink.textContent = 'Delete'; - deleteLink.onclick = function() { return confirm('Delete ' + file.name + '?'); }; + deleteLink.onclick = function(e) { + if (deleteLink.classList.contains('disabled')) { + e.preventDefault(); + return false; + } + if (!confirm('Delete ' + file.name + '?')) { + return false; + } + // Disable button on click to prevent double action + deleteLink.classList.add('disabled', 'opacity-50', 'cursor-not-allowed', 'pointer-events-none'); + deleteLink.innerHTML = 'Deleting...'; + return true; + }; td5.appendChild(deleteLink); } diff --git a/assets/src/js/bigdump.js b/assets/src/js/bigdump.js index a9252a7..f556a4c 100644 --- a/assets/src/js/bigdump.js +++ b/assets/src/js/bigdump.js @@ -34,6 +34,48 @@ }); } + /** + * Disable a button with loading state to prevent double-click + * @param {HTMLElement} button - The button element + * @param {string} loadingText - Text to show while loading + */ + function disableButtonWithLoading(button, loadingText) { + button.disabled = true; + button.classList.add('opacity-50', 'cursor-not-allowed'); + button.innerHTML = '' + loadingText; + } + + /** + * Initialize button disable behavior for import and delete forms + * Prevents double-click by disabling buttons after first click + */ + function initButtonDisable() { + // Handle import forms (forms with 'fn' input but no 'action' input) + document.querySelectorAll('form').forEach(function(form) { + var fnInput = form.querySelector('input[name="fn"]'); + var actionInput = form.querySelector('input[name="action"]'); + var submitBtn = form.querySelector('button[type="submit"]'); + + if (!submitBtn) return; + + // Import forms: have 'fn' but no 'action=delete' + if (fnInput && (!actionInput || actionInput.value !== 'delete')) { + form.addEventListener('submit', function() { + disableButtonWithLoading(submitBtn, 'Importing...'); + }); + } + + // Delete forms: have 'action=delete' + if (actionInput && actionInput.value === 'delete') { + form.addEventListener('submit', function(e) { + // The confirm is already handled by onsubmit in HTML + // If we reach here, user confirmed, so disable button + disableButtonWithLoading(submitBtn, 'Deleting...'); + }); + } + }); + } + /** * Initialize dark mode toggle */ @@ -56,6 +98,7 @@ function init() { initLoadingOverlay(); initDarkModeToggle(); + initButtonDisable(); } // Initialize when DOM is ready diff --git a/assets/src/js/celebration.js b/assets/src/js/celebration.js new file mode 100644 index 0000000..5d80a74 --- /dev/null +++ b/assets/src/js/celebration.js @@ -0,0 +1,466 @@ +/** + * BigDump - Celebration Effects Module (Enhanced) + * Creates realistic fireworks and confetti animations on import completion. + * Features: particle trails, glow effects, 3D confetti rotation, air resistance. + */ +(function() { + 'use strict'; + + var canvas = null; + var ctx = null; + var animationId = null; + var particles = []; + var confetti = []; + var trails = []; + var isRunning = false; + var startTime = 0; + var fireworksDuration = 20000; // 20 seconds of fireworks + var confettiForever = true; // Confetti falls indefinitely + + // Vibrant colors for fireworks + var fireworkPalettes = [ + ['#FF6B6B', '#FF8E8E', '#FFB4B4'], // Red + ['#4ECDC4', '#7EDCD6', '#A8EBE7'], // Teal + ['#45B7D1', '#6FC9DE', '#99DBEB'], // Blue + ['#FFD93D', '#FFE469', '#FFEF9A'], // Yellow + ['#6BCB77', '#8DD896', '#AFE5B5'], // Green + ['#FF8C00', '#FFA533', '#FFBE66'], // Orange + ['#DA70D6', '#E494E0', '#EEB8EA'], // Orchid + ['#9B59B6', '#B07CC6', '#C59FD6'], // Purple + ['#E74C3C', '#ED7669', '#F3A096'], // Coral + ['#3498DB', '#5DADE2', '#85C1E9'] // Sky blue + ]; + + var confettiColors = [ + '#FF6B6B', '#4ECDC4', '#45B7D1', '#FFD93D', '#6BCB77', + '#FF8C00', '#DA70D6', '#9B59B6', '#E74C3C', '#3498DB', + '#F39C12', '#1ABC9C', '#E91E63', '#00BCD4', '#8BC34A' + ]; + + /** + * Enhanced Particle class with trails and glow + */ + function Particle(x, y, color, velocity, options) { + options = options || {}; + this.x = x; + this.y = y; + this.color = color; + this.velocity = velocity; + this.gravity = options.gravity || 0.06; + this.decay = options.decay || 0.015; + this.alpha = 1; + this.size = options.size || 3; + this.hasTrail = options.hasTrail !== false; + this.glowSize = options.glowSize || this.size * 2; + this.friction = options.friction || 0.98; + this.trail = []; + this.maxTrailLength = options.trailLength || 5; + } + + Particle.prototype.update = function() { + // Store trail position + if (this.hasTrail && this.alpha > 0.3) { + this.trail.push({ x: this.x, y: this.y, alpha: this.alpha }); + if (this.trail.length > this.maxTrailLength) { + this.trail.shift(); + } + } + + this.velocity.x *= this.friction; + this.velocity.y *= this.friction; + this.velocity.y += this.gravity; + this.x += this.velocity.x; + this.y += this.velocity.y; + this.alpha -= this.decay; + return this.alpha > 0; + }; + + Particle.prototype.draw = function(ctx) { + // Draw trail + if (this.hasTrail && this.trail.length > 0) { + for (var i = 0; i < this.trail.length; i++) { + var t = this.trail[i]; + var trailAlpha = (i / this.trail.length) * this.alpha * 0.5; + var trailSize = this.size * (0.3 + (i / this.trail.length) * 0.5); + ctx.save(); + ctx.globalAlpha = trailAlpha; + ctx.beginPath(); + ctx.arc(t.x, t.y, trailSize, 0, Math.PI * 2); + ctx.fillStyle = this.color; + ctx.fill(); + ctx.restore(); + } + } + + // Draw glow + ctx.save(); + ctx.globalAlpha = this.alpha * 0.4; + ctx.beginPath(); + ctx.arc(this.x, this.y, this.glowSize, 0, Math.PI * 2); + var gradient = ctx.createRadialGradient(this.x, this.y, 0, this.x, this.y, this.glowSize); + gradient.addColorStop(0, this.color); + gradient.addColorStop(1, 'transparent'); + ctx.fillStyle = gradient; + ctx.fill(); + ctx.restore(); + + // Draw core + ctx.save(); + ctx.globalAlpha = this.alpha; + ctx.beginPath(); + ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2); + ctx.fillStyle = this.color; + ctx.fill(); + + // Bright center + ctx.beginPath(); + ctx.arc(this.x, this.y, this.size * 0.5, 0, Math.PI * 2); + ctx.fillStyle = '#FFFFFF'; + ctx.globalAlpha = this.alpha * 0.8; + ctx.fill(); + ctx.restore(); + }; + + /** + * Enhanced Confetti with 3D rotation and air resistance + */ + function ConfettiPiece(x, y, color) { + this.x = x; + this.y = y; + this.color = color; + this.width = Math.random() * 12 + 6; + this.height = Math.random() * 8 + 4; + this.velocity = { + x: (Math.random() - 0.5) * 8, + y: Math.random() * 2 + 1 + }; + // 3D rotation angles + this.rotationX = Math.random() * 360; + this.rotationY = Math.random() * 360; + this.rotationZ = Math.random() * 360; + this.rotationSpeedX = (Math.random() - 0.5) * 8; + this.rotationSpeedY = (Math.random() - 0.5) * 12; + this.rotationSpeedZ = (Math.random() - 0.5) * 6; + // Oscillation (swaying) + this.oscillationSpeed = Math.random() * 0.08 + 0.03; + this.oscillationDistance = Math.random() * 60 + 30; + this.initialX = x; + this.time = Math.random() * 100; + // Air resistance + this.drag = 0.98 + Math.random() * 0.015; + this.terminalVelocity = 4 + Math.random() * 2; + } + + ConfettiPiece.prototype.update = function(canvasHeight) { + this.time += this.oscillationSpeed; + + // Horizontal oscillation (swaying in wind) + this.x = this.initialX + Math.sin(this.time) * this.oscillationDistance; + this.initialX += this.velocity.x * 0.1; // Drift + + // Apply drag + this.velocity.x *= this.drag; + this.velocity.y += 0.08; // Gravity + + // Terminal velocity + if (this.velocity.y > this.terminalVelocity) { + this.velocity.y = this.terminalVelocity; + } + + this.y += this.velocity.y; + + // Tumbling rotation + this.rotationX += this.rotationSpeedX; + this.rotationY += this.rotationSpeedY; + this.rotationZ += this.rotationSpeedZ; + + // Slow down rotation over time + this.rotationSpeedX *= 0.999; + this.rotationSpeedY *= 0.999; + + return this.y < canvasHeight + 50; + }; + + ConfettiPiece.prototype.draw = function(ctx) { + ctx.save(); + ctx.translate(this.x, this.y); + + // Apply 3D-like rotation (fake perspective) + var scaleX = Math.cos(this.rotationY * Math.PI / 180); + var scaleY = Math.cos(this.rotationX * Math.PI / 180); + ctx.rotate(this.rotationZ * Math.PI / 180); + ctx.scale(scaleX, scaleY); + + // Draw confetti piece with slight shadow + ctx.shadowColor = 'rgba(0,0,0,0.2)'; + ctx.shadowBlur = 2; + ctx.shadowOffsetY = 1; + + ctx.fillStyle = this.color; + ctx.fillRect(-this.width / 2, -this.height / 2, this.width, this.height); + + // Add shine effect + ctx.fillStyle = 'rgba(255,255,255,0.3)'; + ctx.fillRect(-this.width / 2, -this.height / 2, this.width * 0.4, this.height); + + ctx.restore(); + }; + + /** + * Create an enhanced firework explosion + */ + function createFirework(x, y) { + var palette = fireworkPalettes[Math.floor(Math.random() * fireworkPalettes.length)]; + var particleCount = 60 + Math.floor(Math.random() * 40); + var burstSpeed = 4 + Math.random() * 3; + + // Main explosion particles + for (var i = 0; i < particleCount; i++) { + var angle = (Math.PI * 2 / particleCount) * i + (Math.random() - 0.5) * 0.2; + var speed = burstSpeed * (0.6 + Math.random() * 0.8); + var colorIndex = Math.floor(Math.random() * palette.length); + var velocity = { + x: Math.cos(angle) * speed, + y: Math.sin(angle) * speed + }; + particles.push(new Particle(x, y, palette[colorIndex], velocity, { + gravity: 0.05, + decay: 0.010 + Math.random() * 0.008, + size: 2 + Math.random() * 2, + trailLength: 4, + glowSize: 6 + })); + } + + // Inner ring (brighter, faster decay) + var innerCount = 20 + Math.floor(Math.random() * 15); + for (var j = 0; j < innerCount; j++) { + var innerAngle = Math.random() * Math.PI * 2; + var innerSpeed = burstSpeed * 1.3 * Math.random(); + var innerVelocity = { + x: Math.cos(innerAngle) * innerSpeed, + y: Math.sin(innerAngle) * innerSpeed + }; + particles.push(new Particle(x, y, '#FFFFFF', innerVelocity, { + gravity: 0.04, + decay: 0.025, + size: 1.5, + trailLength: 3, + glowSize: 4, + hasTrail: true + })); + } + + // Sparkle particles (twinkle effect) + var sparkleCount = 15 + Math.floor(Math.random() * 10); + for (var k = 0; k < sparkleCount; k++) { + var sparkAngle = Math.random() * Math.PI * 2; + var sparkSpeed = burstSpeed * 0.3 + Math.random() * burstSpeed * 0.8; + var sparkVelocity = { + x: Math.cos(sparkAngle) * sparkSpeed, + y: Math.sin(sparkAngle) * sparkSpeed + }; + particles.push(new Particle(x, y, '#FFFACD', sparkVelocity, { + gravity: 0.08, + decay: 0.03 + Math.random() * 0.02, + size: 1, + glowSize: 3, + hasTrail: false + })); + } + } + + /** + * Create confetti burst from top + */ + function createConfettiBurst(count) { + if (!canvas) return; + + for (var i = 0; i < count; i++) { + var x = Math.random() * canvas.width; + var color = confettiColors[Math.floor(Math.random() * confettiColors.length)]; + confetti.push(new ConfettiPiece(x, -20 - Math.random() * 50, color)); + } + } + + /** + * Create confetti cannon from sides + */ + function createConfettiCannon(fromLeft) { + if (!canvas) return; + + var startX = fromLeft ? -10 : canvas.width + 10; + var angle = fromLeft ? -Math.PI / 4 : -Math.PI * 3 / 4; + + for (var i = 0; i < 30; i++) { + var spread = (Math.random() - 0.5) * 0.5; + var speed = 8 + Math.random() * 6; + var color = confettiColors[Math.floor(Math.random() * confettiColors.length)]; + var piece = new ConfettiPiece(startX, canvas.height * 0.7, color); + piece.velocity.x = Math.cos(angle + spread) * speed; + piece.velocity.y = Math.sin(angle + spread) * speed; + piece.initialX = startX; + confetti.push(piece); + } + } + + /** + * Initialize canvas + */ + function init() { + if (canvas) return; + + canvas = document.createElement('canvas'); + canvas.id = 'celebration-canvas'; + canvas.style.cssText = 'position: fixed; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; z-index: 9999;'; + document.body.appendChild(canvas); + + ctx = canvas.getContext('2d'); + resizeCanvas(); + + window.addEventListener('resize', resizeCanvas); + } + + /** + * Resize canvas to match window + */ + function resizeCanvas() { + if (!canvas) return; + canvas.width = window.innerWidth; + canvas.height = window.innerHeight; + } + + /** + * Animation loop + */ + function animate() { + if (!isRunning) return; + + var elapsed = Date.now() - startTime; + + // Clear canvas with slight fade for trail effect + ctx.fillStyle = 'rgba(0, 0, 0, 0.1)'; + ctx.fillRect(0, 0, canvas.width, canvas.height); + ctx.clearRect(0, 0, canvas.width, canvas.height); + + // Update and draw particles + particles = particles.filter(function(p) { + var alive = p.update(); + if (alive) p.draw(ctx); + return alive; + }); + + // Update and draw confetti + confetti = confetti.filter(function(c) { + var alive = c.update(canvas.height); + if (alive) c.draw(ctx); + return alive; + }); + + // Create new fireworks periodically for 20 seconds + if (elapsed < fireworksDuration) { + // More frequent fireworks at start, gradually decreasing + var fireworkChance = elapsed < 5000 ? 0.08 : (elapsed < 10000 ? 0.05 : 0.03); + if (Math.random() < fireworkChance) { + var x = Math.random() * canvas.width * 0.7 + canvas.width * 0.15; + var y = Math.random() * canvas.height * 0.4 + canvas.height * 0.08; + createFirework(x, y); + } + } + + // Add confetti continuously forever (or until stopped) + if (confettiForever && Math.random() < 0.15) { + createConfettiBurst(1); + } + + // Confetti cannons at specific intervals during first 15 seconds + if (elapsed < 15000) { + var cannonInterval = 2000; // Every 2 seconds + var cannonWindow = 30; + if (elapsed % cannonInterval < cannonWindow && Math.random() < 0.5) { + createConfettiCannon(Math.random() < 0.5); + } + } + + // Continue animation as long as running (confetti forever!) + if (isRunning) { + animationId = requestAnimationFrame(animate); + } else { + cleanup(); + } + } + + /** + * Clean up canvas and reset state + */ + function cleanup() { + isRunning = false; + if (animationId) { + cancelAnimationFrame(animationId); + animationId = null; + } + if (canvas && canvas.parentNode) { + canvas.parentNode.removeChild(canvas); + } + canvas = null; + ctx = null; + particles = []; + confetti = []; + trails = []; + window.removeEventListener('resize', resizeCanvas); + } + + /** + * Start the celebration effects + */ + function start() { + if (isRunning) return; + + init(); + isRunning = true; + startTime = Date.now(); + particles = []; + confetti = []; + trails = []; + + // Initial burst of fireworks (staggered) + var fireworkPositions = [ + { x: 0.3, y: 0.25, delay: 0 }, + { x: 0.7, y: 0.2, delay: 150 }, + { x: 0.5, y: 0.15, delay: 300 }, + { x: 0.25, y: 0.3, delay: 450 }, + { x: 0.75, y: 0.28, delay: 600 } + ]; + + fireworkPositions.forEach(function(pos) { + setTimeout(function() { + if (!isRunning || !canvas) return; + createFirework(canvas.width * pos.x, canvas.height * pos.y); + }, pos.delay); + }); + + // Initial confetti burst + createConfettiBurst(80); + + animate(); + console.log('Celebration started!'); + } + + /** + * Stop the celebration effects immediately + */ + function stop() { + if (!isRunning) return; + cleanup(); + console.log('Celebration stopped'); + } + + // Expose API + window.BigDump = window.BigDump || {}; + window.BigDump.celebration = { + start: start, + stop: stop, + isRunning: function() { return isRunning; } + }; + +})(); diff --git a/assets/src/js/favicon-animator.js b/assets/src/js/favicon-animator.js new file mode 100644 index 0000000..7a03b6c --- /dev/null +++ b/assets/src/js/favicon-animator.js @@ -0,0 +1,145 @@ +/** + * BigDump - Animated Favicon Module + * Creates a bouncing arrow animation during import process. + * Uses Canvas API to dynamically generate favicon frames. + */ +(function() { + 'use strict'; + + var canvas = null; + var ctx = null; + var animationId = null; + var originalFavicon = null; + var linkElement = null; + var frame = 0; + var isAnimating = false; + + // Animation settings + var SIZE = 32; // Favicon size + var ARROW_COLOR = '#3B82F6'; // Blue-500 + var BG_COLOR = '#1E3A5F'; // Dark blue background + var BOUNCE_HEIGHT = 6; // Pixels to bounce + var FRAME_RATE = 60; // ms between frames + + /** + * Initialize canvas and get original favicon + */ + function init() { + if (canvas) return; + + canvas = document.createElement('canvas'); + canvas.width = SIZE; + canvas.height = SIZE; + ctx = canvas.getContext('2d'); + + // Find and store original favicon + linkElement = document.querySelector('link[rel="icon"][type="image/png"]') || + document.querySelector('link[rel="icon"]'); + + if (linkElement) { + originalFavicon = linkElement.href; + } + } + + /** + * Draw a frame of the bouncing arrow animation + * @param {number} offset - Vertical bounce offset + */ + function drawFrame(offset) { + // Clear canvas + ctx.clearRect(0, 0, SIZE, SIZE); + + // Draw circular background + ctx.beginPath(); + ctx.arc(SIZE / 2, SIZE / 2, SIZE / 2 - 1, 0, Math.PI * 2); + ctx.fillStyle = BG_COLOR; + ctx.fill(); + + // Calculate arrow position with bounce + var centerX = SIZE / 2; + var centerY = SIZE / 2 + offset; + + // Draw downward arrow + ctx.beginPath(); + ctx.strokeStyle = ARROW_COLOR; + ctx.fillStyle = ARROW_COLOR; + ctx.lineWidth = 3; + ctx.lineCap = 'round'; + ctx.lineJoin = 'round'; + + // Arrow shaft + ctx.moveTo(centerX, centerY - 8); + ctx.lineTo(centerX, centerY + 4); + ctx.stroke(); + + // Arrow head (triangle) + ctx.beginPath(); + ctx.moveTo(centerX - 6, centerY); + ctx.lineTo(centerX, centerY + 8); + ctx.lineTo(centerX + 6, centerY); + ctx.closePath(); + ctx.fill(); + + // Update favicon + if (linkElement) { + linkElement.href = canvas.toDataURL('image/png'); + } + } + + /** + * Animation loop using eased bounce + */ + function animate() { + if (!isAnimating) return; + + // Eased bounce: sin wave for smooth up/down motion + var bounce = Math.sin(frame * 0.15) * BOUNCE_HEIGHT; + drawFrame(bounce); + + frame++; + animationId = setTimeout(animate, FRAME_RATE); + } + + /** + * Start the favicon animation + */ + function start() { + if (isAnimating) return; + + init(); + isAnimating = true; + frame = 0; + animate(); + console.log('Favicon animation started'); + } + + /** + * Stop the animation and restore original favicon + */ + function stop() { + if (!isAnimating) return; + + isAnimating = false; + + if (animationId) { + clearTimeout(animationId); + animationId = null; + } + + // Restore original favicon + if (linkElement && originalFavicon) { + linkElement.href = originalFavicon; + } + + console.log('Favicon animation stopped'); + } + + // Expose API + window.BigDump = window.BigDump || {}; + window.BigDump.faviconAnimator = { + start: start, + stop: stop, + isAnimating: function() { return isAnimating; } + }; + +})(); diff --git a/assets/src/js/filepolling.js b/assets/src/js/filepolling.js index b2a5397..77a1438 100644 --- a/assets/src/js/filepolling.js +++ b/assets/src/js/filepolling.js @@ -203,6 +203,13 @@ importBtn.type = 'submit'; importBtn.className = 'btn btn-green'; importBtn.textContent = 'Import'; + // Disable button on click to prevent double submission + form.onsubmit = function() { + importBtn.disabled = true; + importBtn.classList.add('opacity-50', 'cursor-not-allowed'); + importBtn.innerHTML = 'Importing...'; + return true; // Allow form submission + }; form.appendChild(importBtn); td5.appendChild(form); td5.appendChild(document.createTextNode(' ')); @@ -219,7 +226,19 @@ deleteLink.href = '?delete=' + encodeURIComponent(file.name); deleteLink.className = 'btn btn-red'; deleteLink.textContent = 'Delete'; - deleteLink.onclick = function() { return confirm('Delete ' + file.name + '?'); }; + deleteLink.onclick = function(e) { + if (deleteLink.classList.contains('disabled')) { + e.preventDefault(); + return false; + } + if (!confirm('Delete ' + file.name + '?')) { + return false; + } + // Disable button on click to prevent double action + deleteLink.classList.add('disabled', 'opacity-50', 'cursor-not-allowed', 'pointer-events-none'); + deleteLink.innerHTML = 'Deleting...'; + return true; + }; td5.appendChild(deleteLink); } diff --git a/src/Core/Application.php b/src/Core/Application.php index d2be40a..6f7c0ee 100644 --- a/src/Core/Application.php +++ b/src/Core/Application.php @@ -23,7 +23,7 @@ class Application /** * Application version. */ - public const VERSION = '2.26'; + public const VERSION = '2.28'; /** * Configuration instance. diff --git a/src/Core/View.php b/src/Core/View.php index 60c3a7b..9c23751 100644 --- a/src/Core/View.php +++ b/src/Core/View.php @@ -313,6 +313,8 @@ private function loadInlinedAssets(): void 'modal.min.js', 'filepolling.min.js', 'fileupload.min.js', + 'celebration.min.js', + 'favicon-animator.min.js', ]; foreach ($jsOrder as $jsFile) { diff --git a/src/Models/FileHandler.php b/src/Models/FileHandler.php index 0388193..d41d2a5 100644 --- a/src/Models/FileHandler.php +++ b/src/Models/FileHandler.php @@ -259,7 +259,7 @@ private function ensureUploadDir(): void /** * Lists available dump files * - * @return array List of files + * @return array List of files sorted by date (newest first) */ public function listFiles(): array { @@ -297,10 +297,12 @@ public function listFiles(): array continue; } + $mtime = filemtime($filepath) ?: 0; $files[] = [ 'name' => $filename, 'size' => filesize($filepath) ?: 0, - 'date' => date('Y-m-d H:i:s', filemtime($filepath) ?: 0), + 'date' => date('Y-m-d H:i:s', $mtime), + 'timestamp' => $mtime, 'type' => $this->getFileType($extension), 'path' => $filepath, ]; @@ -308,8 +310,8 @@ public function listFiles(): array closedir($handle); - // Sort by name - usort($files, fn($a, $b) => strcasecmp($a['name'], $b['name'])); + // Sort by modification date (newest first) + usort($files, fn($a, $b) => $b['timestamp'] <=> $a['timestamp']); return $files; } diff --git a/src/Services/AjaxService.php b/src/Services/AjaxService.php index 1e1e7cc..0d32d28 100644 --- a/src/Services/AjaxService.php +++ b/src/Services/AjaxService.php @@ -135,8 +135,11 @@ function displayErrorInPage(message, stats) { helpText + '

    ' + actionButtons + - 'Resume' + - 'Back to Home' + + '
    ' + + '' + + '' + + '
    ' + + 'Back to home' + '
    '; // Find main content area and insert error at the beginning @@ -241,28 +244,28 @@ function displaySuccessInPage(stats, warnings, warningsCount, warningsLimitReach '
    ' + '' + '
    ' + - '
    ' + - '
    ' + queriesDone + '
    ' + - '
    Queries
    ' + + '
    ' + + '
    ' + queriesDone + '
    ' + + '
    Queries
    ' + '
    ' + - '
    ' + - '
    ' + linesDone + '
    ' + - '
    Lines
    ' + + '
    ' + + '
    ' + linesDone + '
    ' + + '
    Lines
    ' + '
    ' + - '
    ' + - '
    ' + bytesDone + '
    ' + - '
    Processed
    ' + + '
    ' + + '
    ' + bytesDone + '
    ' + + '
    Processed
    ' + '
    ' + - '
    ' + - '
    ' + elapsedTime + '
    ' + - '
    Duration
    ' + + '
    ' + + '
    ' + elapsedTime + '
    ' + + '
    Duration
    ' + '
    ' + '
    ' + '
    ' + warningsHtml + ''; // Find main content area and replace content @@ -301,6 +304,7 @@ function escapeHtml(text) { // Elapsed timer variables var elapsedTimerInterval = null; + var timerStarted = false; // Prevent timer reset on SSE reconnect /** * Formats elapsed seconds into HH:MM:SS format. @@ -325,10 +329,14 @@ function updateElapsedTimer() { /** * Starts the elapsed timer (call once when import begins). + * Will not reset if already started (prevents reset on SSE reconnect). */ function startElapsedTimer() { - // Reset start time - startTime = Date.now(); + // Only reset start time on first call (not on SSE reconnect) + if (!timerStarted) { + startTime = Date.now(); + timerStarted = true; + } // Clear any existing interval if (elapsedTimerInterval) { clearInterval(elapsedTimerInterval); @@ -346,6 +354,8 @@ function stopElapsedTimer() { clearInterval(elapsedTimerInterval); elapsedTimerInterval = null; } + // Reset flag so next import can start fresh + timerStarted = false; } /** diff --git a/templates/error.php b/templates/error.php index aca9db6..94cc5c0 100644 --- a/templates/error.php +++ b/templates/error.php @@ -37,7 +37,7 @@

    How to fix it:

      -
    1. Click "Back to Home" below
    2. +
    3. Click "Back to home" below
    4. Select your SQL dump file from the file list
    5. Click "Start Import" to begin
    6. Wait for the import to complete without refreshing the page
    7. @@ -70,7 +70,7 @@

      How to fix it:

        -
      1. Click "Back to Home" below
      2. +
      3. Click "Back to home" below
      4. Check the file list to see available files
      5. Upload your SQL dump file again if needed
      6. Make sure the file has .sql, .gz, or .csv extension
      7. @@ -86,6 +86,6 @@ diff --git a/templates/home.php b/templates/home.php index 4b8fbcd..dca55b8 100644 --- a/templates/home.php +++ b/templates/home.php @@ -105,7 +105,7 @@ Filename Size - Date + Date Added Type Actions diff --git a/templates/import.php b/templates/import.php index e1fa525..c63d018 100644 --- a/templates/import.php +++ b/templates/import.php @@ -342,7 +342,7 @@ hasError()): ?> @@ -386,8 +386,11 @@ class="btn btn-amber" or - Start Over (resume) - Back to Home +
        + + +
        + Back to home (DROP old tables before restarting) diff --git a/templates/layout.php b/templates/layout.php index 2221d5c..5fc8dad 100644 --- a/templates/layout.php +++ b/templates/layout.php @@ -99,5 +99,7 @@ + + From d1666f15ddbb712f9f2b9de0b2e9a2f122c73992 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C6=90=C9=94=C4=B1s3?= <8407711+w3spi5@users.noreply.github.com> Date: Wed, 28 Jan 2026 15:12:02 +0100 Subject: [PATCH 05/22] add icon buttons with spinner animation on click --- assets/dist/bigdump.min.js | 2 +- assets/dist/filepolling.min.js | 2 +- assets/{icons.svg => icons2.svg} | 3 +++ assets/js/bigdump.js | 33 +++++++++-------------- assets/js/filepolling.js | 46 +++++++++++++++++++++++--------- assets/src/js/bigdump.js | 33 +++++++++-------------- assets/src/js/filepolling.js | 40 ++++++++++++++++++--------- assets/src/js/history.js | 13 ++++----- assets/src/js/preview.js | 2 +- src/Services/AjaxService.php | 42 ++++++++++++++++++++++++----- templates/home.php | 35 +++++++++++++----------- templates/layout.php | 4 +-- 12 files changed, 156 insertions(+), 99 deletions(-) rename assets/{icons.svg => icons2.svg} (92%) diff --git a/assets/dist/bigdump.min.js b/assets/dist/bigdump.min.js index 98fa28c..a8ff830 100644 --- a/assets/dist/bigdump.min.js +++ b/assets/dist/bigdump.min.js @@ -1 +1 @@ -(function(){"use strict";function o(){var e=document.getElementById("loadingOverlay");e&&document.querySelectorAll("form").forEach(function(t){t.querySelector('input[name="fn"]')&&t.addEventListener("submit",function(i){e.classList.add("active");var n=t.querySelector('input[name="fn"]');if(n){var u=n.value,r=e.querySelector(".loading-subtext");r&&(r.textContent="Loading "+u)}})})}function d(){var e=document.getElementById("darkModeToggle");e&&e.addEventListener("click",function(){var t=document.documentElement,i=t.getAttribute("data-theme")||"light",n=i==="dark"?"light":"dark";t.setAttribute("data-theme",n),localStorage.setItem("bigdump-theme",n)})}function c(e,t){e.disabled=!0,e.classList.add("opacity-50","cursor-not-allowed"),e.innerHTML=''+t}function b(){document.querySelectorAll("form").forEach(function(e){var t=e.querySelector('input[name="fn"]'),i=e.querySelector('input[name="action"]'),n=e.querySelector('button[type="submit"]');n&&(t&&(!i||i.value!=="delete")&&e.addEventListener("submit",function(){c(n,"Importing...")}),i&&i.value==="delete"&&e.addEventListener("submit",function(u){c(n,"Deleting...")}))})}function a(){o(),d(),b()}document.readyState==="loading"?document.addEventListener("DOMContentLoaded",a):a()})(); +!function(){"use strict";function e(){var e,t;(e=document.getElementById("loadingOverlay"))&&document.querySelectorAll("form").forEach(function(t){t.querySelector('input[name="fn"]')&&t.addEventListener("submit",function(n){e.classList.add("active");var a=t.querySelector('input[name="fn"]');if(a){var o=a.value,r=e.querySelector(".loading-subtext");r&&(r.textContent="Loading "+o)}})}),(t=document.getElementById("darkModeToggle"))&&t.addEventListener("click",function(){var e=document.documentElement,t="dark"===(e.getAttribute("data-theme")||"light")?"light":"dark";e.setAttribute("data-theme",t),localStorage.setItem("bigdump-theme",t)}),document.querySelectorAll("form").forEach(function(e){var t=e.querySelector('button[type="submit"]');t&&t.querySelector(".btn-icon-swap")&&e.addEventListener("submit",function(){!function(e){e.disabled=!0,e.classList.add("opacity-50","cursor-not-allowed");var t=e.querySelector(".btn-icon-swap");t&&(t.innerHTML='',t.classList.add("animate-spin"))}(t)})})}"loading"===document.readyState?document.addEventListener("DOMContentLoaded",e):e()}(); \ No newline at end of file diff --git a/assets/dist/filepolling.min.js b/assets/dist/filepolling.min.js index aaca51e..7e64a07 100644 --- a/assets/dist/filepolling.min.js +++ b/assets/dist/filepolling.min.js @@ -1 +1 @@ -(function(){"use strict";var C=document.getElementById("bigdump-config");if(!C)return;var B=JSON.parse(C.dataset.dbConfigured||"false"),M=JSON.parse(C.dataset.gzipSupported||"false"),G=4e3,p=null,g=new Set,f=new Set;function O(e){switch(e){case"SQL":return"bg-blue-100 dark:bg-blue-900/50 text-blue-800 dark:text-blue-200";case"GZip":return"bg-purple-100 dark:bg-purple-900/50 text-purple-800 dark:text-purple-200";case"CSV":return"bg-green-100 dark:bg-green-900/50 text-green-800 dark:text-green-200";default:return"bg-blue-100 dark:bg-blue-900/50 text-blue-800 dark:text-blue-200"}}function Z(e){var t=e.split(".").pop().toLowerCase();return t==="sql"?"SQL":t==="gz"?"GZip":t==="csv"?"CSV":"SQL"}function R(){var e=document.getElementById("previewModal"),t=document.getElementById("historyModal");return e&&!e.classList.contains("hidden")||t&&!t.classList.contains("hidden")}function z(e){var t=document.getElementById("noFilesMessage"),r=document.getElementById("filesTableContainer");!t||!r||(e?(t.classList.remove("hidden"),r.classList.add("hidden")):(t.classList.add("hidden"),r.classList.remove("hidden")))}function T(e,t,r){var n=document.createElement("tr");n.className="hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors duration-150",n.setAttribute("data-filename",e.name),t&&(n.classList.add("bg-green-50","dark:bg-green-900/20","animate-pulse"),setTimeout(function(){n.classList.remove("bg-green-50","dark:bg-green-900/20","animate-pulse")},3e3)),r&&n.classList.add("bg-amber-50","dark:bg-amber-900/20");var u=B&&(e.type!=="GZip"||M),a="px-4 py-3 border-b border-gray-200 dark:border-gray-700 text-gray-900 dark:text-gray-100",i=document.createElement("td");i.className=a;var l=document.createElement("strong");if(l.textContent=e.name,i.appendChild(l),r){var v=document.createElement("span");v.className="ml-2 text-amber-600 dark:text-amber-400 text-xs";var x=document.createElementNS("http://www.w3.org/2000/svg","svg");x.setAttribute("class","icon w-4 h-4 fill-current inline-block animate-spin");var P=document.createElementNS("http://www.w3.org/2000/svg","use");P.setAttributeNS("http://www.w3.org/1999/xlink","href","assets/icons.svg#spinner"),x.appendChild(P),v.appendChild(x),v.appendChild(document.createTextNode(" Uploading...")),i.appendChild(v)}else if(t){var E=document.createElement("span");E.className="ml-2 px-1.5 py-0.5 bg-green-500 text-white text-xs rounded font-medium",E.textContent="NEW",i.appendChild(E)}n.appendChild(i);var y=document.createElement("td");y.className=a,y.textContent=r?"--":e.sizeFormatted,n.appendChild(y);var N=document.createElement("td");N.className=a,N.textContent=r?"--":e.date,n.appendChild(N);var F=document.createElement("td");F.className=a;var k=document.createElement("span");k.className="px-2 py-1 rounded text-xs font-medium "+O(e.type),k.textContent=e.type,F.appendChild(k),n.appendChild(F);var d=document.createElement("td");if(d.className=a+" text-center",r){var S=document.createElement("span");S.className="text-gray-400",S.textContent="--",d.appendChild(S)}else{if(u){var o=document.createElement("button");o.type="button",o.className="btn btn-icon btn-purple",o.title="Preview SQL content",o.onclick=function(){window.previewFile(e.name)};var L=document.createElementNS("http://www.w3.org/2000/svg","svg");L.setAttribute("class","icon w-4 h-4 fill-current");var A=document.createElementNS("http://www.w3.org/2000/svg","use");A.setAttributeNS("http://www.w3.org/1999/xlink","href","assets/icons.svg#eye"),L.appendChild(A),o.appendChild(L),d.appendChild(o),d.appendChild(document.createTextNode(" "));var s=document.createElement("form");s.method="post",s.action="",s.style.display="inline";var b=document.createElement("input");b.type="hidden",b.name="fn",b.value=e.name,s.appendChild(b);var w=document.createElement("button");w.type="submit",w.className="btn btn-green",w.textContent="Import",s.onsubmit=function(){return w.disabled=!0,w.classList.add("opacity-50","cursor-not-allowed"),w.innerHTML='Importing...',!0},s.appendChild(w),d.appendChild(s),d.appendChild(document.createTextNode(" "))}else if(e.type==="GZip"){var D=document.createElement("span");D.className="text-muted",D.textContent="GZip not supported",d.appendChild(D),d.appendChild(document.createTextNode(" "))}var m=document.createElement("a");m.href="?delete="+encodeURIComponent(e.name),m.className="btn btn-red",m.textContent="Delete",m.onclick=function(j){return m.classList.contains("disabled")?(j.preventDefault(),!1):confirm("Delete "+e.name+"?")?(m.classList.add("disabled","opacity-50","cursor-not-allowed","pointer-events-none"),m.innerHTML='Deleting...',!0):!1},d.appendChild(m)}return n.appendChild(d),n}function Q(e,t){var r=document.getElementById("fileTableBody");if(r){var n=new Set(t.map(function(a){return a.name})),u=document.createDocumentFragment();f.forEach(function(a){if(!e.find(function(l){return l.name===a})){var i={name:a,type:Z(a),sizeFormatted:"--",date:"--"};u.appendChild(T(i,!1,!0))}}),e.forEach(function(a){var i=n.has(a.name),l=f.has(a.name);u.appendChild(T(a,i,l))}),r.replaceChildren(u)}}function _(){document.querySelectorAll("#fileTableBody tr[data-filename]").forEach(function(e){g.add(e.getAttribute("data-filename"))})}function h(){_(),p=setInterval(c,G)}function I(){p&&(clearInterval(p),p=null)}function c(){R()||fetch("/files/list").then(function(e){return e.json()}).then(function(e){if(e.success){var t=new Set(e.files.map(function(n){return n.name})),r=e.files.filter(function(n){return!g.has(n.name)});Q(e.files,r),g=t,z(e.files.length===0)}}).catch(function(e){console.error("File polling error:",e)})}function U(e){f.add(e),c()}function V(e){f.delete(e),g.add(e),c()}document.addEventListener("visibilitychange",function(){document.hidden?I():h()}),document.readyState==="loading"?document.addEventListener("DOMContentLoaded",h):h(),window.BigDump=window.BigDump||{},window.BigDump.refreshFileList=c,window.BigDump.trackUploadStart=U,window.BigDump.trackUploadEnd=V,window.BigDump.startFilePolling=h,window.BigDump.stopFilePolling=I,window.refreshFileList=c})(); +!function(){"use strict";var e=document.getElementById("bigdump-config");if(e){var t=JSON.parse(e.dataset.dbConfigured||"false"),n=JSON.parse(e.dataset.gzipSupported||"false"),a=null,r=new Set,d=new Set;document.addEventListener("visibilitychange",function(){document.hidden?o():l()}),"loading"===document.readyState?document.addEventListener("DOMContentLoaded",l):l(),window.BigDump=window.BigDump||{},window.BigDump.refreshFileList=c,window.BigDump.trackUploadStart=function(e){d.add(e),c()},window.BigDump.trackUploadEnd=function(e){d.delete(e),r.add(e),c()},window.BigDump.startFilePolling=l,window.BigDump.stopFilePolling=o,window.refreshFileList=c}function i(e){var t=e.split(".").pop().toLowerCase();return"sql"===t?"SQL":"gz"===t?"GZip":"csv"===t?"CSV":"SQL"}function s(e,a,r){var d=document.createElement("tr");d.className="hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors duration-150",d.setAttribute("data-filename",e.name),a&&(d.classList.add("bg-green-50","dark:bg-green-900/20","animate-pulse"),setTimeout(function(){d.classList.remove("bg-green-50","dark:bg-green-900/20","animate-pulse")},3e3)),r&&d.classList.add("bg-amber-50","dark:bg-amber-900/20");var i=t&&("GZip"!==e.type||n),s="px-4 py-3 border-b border-gray-200 dark:border-gray-700 text-gray-900 dark:text-gray-100",l=document.createElement("td");l.className=s;var o=document.createElement("strong");if(o.textContent=e.name,l.appendChild(o),r){var c=document.createElement("span");c.className="ml-2 text-amber-600 dark:text-amber-400 text-xs";var p=document.createElementNS("http://www.w3.org/2000/svg","svg");p.setAttribute("class","icon w-4 h-4 fill-current inline-block animate-spin");var m=document.createElementNS("http://www.w3.org/2000/svg","use");m.setAttributeNS("http://www.w3.org/1999/xlink","href","assets/icons2.svg#spinner"),p.appendChild(m),c.appendChild(p),c.appendChild(document.createTextNode(" Uploading...")),l.appendChild(c)}else if(a){var u=document.createElement("span");u.className="ml-2 px-1.5 py-0.5 bg-green-500 text-white text-xs rounded font-medium",u.textContent="NEW",l.appendChild(u)}d.appendChild(l);var g=document.createElement("td");g.className=s,g.textContent=r?"--":e.sizeFormatted,d.appendChild(g);var w=document.createElement("td");w.className=s,w.textContent=r?"--":e.date,d.appendChild(w);var h=document.createElement("td");h.className=s;var f=document.createElement("span");f.className="px-2 py-1 rounded text-xs font-medium "+function(e){switch(e){case"SQL":default:return"bg-blue-100 dark:bg-blue-900/50 text-blue-800 dark:text-blue-200";case"GZip":return"bg-purple-100 dark:bg-purple-900/50 text-purple-800 dark:text-purple-200";case"CSV":return"bg-green-100 dark:bg-green-900/50 text-green-800 dark:text-green-200"}}(e.type),f.textContent=e.type,h.appendChild(f),d.appendChild(h);var v=document.createElement("td");if(v.className=s+" text-center",r){var b=document.createElement("span");b.className="text-gray-400",b.textContent="--",v.appendChild(b)}else{if(i){var C=document.createElement("button");C.type="button",C.className="btn btn-icon btn-purple",C.title="Preview SQL content",C.onclick=function(){window.previewFile(e.name)};var x=document.createElementNS("http://www.w3.org/2000/svg","svg");x.setAttribute("class","icon w-4 h-4 fill-current");var E=document.createElementNS("http://www.w3.org/2000/svg","use");E.setAttributeNS("http://www.w3.org/1999/xlink","href","assets/icons2.svg#eye"),x.appendChild(E),C.appendChild(x),v.appendChild(C),v.appendChild(document.createTextNode(" "));var N=document.createElement("form");N.method="post",N.action="",N.style.cssText="display:inline; vertical-align:middle";var y=document.createElement("input");y.type="hidden",y.name="fn",y.value=e.name,N.appendChild(y);var S=document.createElement("button");S.type="submit",S.className="btn btn-green";var k=document.createElementNS("http://www.w3.org/2000/svg","svg");k.setAttribute("class","icon w-4 h-4 fill-current mr-1");var L=document.createElementNS("http://www.w3.org/2000/svg","use");L.setAttributeNS("http://www.w3.org/1999/xlink","href","assets/icons2.svg#download"),k.appendChild(L),S.appendChild(k),S.appendChild(document.createTextNode("Import")),N.onsubmit=function(){return S.disabled=!0,S.classList.add("opacity-50","cursor-not-allowed"),k.classList.add("animate-spin"),L.setAttributeNS("http://www.w3.org/1999/xlink","href","assets/icons2.svg#spinner"),!0},N.appendChild(S),v.appendChild(N),v.appendChild(document.createTextNode(" "))}else if("GZip"===e.type){var B=document.createElement("span");B.className="text-muted",B.textContent="GZip not supported",v.appendChild(B),v.appendChild(document.createTextNode(" "))}var A=document.createElement("a");A.href="?delete="+encodeURIComponent(e.name),A.className="btn btn-icon btn-red",A.title="Delete file";var D=document.createElementNS("http://www.w3.org/2000/svg","svg");D.setAttribute("class","icon w-4 h-4 fill-current");var F=document.createElementNS("http://www.w3.org/2000/svg","use");F.setAttributeNS("http://www.w3.org/1999/xlink","href","assets/icons2.svg#trash"),D.appendChild(F),A.appendChild(D),A.onclick=function(t){return A.classList.contains("disabled")?(t.preventDefault(),!1):!!confirm("Delete "+e.name+"?")&&(A.classList.add("disabled","opacity-50","cursor-not-allowed","pointer-events-none"),D.classList.add("animate-spin"),F.setAttributeNS("http://www.w3.org/1999/xlink","href","assets/icons2.svg#spinner"),!0)},v.appendChild(A)}return d.appendChild(v),d}function l(){document.querySelectorAll("#fileTableBody tr[data-filename]").forEach(function(e){r.add(e.getAttribute("data-filename"))}),a=setInterval(c,4e3)}function o(){a&&(clearInterval(a),a=null)}function c(){var e,t;(e=document.getElementById("previewModal"),t=document.getElementById("historyModal"),e&&!e.classList.contains("hidden")||t&&!t.classList.contains("hidden"))||fetch("/files/list").then(function(e){return e.json()}).then(function(e){if(e.success){var t,n,a,l=new Set(e.files.map(function(e){return e.name})),o=e.files.filter(function(e){return!r.has(e.name)});!function(e,t){var n=document.getElementById("fileTableBody");if(n){var a=new Set(t.map(function(e){return e.name})),r=document.createDocumentFragment();d.forEach(function(t){if(!e.find(function(e){return e.name===t})){var n={name:t,type:i(t),sizeFormatted:"--",date:"--"};r.appendChild(s(n,!1,!0))}}),e.forEach(function(e){var t=a.has(e.name),n=d.has(e.name);r.appendChild(s(e,t,n))}),n.replaceChildren(r)}}(e.files,o),r=l,t=0===e.files.length,n=document.getElementById("noFilesMessage"),a=document.getElementById("filesTableContainer"),n&&a&&(t?(n.classList.remove("hidden"),a.classList.add("hidden")):(n.classList.add("hidden"),a.classList.remove("hidden")))}}).catch(function(e){console.error("File polling error:",e)})}}(); \ No newline at end of file diff --git a/assets/icons.svg b/assets/icons2.svg similarity index 92% rename from assets/icons.svg rename to assets/icons2.svg index 1555377..1809e4f 100644 --- a/assets/icons.svg +++ b/assets/icons2.svg @@ -51,4 +51,7 @@ + + + diff --git a/assets/js/bigdump.js b/assets/js/bigdump.js index f556a4c..f40f9dc 100644 --- a/assets/js/bigdump.js +++ b/assets/js/bigdump.js @@ -35,14 +35,18 @@ } /** - * Disable a button with loading state to prevent double-click + * Disable a button and swap its icon to a spinner * @param {HTMLElement} button - The button element - * @param {string} loadingText - Text to show while loading */ - function disableButtonWithLoading(button, loadingText) { + function disableButtonWithSpinner(button) { button.disabled = true; button.classList.add('opacity-50', 'cursor-not-allowed'); - button.innerHTML = '' + loadingText; + // Find the swappable icon and change it to spinner + var iconSwap = button.querySelector('.btn-icon-swap'); + if (iconSwap) { + iconSwap.innerHTML = ''; + iconSwap.classList.add('animate-spin'); + } } /** @@ -50,27 +54,16 @@ * Prevents double-click by disabling buttons after first click */ function initButtonDisable() { - // Handle import forms (forms with 'fn' input but no 'action' input) + // Handle forms with swappable icons (new style) document.querySelectorAll('form').forEach(function(form) { - var fnInput = form.querySelector('input[name="fn"]'); - var actionInput = form.querySelector('input[name="action"]'); var submitBtn = form.querySelector('button[type="submit"]'); - if (!submitBtn) return; - // Import forms: have 'fn' but no 'action=delete' - if (fnInput && (!actionInput || actionInput.value !== 'delete')) { + // Only add handler if button has swappable icon + var iconSwap = submitBtn.querySelector('.btn-icon-swap'); + if (iconSwap) { form.addEventListener('submit', function() { - disableButtonWithLoading(submitBtn, 'Importing...'); - }); - } - - // Delete forms: have 'action=delete' - if (actionInput && actionInput.value === 'delete') { - form.addEventListener('submit', function(e) { - // The confirm is already handled by onsubmit in HTML - // If we reach here, user confirmed, so disable button - disableButtonWithLoading(submitBtn, 'Deleting...'); + disableButtonWithSpinner(submitBtn); }); } }); diff --git a/assets/js/filepolling.js b/assets/js/filepolling.js index b8587da..82500f9 100644 --- a/assets/js/filepolling.js +++ b/assets/js/filepolling.js @@ -126,8 +126,11 @@ if (isUploading) { var uploadSpan = document.createElement('span'); uploadSpan.className = 'ml-2 text-amber-600 dark:text-amber-400 text-xs'; - var spinner = document.createElement('i'); - spinner.className = 'fa-solid fa-spinner fa-spin'; + var spinner = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); + spinner.setAttribute('class', 'icon w-4 h-4 fill-current inline-block animate-spin'); + var spinnerUse = document.createElementNS('http://www.w3.org/2000/svg', 'use'); + spinnerUse.setAttributeNS('http://www.w3.org/1999/xlink', 'href', 'assets/icons2.svg#spinner'); + spinner.appendChild(spinnerUse); uploadSpan.appendChild(spinner); uploadSpan.appendChild(document.createTextNode(' Uploading...')); td1.appendChild(uploadSpan); @@ -177,8 +180,11 @@ previewBtn.className = 'btn btn-icon btn-purple'; previewBtn.title = 'Preview SQL content'; previewBtn.onclick = function() { window.previewFile(file.name); }; - var eyeIcon = document.createElement('i'); - eyeIcon.className = 'fa-solid fa-eye'; + var eyeIcon = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); + eyeIcon.setAttribute('class', 'icon w-4 h-4 fill-current'); + var useEl = document.createElementNS('http://www.w3.org/2000/svg', 'use'); + useEl.setAttributeNS('http://www.w3.org/1999/xlink', 'href', 'assets/icons2.svg#eye'); + eyeIcon.appendChild(useEl); previewBtn.appendChild(eyeIcon); td5.appendChild(previewBtn); td5.appendChild(document.createTextNode(' ')); @@ -187,7 +193,7 @@ var form = document.createElement('form'); form.method = 'post'; form.action = ''; - form.style.display = 'inline'; + form.style.cssText = 'display:inline; vertical-align:middle'; var hiddenInput = document.createElement('input'); hiddenInput.type = 'hidden'; hiddenInput.name = 'fn'; @@ -196,12 +202,21 @@ var importBtn = document.createElement('button'); importBtn.type = 'submit'; importBtn.className = 'btn btn-green'; - importBtn.textContent = 'Import'; - // Disable button on click to prevent double submission + // Add database icon before text + var dbIcon = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); + dbIcon.setAttribute('class', 'icon w-4 h-4 fill-current mr-1'); + var dbUse = document.createElementNS('http://www.w3.org/2000/svg', 'use'); + dbUse.setAttributeNS('http://www.w3.org/1999/xlink', 'href', 'assets/icons2.svg#download'); + dbIcon.appendChild(dbUse); + importBtn.appendChild(dbIcon); + importBtn.appendChild(document.createTextNode('Import')); + // Disable button on click and swap icon to spinner form.onsubmit = function() { importBtn.disabled = true; importBtn.classList.add('opacity-50', 'cursor-not-allowed'); - importBtn.innerHTML = 'Importing...'; + // Swap icon to spinner + dbIcon.classList.add('animate-spin'); + dbUse.setAttributeNS('http://www.w3.org/1999/xlink', 'href', 'assets/icons2.svg#spinner'); return true; // Allow form submission }; form.appendChild(importBtn); @@ -218,8 +233,14 @@ // Delete button var deleteLink = document.createElement('a'); deleteLink.href = '?delete=' + encodeURIComponent(file.name); - deleteLink.className = 'btn btn-red'; - deleteLink.textContent = 'Delete'; + deleteLink.className = 'btn btn-icon btn-red'; + deleteLink.title = 'Delete file'; + var trashIcon = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); + trashIcon.setAttribute('class', 'icon w-4 h-4 fill-current'); + var trashUse = document.createElementNS('http://www.w3.org/2000/svg', 'use'); + trashUse.setAttributeNS('http://www.w3.org/1999/xlink', 'href', 'assets/icons2.svg#trash'); + trashIcon.appendChild(trashUse); + deleteLink.appendChild(trashIcon); deleteLink.onclick = function(e) { if (deleteLink.classList.contains('disabled')) { e.preventDefault(); @@ -228,9 +249,10 @@ if (!confirm('Delete ' + file.name + '?')) { return false; } - // Disable button on click to prevent double action + // Disable button and swap icon to spinner deleteLink.classList.add('disabled', 'opacity-50', 'cursor-not-allowed', 'pointer-events-none'); - deleteLink.innerHTML = 'Deleting...'; + trashIcon.classList.add('animate-spin'); + trashUse.setAttributeNS('http://www.w3.org/1999/xlink', 'href', 'assets/icons2.svg#spinner'); return true; }; td5.appendChild(deleteLink); diff --git a/assets/src/js/bigdump.js b/assets/src/js/bigdump.js index f556a4c..f40f9dc 100644 --- a/assets/src/js/bigdump.js +++ b/assets/src/js/bigdump.js @@ -35,14 +35,18 @@ } /** - * Disable a button with loading state to prevent double-click + * Disable a button and swap its icon to a spinner * @param {HTMLElement} button - The button element - * @param {string} loadingText - Text to show while loading */ - function disableButtonWithLoading(button, loadingText) { + function disableButtonWithSpinner(button) { button.disabled = true; button.classList.add('opacity-50', 'cursor-not-allowed'); - button.innerHTML = '' + loadingText; + // Find the swappable icon and change it to spinner + var iconSwap = button.querySelector('.btn-icon-swap'); + if (iconSwap) { + iconSwap.innerHTML = ''; + iconSwap.classList.add('animate-spin'); + } } /** @@ -50,27 +54,16 @@ * Prevents double-click by disabling buttons after first click */ function initButtonDisable() { - // Handle import forms (forms with 'fn' input but no 'action' input) + // Handle forms with swappable icons (new style) document.querySelectorAll('form').forEach(function(form) { - var fnInput = form.querySelector('input[name="fn"]'); - var actionInput = form.querySelector('input[name="action"]'); var submitBtn = form.querySelector('button[type="submit"]'); - if (!submitBtn) return; - // Import forms: have 'fn' but no 'action=delete' - if (fnInput && (!actionInput || actionInput.value !== 'delete')) { + // Only add handler if button has swappable icon + var iconSwap = submitBtn.querySelector('.btn-icon-swap'); + if (iconSwap) { form.addEventListener('submit', function() { - disableButtonWithLoading(submitBtn, 'Importing...'); - }); - } - - // Delete forms: have 'action=delete' - if (actionInput && actionInput.value === 'delete') { - form.addEventListener('submit', function(e) { - // The confirm is already handled by onsubmit in HTML - // If we reach here, user confirmed, so disable button - disableButtonWithLoading(submitBtn, 'Deleting...'); + disableButtonWithSpinner(submitBtn); }); } }); diff --git a/assets/src/js/filepolling.js b/assets/src/js/filepolling.js index 77a1438..82500f9 100644 --- a/assets/src/js/filepolling.js +++ b/assets/src/js/filepolling.js @@ -129,7 +129,7 @@ var spinner = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); spinner.setAttribute('class', 'icon w-4 h-4 fill-current inline-block animate-spin'); var spinnerUse = document.createElementNS('http://www.w3.org/2000/svg', 'use'); - spinnerUse.setAttributeNS('http://www.w3.org/1999/xlink', 'href', 'assets/icons.svg#spinner'); + spinnerUse.setAttributeNS('http://www.w3.org/1999/xlink', 'href', 'assets/icons2.svg#spinner'); spinner.appendChild(spinnerUse); uploadSpan.appendChild(spinner); uploadSpan.appendChild(document.createTextNode(' Uploading...')); @@ -182,9 +182,9 @@ previewBtn.onclick = function() { window.previewFile(file.name); }; var eyeIcon = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); eyeIcon.setAttribute('class', 'icon w-4 h-4 fill-current'); - var eyeUse = document.createElementNS('http://www.w3.org/2000/svg', 'use'); - eyeUse.setAttributeNS('http://www.w3.org/1999/xlink', 'href', 'assets/icons.svg#eye'); - eyeIcon.appendChild(eyeUse); + var useEl = document.createElementNS('http://www.w3.org/2000/svg', 'use'); + useEl.setAttributeNS('http://www.w3.org/1999/xlink', 'href', 'assets/icons2.svg#eye'); + eyeIcon.appendChild(useEl); previewBtn.appendChild(eyeIcon); td5.appendChild(previewBtn); td5.appendChild(document.createTextNode(' ')); @@ -193,7 +193,7 @@ var form = document.createElement('form'); form.method = 'post'; form.action = ''; - form.style.display = 'inline'; + form.style.cssText = 'display:inline; vertical-align:middle'; var hiddenInput = document.createElement('input'); hiddenInput.type = 'hidden'; hiddenInput.name = 'fn'; @@ -202,12 +202,21 @@ var importBtn = document.createElement('button'); importBtn.type = 'submit'; importBtn.className = 'btn btn-green'; - importBtn.textContent = 'Import'; - // Disable button on click to prevent double submission + // Add database icon before text + var dbIcon = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); + dbIcon.setAttribute('class', 'icon w-4 h-4 fill-current mr-1'); + var dbUse = document.createElementNS('http://www.w3.org/2000/svg', 'use'); + dbUse.setAttributeNS('http://www.w3.org/1999/xlink', 'href', 'assets/icons2.svg#download'); + dbIcon.appendChild(dbUse); + importBtn.appendChild(dbIcon); + importBtn.appendChild(document.createTextNode('Import')); + // Disable button on click and swap icon to spinner form.onsubmit = function() { importBtn.disabled = true; importBtn.classList.add('opacity-50', 'cursor-not-allowed'); - importBtn.innerHTML = 'Importing...'; + // Swap icon to spinner + dbIcon.classList.add('animate-spin'); + dbUse.setAttributeNS('http://www.w3.org/1999/xlink', 'href', 'assets/icons2.svg#spinner'); return true; // Allow form submission }; form.appendChild(importBtn); @@ -224,8 +233,14 @@ // Delete button var deleteLink = document.createElement('a'); deleteLink.href = '?delete=' + encodeURIComponent(file.name); - deleteLink.className = 'btn btn-red'; - deleteLink.textContent = 'Delete'; + deleteLink.className = 'btn btn-icon btn-red'; + deleteLink.title = 'Delete file'; + var trashIcon = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); + trashIcon.setAttribute('class', 'icon w-4 h-4 fill-current'); + var trashUse = document.createElementNS('http://www.w3.org/2000/svg', 'use'); + trashUse.setAttributeNS('http://www.w3.org/1999/xlink', 'href', 'assets/icons2.svg#trash'); + trashIcon.appendChild(trashUse); + deleteLink.appendChild(trashIcon); deleteLink.onclick = function(e) { if (deleteLink.classList.contains('disabled')) { e.preventDefault(); @@ -234,9 +249,10 @@ if (!confirm('Delete ' + file.name + '?')) { return false; } - // Disable button on click to prevent double action + // Disable button and swap icon to spinner deleteLink.classList.add('disabled', 'opacity-50', 'cursor-not-allowed', 'pointer-events-none'); - deleteLink.innerHTML = 'Deleting...'; + trashIcon.classList.add('animate-spin'); + trashUse.setAttributeNS('http://www.w3.org/1999/xlink', 'href', 'assets/icons2.svg#spinner'); return true; }; td5.appendChild(deleteLink); diff --git a/assets/src/js/history.js b/assets/src/js/history.js index 8ba40ba..10fc725 100644 --- a/assets/src/js/history.js +++ b/assets/src/js/history.js @@ -59,14 +59,11 @@ // Status icon var tdStatus = document.createElement('td'); tdStatus.className = 'px-4 py-3 border-b border-gray-200 dark:border-gray-700 text-center'; - var iconName = entry.success ? 'circle-check' : 'circle-xmark'; - var iconColor = entry.success ? 'text-green-500' : 'text-red-500'; - var svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); - svg.setAttribute('class', 'icon w-5 h-5 fill-current inline-block ' + iconColor); - var use = document.createElementNS('http://www.w3.org/2000/svg', 'use'); - use.setAttributeNS('http://www.w3.org/1999/xlink', 'href', 'assets/icons.svg#' + iconName); - svg.appendChild(use); - tdStatus.appendChild(svg); + var icon = document.createElement('i'); + icon.className = entry.success + ? 'fa-solid fa-circle-check text-green-500' + : 'fa-solid fa-circle-xmark text-red-500'; + tdStatus.appendChild(icon); // Filename var tdFile = document.createElement('td'); diff --git a/assets/src/js/preview.js b/assets/src/js/preview.js index d7660a2..3ff551b 100644 --- a/assets/src/js/preview.js +++ b/assets/src/js/preview.js @@ -93,7 +93,7 @@ document.getElementById('previewModalSubtitle').textContent = data.fileSizeFormatted + (data.isGzip ? ' (GZip compressed)' : ''); document.getElementById('previewFileSize').textContent = data.fileSizeFormatted; document.getElementById('previewFileType').textContent = data.isGzip ? 'GZip' : 'SQL'; - document.getElementById('previewTotalLines').textContent = data.totalLines.toLocaleString(); + document.getElementById('previewLinesCount').textContent = data.linesPreview; document.getElementById('previewQueriesCount').textContent = data.queriesPreview; document.getElementById('tabQueriesCount').textContent = data.queriesPreview; diff --git a/src/Services/AjaxService.php b/src/Services/AjaxService.php index 0d32d28..f9f1449 100644 --- a/src/Services/AjaxService.php +++ b/src/Services/AjaxService.php @@ -861,10 +861,12 @@ function createConnection() { if (source) source.close(); }); - // Handle Stop Import button - close SSE before navigation + // Handle Stop Import button - close SSE and show stopped message document.addEventListener('click', function(e) { - var stopLink = e.target.closest('a[href*="stop_import"]'); + var stopLink = e.target.closest('a[href*="import/stop"]'); if (stopLink) { + e.preventDefault(); + // Close SSE connection immediately intentionalClose = true; stopElapsedTimer(); @@ -874,11 +876,37 @@ function createConnection() { source = null; console.log('SSE: Closed by user (Stop Import)'); } - // Small delay to ensure connection is closed before navigation - e.preventDefault(); - setTimeout(function() { - window.location.href = stopLink.href; - }, 100); + + // Stop progress bar animation + var progressBar = document.querySelector('.progress-bar'); + if (progressBar) { + progressBar.style.animation = 'none'; + progressBar.style.backgroundImage = 'none'; + } + + // Get elapsed time for display + var elapsedEl = document.getElementById('elapsedTime'); + var elapsedTime = elapsedEl ? elapsedEl.textContent : '00:00:00'; + + // Show stopped message in page + var stoppedHtml = '
        ' + + '
        ⏹️
        ' + + '

        Import Stopped

        ' + + '

        The import was manually stopped after ' + elapsedTime + '.

        ' + + '

        Progress has been saved. You can resume later by selecting the same file.

        ' + + '' + + '🏠 Return to Home' + + '' + + '
        '; + + // Replace main content + var mainContent = document.querySelector('main'); + if (mainContent) { + mainContent.innerHTML = stoppedHtml; + } + + // Also call the server to clear session (fire and forget) + fetch(stopLink.href, { method: 'GET' }).catch(function() {}); } }); })(); diff --git a/templates/home.php b/templates/home.php index dca55b8..3d5e6a6 100644 --- a/templates/home.php +++ b/templates/home.php @@ -88,7 +88,7 @@

        Available Dump Files

        @@ -147,21 +147,26 @@ onclick="previewFile('escapeJs($file['name']) ?>')" class="btn btn-icon btn-purple" title="Preview SQL content"> - + -
        + - +
        not supported -
        + - +
        @@ -277,7 +282,7 @@ class="hidden"
    @@ -294,7 +299,7 @@ class="hidden" @@ -324,10 +329,10 @@ class="hidden"
    @@ -347,7 +352,7 @@ class="hidden"
    @@ -366,12 +371,12 @@ class="hidden" @@ -429,7 +434,7 @@ class="hidden" From ab821f803658ea6f9593e7e1d5acbcdef69846cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C6=90=C9=94=C4=B1s3?= <8407711+w3spi5@users.noreply.github.com> Date: Wed, 28 Jan 2026 15:22:51 +0100 Subject: [PATCH 06/22] solved resume issue --- src/Services/AjaxService.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Services/AjaxService.php b/src/Services/AjaxService.php index f9f1449..2f7f4d1 100644 --- a/src/Services/AjaxService.php +++ b/src/Services/AjaxService.php @@ -892,8 +892,7 @@ function createConnection() { var stoppedHtml = '
    ' + '
    ⏹️
    ' + '

    Import Stopped

    ' + - '

    The import was manually stopped after ' + elapsedTime + '.

    ' + - '

    Progress has been saved. You can resume later by selecting the same file.

    ' + + '

    The import was manually stopped after ' + elapsedTime + '.

    ' + '' + '🏠 Return to Home' + '' + From 4e4b9bc2ac69558ec3660a403fa797e6871f6354 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C6=90=C9=94=C4=B1s3?= <8407711+w3spi5@users.noreply.github.com> Date: Thu, 29 Jan 2026 12:09:12 +0100 Subject: [PATCH 07/22] Fix dark mode readability for SSE import warning details --- assets/dist/app.min.css | 2 +- assets/src/css/tailwind.css | 114 +++++++++++++++++++++++++++++++++++ src/Services/AjaxService.php | 20 +++--- 3 files changed, 125 insertions(+), 11 deletions(-) diff --git a/assets/dist/app.min.css b/assets/dist/app.min.css index 3f1d2f3..b9bd760 100644 --- a/assets/dist/app.min.css +++ b/assets/dist/app.min.css @@ -1,2 +1,2 @@ /*! tailwindcss v4.1.18 | MIT License | https://tailwindcss.com */ -@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0;--tw-space-y-reverse:0;--tw-border-style:solid;--tw-gradient-position:initial;--tw-gradient-from:#0000;--tw-gradient-via:#0000;--tw-gradient-to:#0000;--tw-gradient-stops:initial;--tw-gradient-via-stops:initial;--tw-gradient-from-position:0%;--tw-gradient-via-position:50%;--tw-gradient-to-position:100%;--tw-font-weight:initial;--tw-tracking:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-duration:initial;--tw-scale-x:1;--tw-scale-y:1;--tw-scale-z:1}}}@layer theme{:root,:host{--font-sans:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--font-mono:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;--color-red-50:oklch(97.1% .013 17.38);--color-red-100:oklch(93.6% .032 17.717);--color-red-200:oklch(88.5% .062 18.334);--color-red-300:oklch(80.8% .114 19.571);--color-red-400:oklch(70.4% .191 22.216);--color-red-500:oklch(63.7% .237 25.331);--color-red-600:oklch(57.7% .245 27.325);--color-red-700:oklch(50.5% .213 27.518);--color-red-800:oklch(44.4% .177 26.899);--color-red-900:oklch(39.6% .141 25.723);--color-amber-50:oklch(98.7% .022 95.277);--color-amber-100:oklch(96.2% .059 95.617);--color-amber-200:oklch(92.4% .12 95.746);--color-amber-300:oklch(87.9% .169 91.605);--color-amber-400:oklch(82.8% .189 84.429);--color-amber-500:oklch(76.9% .188 70.08);--color-amber-600:oklch(66.6% .179 58.318);--color-amber-700:oklch(55.5% .163 48.998);--color-amber-800:oklch(47.3% .137 46.201);--color-amber-900:oklch(41.4% .112 45.904);--color-green-50:oklch(98.2% .018 155.826);--color-green-100:oklch(96.2% .044 156.743);--color-green-200:oklch(92.5% .084 155.995);--color-green-300:oklch(87.1% .15 154.449);--color-green-400:oklch(79.2% .209 151.711);--color-green-500:oklch(72.3% .219 149.579);--color-green-600:oklch(62.7% .194 149.214);--color-green-700:oklch(52.7% .154 150.069);--color-green-800:oklch(44.8% .119 151.328);--color-green-900:oklch(39.3% .095 152.535);--color-cyan-100:oklch(95.6% .045 203.388);--color-cyan-200:oklch(91.7% .08 205.041);--color-cyan-500:oklch(71.5% .143 215.221);--color-cyan-600:oklch(60.9% .126 221.723);--color-blue-50:oklch(97% .014 254.604);--color-blue-100:oklch(93.2% .032 255.585);--color-blue-200:oklch(88.2% .059 254.128);--color-blue-300:oklch(80.9% .105 251.813);--color-blue-400:oklch(70.7% .165 254.624);--color-blue-500:oklch(62.3% .214 259.815);--color-blue-600:oklch(54.6% .245 262.881);--color-blue-700:oklch(48.8% .243 264.376);--color-blue-800:oklch(42.4% .199 265.638);--color-blue-900:oklch(37.9% .146 265.522);--color-indigo-500:oklch(58.5% .233 277.117);--color-purple-100:oklch(94.6% .033 307.174);--color-purple-200:oklch(90.2% .063 306.703);--color-purple-800:oklch(43.8% .218 303.724);--color-purple-900:oklch(38.1% .176 304.987);--color-rose-900:oklch(41% .159 10.272);--color-gray-50:oklch(98.5% .002 247.839);--color-gray-100:oklch(96.7% .003 264.542);--color-gray-200:oklch(92.8% .006 264.531);--color-gray-300:oklch(87.2% .01 258.338);--color-gray-400:oklch(70.7% .022 261.325);--color-gray-500:oklch(55.1% .027 264.364);--color-gray-600:oklch(44.6% .03 256.802);--color-gray-700:oklch(37.3% .034 259.733);--color-gray-800:oklch(27.8% .033 256.848);--color-gray-900:oklch(21% .034 264.665);--color-black:#000;--color-white:#fff;--spacing:.25rem;--text-xs:.75rem;--text-xs--line-height:calc(1/.75);--text-sm:.875rem;--text-sm--line-height:calc(1.25/.875);--text-lg:1.125rem;--text-lg--line-height:calc(1.75/1.125);--text-xl:1.25rem;--text-xl--line-height:calc(1.75/1.25);--text-2xl:1.5rem;--text-2xl--line-height:calc(2/1.5);--font-weight-medium:500;--font-weight-semibold:600;--font-weight-bold:700;--tracking-wide:.025em;--tracking-wider:.05em;--radius-md:.375rem;--radius-lg:.5rem;--radius-xl:.75rem;--animate-spin:spin 1s linear infinite;--animate-pulse:pulse 2s cubic-bezier(.4,0,.6,1)infinite;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4,0,.2,1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono);--animate-progress-stripe:progress-stripe 1s linear infinite}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab, red, red)){::placeholder{color:color-mix(in oklab,currentcolor 50%,transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}}@layer components;@layer utilities{.visible{visibility:visible}.absolute{position:absolute}.fixed{position:fixed}.relative{position:relative}.static{position:static}.sticky{position:sticky}.inset-0{inset:calc(var(--spacing)*0)}.top-0{top:calc(var(--spacing)*0)}.z-50{z-index:50}.container{width:100%}@media (min-width:40rem){.container{max-width:40rem}}@media (min-width:48rem){.container{max-width:48rem}}@media (min-width:64rem){.container{max-width:64rem}}@media (min-width:80rem){.container{max-width:80rem}}@media (min-width:96rem){.container{max-width:96rem}}.mx-2{margin-inline:calc(var(--spacing)*2)}.mx-auto{margin-inline:auto}.my-6{margin-block:calc(var(--spacing)*6)}.mt-1{margin-top:calc(var(--spacing)*1)}.mt-2{margin-top:calc(var(--spacing)*2)}.mt-3{margin-top:calc(var(--spacing)*3)}.mt-4{margin-top:calc(var(--spacing)*4)}.mr-1{margin-right:calc(var(--spacing)*1)}.mr-2{margin-right:calc(var(--spacing)*2)}.mb-2{margin-bottom:calc(var(--spacing)*2)}.mb-3{margin-bottom:calc(var(--spacing)*3)}.mb-4{margin-bottom:calc(var(--spacing)*4)}.mb-6{margin-bottom:calc(var(--spacing)*6)}.ml-1{margin-left:calc(var(--spacing)*1)}.ml-2{margin-left:calc(var(--spacing)*2)}.ml-4{margin-left:calc(var(--spacing)*4)}.block{display:block}.contents{display:contents}.flex{display:flex}.grid{display:grid}.hidden{display:none}.inline{display:inline}.inline-block{display:inline-block}.inline-flex{display:inline-flex}.table{display:table}.h-2\.5{height:calc(var(--spacing)*2.5)}.h-4{height:calc(var(--spacing)*4)}.h-5{height:calc(var(--spacing)*5)}.h-6{height:calc(var(--spacing)*6)}.h-8{height:calc(var(--spacing)*8)}.h-10{height:calc(var(--spacing)*10)}.h-12{height:calc(var(--spacing)*12)}.h-full{height:100%}.min-h-screen{min-height:100vh}.w-4{width:calc(var(--spacing)*4)}.w-5{width:calc(var(--spacing)*5)}.w-6{width:calc(var(--spacing)*6)}.w-8{width:calc(var(--spacing)*8)}.w-10{width:calc(var(--spacing)*10)}.w-12{width:calc(var(--spacing)*12)}.w-full{width:100%}.min-w-\[44px\]{min-width:44px}.flex-1{flex:1}.flex-shrink-0{flex-shrink:0}.-translate-y-1{--tw-translate-y:calc(var(--spacing)*-1);translate:var(--tw-translate-x)var(--tw-translate-y)}.animate-progress-stripe{animation:var(--animate-progress-stripe)}.animate-pulse{animation:var(--animate-pulse)}.animate-spin{animation:var(--animate-spin)}.cursor-help{cursor:help}.cursor-pointer{cursor:pointer}.list-inside{list-style-position:inside}.list-decimal{list-style-type:decimal}.list-disc{list-style-type:disc}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-center{align-items:center}.items-start{align-items:flex-start}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.gap-1{gap:calc(var(--spacing)*1)}.gap-2{gap:calc(var(--spacing)*2)}.gap-3{gap:calc(var(--spacing)*3)}.gap-4{gap:calc(var(--spacing)*4)}:where(.space-y-1>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*1)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*1)*calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-2>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*2)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*2)*calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-3>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*3)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*3)*calc(1 - var(--tw-space-y-reverse)))}.truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.overflow-auto{overflow:auto}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.rounded{border-radius:.25rem}.rounded-full{border-radius:3.40282e38px}.rounded-lg{border-radius:var(--radius-lg)}.rounded-md{border-radius:var(--radius-md)}.rounded-xl{border-radius:var(--radius-xl)}.border{border-style:var(--tw-border-style);border-width:1px}.border-2{border-style:var(--tw-border-style);border-width:2px}.border-4{border-style:var(--tw-border-style);border-width:4px}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.border-b-2{border-bottom-style:var(--tw-border-style);border-bottom-width:2px}.border-blue-200{border-color:var(--color-blue-200)}.border-blue-500{border-color:var(--color-blue-500)}.border-gray-200{border-color:var(--color-gray-200)}.border-red-200{border-color:var(--color-red-200)}.border-transparent{border-color:#0000}.border-white\/30{border-color:#ffffff4d}@supports (color:color-mix(in lab, red, red)){.border-white\/30{border-color:color-mix(in oklab,var(--color-white)30%,transparent)}}.border-t-blue-600{border-top-color:var(--color-blue-600)}.bg-amber-50{background-color:var(--color-amber-50)}.bg-amber-100{background-color:var(--color-amber-100)}.bg-amber-500{background-color:var(--color-amber-500)}.bg-black\/50{background-color:#00000080}@supports (color:color-mix(in lab, red, red)){.bg-black\/50{background-color:color-mix(in oklab,var(--color-black)50%,transparent)}}.bg-blue-50{background-color:var(--color-blue-50)}.bg-blue-100{background-color:var(--color-blue-100)}.bg-blue-600{background-color:var(--color-blue-600)}.bg-cyan-500{background-color:var(--color-cyan-500)}.bg-gray-50{background-color:var(--color-gray-50)}.bg-gray-100{background-color:var(--color-gray-100)}.bg-gray-200{background-color:var(--color-gray-200)}.bg-gray-900{background-color:var(--color-gray-900)}.bg-green-50{background-color:var(--color-green-50)}.bg-green-100{background-color:var(--color-green-100)}.bg-green-500{background-color:var(--color-green-500)}.bg-purple-100{background-color:var(--color-purple-100)}.bg-red-50{background-color:var(--color-red-50)}.bg-red-100{background-color:var(--color-red-100)}.bg-red-200{background-color:var(--color-red-200)}.bg-white{background-color:var(--color-white)}.bg-white\/15{background-color:#ffffff26}@supports (color:color-mix(in lab, red, red)){.bg-white\/15{background-color:color-mix(in oklab,var(--color-white)15%,transparent)}}.bg-white\/20{background-color:#fff3}@supports (color:color-mix(in lab, red, red)){.bg-white\/20{background-color:color-mix(in oklab,var(--color-white)20%,transparent)}}.bg-gradient-to-r{--tw-gradient-position:to right in oklab;background-image:linear-gradient(var(--tw-gradient-stops))}.from-blue-500{--tw-gradient-from:var(--color-blue-500);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-cyan-600{--tw-gradient-from:var(--color-cyan-600);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.to-blue-600{--tw-gradient-to:var(--color-blue-600);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.to-rose-900{--tw-gradient-to:var(--color-rose-900);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.bg-\[length\:1rem_1rem\]{background-size:1rem 1rem}.fill-current{fill:currentColor}.fill-indigo-500{fill:var(--color-indigo-500)}.p-0{padding:calc(var(--spacing)*0)}.p-4{padding:calc(var(--spacing)*4)}.p-6{padding:calc(var(--spacing)*6)}.p-8{padding:calc(var(--spacing)*8)}.px-1\.5{padding-inline:calc(var(--spacing)*1.5)}.px-2{padding-inline:calc(var(--spacing)*2)}.px-3{padding-inline:calc(var(--spacing)*3)}.px-4{padding-inline:calc(var(--spacing)*4)}.px-8{padding-inline:calc(var(--spacing)*8)}.py-0\.5{padding-block:calc(var(--spacing)*.5)}.py-1{padding-block:calc(var(--spacing)*1)}.py-2{padding-block:calc(var(--spacing)*2)}.py-3{padding-block:calc(var(--spacing)*3)}.py-4{padding-block:calc(var(--spacing)*4)}.py-6{padding-block:calc(var(--spacing)*6)}.py-8{padding-block:calc(var(--spacing)*8)}.text-center{text-align:center}.text-left{text-align:left}.font-mono{font-family:var(--font-mono)}.text-2xl{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height))}.text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xl{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.font-bold{--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold)}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.tracking-wide{--tw-tracking:var(--tracking-wide);letter-spacing:var(--tracking-wide)}.tracking-wider{--tw-tracking:var(--tracking-wider);letter-spacing:var(--tracking-wider)}.whitespace-pre-wrap{white-space:pre-wrap}.text-amber-600{color:var(--color-amber-600)}.text-amber-700{color:var(--color-amber-700)}.text-amber-800{color:var(--color-amber-800)}.text-blue-500{color:var(--color-blue-500)}.text-blue-600{color:var(--color-blue-600)}.text-blue-700{color:var(--color-blue-700)}.text-blue-800{color:var(--color-blue-800)}.text-cyan-100{color:var(--color-cyan-100)}.text-gray-100{color:var(--color-gray-100)}.text-gray-400{color:var(--color-gray-400)}.text-gray-500{color:var(--color-gray-500)}.text-gray-600{color:var(--color-gray-600)}.text-gray-700{color:var(--color-gray-700)}.text-gray-800{color:var(--color-gray-800)}.text-gray-900{color:var(--color-gray-900)}.text-green-500{color:var(--color-green-500)}.text-green-600{color:var(--color-green-600)}.text-green-700{color:var(--color-green-700)}.text-green-800{color:var(--color-green-800)}.text-purple-800{color:var(--color-purple-800)}.text-red-500{color:var(--color-red-500)}.text-red-600{color:var(--color-red-600)}.text-red-700{color:var(--color-red-700)}.text-red-800{color:var(--color-red-800)}.text-white{color:var(--color-white)}.uppercase{text-transform:uppercase}.no-underline{text-decoration-line:none}.opacity-80{opacity:.8}.shadow{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a),0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-lg{--tw-shadow:0 10px 15px -3px var(--tw-shadow-color,#0000001a),0 4px 6px -4px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-md{--tw-shadow:0 4px 6px -1px var(--tw-shadow-color,#0000001a),0 2px 4px -2px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-sm{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a),0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-xl{--tw-shadow:0 20px 25px -5px var(--tw-shadow-color,#0000001a),0 8px 10px -6px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.ring{--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(1px + var(--tw-ring-offset-width))var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.transition-all{transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-colors{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-transform{transition-property:transform,translate,scale,rotate;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.duration-150{--tw-duration:.15s;transition-duration:.15s}.duration-300{--tw-duration:.3s;transition-duration:.3s}.even\:bg-gray-50:nth-child(2n){background-color:var(--color-gray-50)}@media (hover:hover){.hover\:scale-105:hover{--tw-scale-x:105%;--tw-scale-y:105%;--tw-scale-z:105%;scale:var(--tw-scale-x)var(--tw-scale-y)}.hover\:bg-amber-600:hover{background-color:var(--color-amber-600)}.hover\:bg-blue-700:hover{background-color:var(--color-blue-700)}.hover\:bg-cyan-600:hover{background-color:var(--color-cyan-600)}.hover\:bg-gray-50:hover{background-color:var(--color-gray-50)}.hover\:bg-green-600:hover{background-color:var(--color-green-600)}.hover\:bg-white\/25:hover{background-color:#ffffff40}@supports (color:color-mix(in lab, red, red)){.hover\:bg-white\/25:hover{background-color:color-mix(in oklab,var(--color-white)25%,transparent)}}.hover\:text-white:hover{color:var(--color-white)}}.active\:scale-95:active{--tw-scale-x:95%;--tw-scale-y:95%;--tw-scale-z:95%;scale:var(--tw-scale-x)var(--tw-scale-y)}@media (min-width:48rem){.md\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.md\:grid-cols-6{grid-template-columns:repeat(6,minmax(0,1fr))}}.dark\:border-blue-800:where([data-theme=dark],[data-theme=dark] *){border-color:var(--color-blue-800)}.dark\:border-gray-600:where([data-theme=dark],[data-theme=dark] *){border-color:var(--color-gray-600)}.dark\:border-gray-700:where([data-theme=dark],[data-theme=dark] *){border-color:var(--color-gray-700)}.dark\:border-red-800:where([data-theme=dark],[data-theme=dark] *){border-color:var(--color-red-800)}.dark\:bg-amber-900\/20:where([data-theme=dark],[data-theme=dark] *){background-color:#7b330633}@supports (color:color-mix(in lab, red, red)){.dark\:bg-amber-900\/20:where([data-theme=dark],[data-theme=dark] *){background-color:color-mix(in oklab,var(--color-amber-900)20%,transparent)}}.dark\:bg-amber-900\/30:where([data-theme=dark],[data-theme=dark] *){background-color:#7b33064d}@supports (color:color-mix(in lab, red, red)){.dark\:bg-amber-900\/30:where([data-theme=dark],[data-theme=dark] *){background-color:color-mix(in oklab,var(--color-amber-900)30%,transparent)}}.dark\:bg-amber-900\/50:where([data-theme=dark],[data-theme=dark] *){background-color:#7b330680}@supports (color:color-mix(in lab, red, red)){.dark\:bg-amber-900\/50:where([data-theme=dark],[data-theme=dark] *){background-color:color-mix(in oklab,var(--color-amber-900)50%,transparent)}}.dark\:bg-blue-800\/30:where([data-theme=dark],[data-theme=dark] *){background-color:#193cb84d}@supports (color:color-mix(in lab, red, red)){.dark\:bg-blue-800\/30:where([data-theme=dark],[data-theme=dark] *){background-color:color-mix(in oklab,var(--color-blue-800)30%,transparent)}}.dark\:bg-blue-900\/20:where([data-theme=dark],[data-theme=dark] *){background-color:#1c398e33}@supports (color:color-mix(in lab, red, red)){.dark\:bg-blue-900\/20:where([data-theme=dark],[data-theme=dark] *){background-color:color-mix(in oklab,var(--color-blue-900)20%,transparent)}}.dark\:bg-blue-900\/30:where([data-theme=dark],[data-theme=dark] *){background-color:#1c398e4d}@supports (color:color-mix(in lab, red, red)){.dark\:bg-blue-900\/30:where([data-theme=dark],[data-theme=dark] *){background-color:color-mix(in oklab,var(--color-blue-900)30%,transparent)}}.dark\:bg-blue-900\/50:where([data-theme=dark],[data-theme=dark] *){background-color:#1c398e80}@supports (color:color-mix(in lab, red, red)){.dark\:bg-blue-900\/50:where([data-theme=dark],[data-theme=dark] *){background-color:color-mix(in oklab,var(--color-blue-900)50%,transparent)}}.dark\:bg-gray-600:where([data-theme=dark],[data-theme=dark] *){background-color:var(--color-gray-600)}.dark\:bg-gray-700:where([data-theme=dark],[data-theme=dark] *){background-color:var(--color-gray-700)}.dark\:bg-gray-800:where([data-theme=dark],[data-theme=dark] *){background-color:var(--color-gray-800)}.dark\:bg-gray-900:where([data-theme=dark],[data-theme=dark] *){background-color:var(--color-gray-900)}.dark\:bg-green-900\/20:where([data-theme=dark],[data-theme=dark] *){background-color:#0d542b33}@supports (color:color-mix(in lab, red, red)){.dark\:bg-green-900\/20:where([data-theme=dark],[data-theme=dark] *){background-color:color-mix(in oklab,var(--color-green-900)20%,transparent)}}.dark\:bg-green-900\/30:where([data-theme=dark],[data-theme=dark] *){background-color:#0d542b4d}@supports (color:color-mix(in lab, red, red)){.dark\:bg-green-900\/30:where([data-theme=dark],[data-theme=dark] *){background-color:color-mix(in oklab,var(--color-green-900)30%,transparent)}}.dark\:bg-green-900\/50:where([data-theme=dark],[data-theme=dark] *){background-color:#0d542b80}@supports (color:color-mix(in lab, red, red)){.dark\:bg-green-900\/50:where([data-theme=dark],[data-theme=dark] *){background-color:color-mix(in oklab,var(--color-green-900)50%,transparent)}}.dark\:bg-purple-900\/50:where([data-theme=dark],[data-theme=dark] *){background-color:#59168b80}@supports (color:color-mix(in lab, red, red)){.dark\:bg-purple-900\/50:where([data-theme=dark],[data-theme=dark] *){background-color:color-mix(in oklab,var(--color-purple-900)50%,transparent)}}.dark\:bg-red-800:where([data-theme=dark],[data-theme=dark] *){background-color:var(--color-red-800)}.dark\:bg-red-900\/20:where([data-theme=dark],[data-theme=dark] *){background-color:#82181a33}@supports (color:color-mix(in lab, red, red)){.dark\:bg-red-900\/20:where([data-theme=dark],[data-theme=dark] *){background-color:color-mix(in oklab,var(--color-red-900)20%,transparent)}}.dark\:bg-red-900\/30:where([data-theme=dark],[data-theme=dark] *){background-color:#82181a4d}@supports (color:color-mix(in lab, red, red)){.dark\:bg-red-900\/30:where([data-theme=dark],[data-theme=dark] *){background-color:color-mix(in oklab,var(--color-red-900)30%,transparent)}}.dark\:bg-red-900\/40:where([data-theme=dark],[data-theme=dark] *){background-color:#82181a66}@supports (color:color-mix(in lab, red, red)){.dark\:bg-red-900\/40:where([data-theme=dark],[data-theme=dark] *){background-color:color-mix(in oklab,var(--color-red-900)40%,transparent)}}.dark\:bg-red-900\/50:where([data-theme=dark],[data-theme=dark] *){background-color:#82181a80}@supports (color:color-mix(in lab, red, red)){.dark\:bg-red-900\/50:where([data-theme=dark],[data-theme=dark] *){background-color:color-mix(in oklab,var(--color-red-900)50%,transparent)}}.dark\:text-amber-200:where([data-theme=dark],[data-theme=dark] *){color:var(--color-amber-200)}.dark\:text-amber-300:where([data-theme=dark],[data-theme=dark] *){color:var(--color-amber-300)}.dark\:text-amber-400:where([data-theme=dark],[data-theme=dark] *){color:var(--color-amber-400)}.dark\:text-blue-200:where([data-theme=dark],[data-theme=dark] *){color:var(--color-blue-200)}.dark\:text-blue-300:where([data-theme=dark],[data-theme=dark] *){color:var(--color-blue-300)}.dark\:text-blue-400:where([data-theme=dark],[data-theme=dark] *){color:var(--color-blue-400)}.dark\:text-cyan-200:where([data-theme=dark],[data-theme=dark] *){color:var(--color-cyan-200)}.dark\:text-gray-100:where([data-theme=dark],[data-theme=dark] *){color:var(--color-gray-100)}.dark\:text-gray-200:where([data-theme=dark],[data-theme=dark] *){color:var(--color-gray-200)}.dark\:text-gray-300:where([data-theme=dark],[data-theme=dark] *){color:var(--color-gray-300)}.dark\:text-gray-400:where([data-theme=dark],[data-theme=dark] *){color:var(--color-gray-400)}.dark\:text-green-200:where([data-theme=dark],[data-theme=dark] *){color:var(--color-green-200)}.dark\:text-green-300:where([data-theme=dark],[data-theme=dark] *){color:var(--color-green-300)}.dark\:text-green-400:where([data-theme=dark],[data-theme=dark] *){color:var(--color-green-400)}.dark\:text-purple-200:where([data-theme=dark],[data-theme=dark] *){color:var(--color-purple-200)}.dark\:text-red-200:where([data-theme=dark],[data-theme=dark] *){color:var(--color-red-200)}.dark\:text-red-300:where([data-theme=dark],[data-theme=dark] *){color:var(--color-red-300)}.dark\:text-red-400:where([data-theme=dark],[data-theme=dark] *){color:var(--color-red-400)}.dark\:even\:bg-gray-800\/50:where([data-theme=dark],[data-theme=dark] *):nth-child(2n){background-color:#1e293980}@supports (color:color-mix(in lab, red, red)){.dark\:even\:bg-gray-800\/50:where([data-theme=dark],[data-theme=dark] *):nth-child(2n){background-color:color-mix(in oklab,var(--color-gray-800)50%,transparent)}}@media (hover:hover){.dark\:hover\:bg-gray-700:where([data-theme=dark],[data-theme=dark] *):hover{background-color:var(--color-gray-700)}}}@keyframes progress-stripe{0%{background-position:1rem 0}to{background-position:0 0}}.icon{vertical-align:middle;fill:currentColor;width:1em;height:1em;display:inline-block}.icon-lg{width:1.5em;height:1.5em}*,:before,:after{transition:background-color .3s,border-color .3s,color .3s}.btn{text-align:center;cursor:pointer;border:none;border-radius:.375rem;justify-content:center;align-items:center;padding:.5rem 1rem;font-size:.875rem;font-weight:500;line-height:1.25rem;text-decoration:none;transition:all .15s;display:inline-flex}.btn:hover{transform:scale(1.05);box-shadow:0 10px 15px -3px #0000001a,0 4px 6px -4px #0000001a}.btn:active{transform:scale(.95)}.btn:focus{outline-offset:2px;outline:2px solid #0000}.btn:focus-visible{box-shadow:0 0 0 2px #fff,0 0 0 4px #3b82f6}.btn-sm{padding:.375rem .75rem;font-size:.75rem}.btn-icon{min-width:2.25rem;min-height:2.25rem;vertical-align:middle;padding:.5rem .75rem}.btn-blue{color:#fff;background-color:#2563eb}.btn-blue:hover{background-color:#1d4ed8}.btn-green{color:#fff;background-color:#22c55e}.btn-green:hover{background-color:#16a34a}.btn-red{color:#fff;background-color:#ef4444}.btn-red:hover{background-color:#dc2626}.btn-amber{color:#fff;background-color:#f59e0b}.btn-amber:hover{background-color:#d97706}.btn-cyan{color:#fff;background-color:#06b6d4}.btn-cyan:hover{background-color:#0891b2}.btn-purple{color:#fff;background-color:#a855f7}.btn-purple:hover{background-color:#9333ea}.btn-indigo{color:#fff;background-color:#6366f1}.btn-indigo:hover{background-color:#4f46e5}.btn-gray{color:#fff;background-color:#6b7280}.btn-gray:hover{background-color:#4b5563}.btn-secondary{color:#374151;background-color:#e5e7eb}.btn-secondary:hover{background-color:#d1d5db}[data-theme=dark] .btn-secondary{color:#e5e7eb;background-color:#374151}[data-theme=dark] .btn-secondary:hover{background-color:#4b5563}.btn-danger-ghost{color:#b91c1c;background-color:#fee2e2}.btn-danger-ghost:hover{background-color:#fecaca}[data-theme=dark] .btn-danger-ghost{color:#fca5a5;background-color:#7f1d1d4d}[data-theme=dark] .btn-danger-ghost:hover{background-color:#7f1d1d80}.alert{border-style:solid;border-width:1px;border-radius:.5rem;margin-bottom:1rem;padding:.75rem 1rem;font-size:.875rem;line-height:1.25rem}.alert-success{color:#166534;background-color:#dcfce7;border-color:#86efac}[data-theme=dark] .alert-success{color:#bbf7d0;background-color:#22c55e33;border-color:#166534}.alert-error{color:#991b1b;background-color:#fee2e2;border-color:#fca5a5}[data-theme=dark] .alert-error{color:#fecaca;background-color:#ef444433;border-color:#7f1d1d}.alert-warning{color:#92400e;background-color:#fef3c7;border-color:#fcd34d}[data-theme=dark] .alert-warning{color:#fde68a;background-color:#f59e0b33;border-color:#78350f}.alert-info{color:#155e75;background-color:#ecfeff;border-color:#a5f3fc}[data-theme=dark] .alert-info{color:#a5f3fc;background-color:#06b6d433;border-color:#164e63}.alert-lg{border-radius:.75rem;margin-bottom:1.5rem;padding:1.5rem}.card{background-color:#fff;border:1px solid #e5e7eb;border-radius:.5rem;box-shadow:0 1px 2px #0000000d}[data-theme=dark] .card{background-color:#1f2937;border-color:#374151}.card-body{padding:1.25rem}.card-header{text-transform:uppercase;letter-spacing:.05em;color:#6b7280;border-bottom:1px solid #e5e7eb;padding:.75rem 1.5rem;font-size:.75rem;font-weight:600}[data-theme=dark] .card-header{color:#9ca3af;border-color:#374151}.info-box{color:#fff;background:linear-gradient(90deg,#0891b2,#0e7490);border-radius:.75rem;margin-bottom:1.5rem;padding:1rem 1.5rem}[data-theme=dark] .info-box{background:linear-gradient(90deg,#155e75,#164e63)}.stat-box{text-align:center;background-color:#fff;border:1px solid #e5e7eb;border-radius:.5rem;padding:1.25rem;transition:all .2s;box-shadow:0 1px 2px #0000000d}[data-theme=dark] .stat-box{background-color:#1f2937;border-color:#374151}.stat-box:hover{transform:translateY(-.25rem);box-shadow:0 10px 15px -3px #0000001a,0 4px 6px -4px #0000001a}.stat-value{color:#111827;font-size:1.875rem;font-weight:700}[data-theme=dark] .stat-value{color:#f3f4f6}.stat-label{text-transform:uppercase;letter-spacing:.05em;color:#6b7280;margin-top:.5rem;font-size:.75rem}[data-theme=dark] .stat-label{color:#9ca3af}.metric-box{text-align:center;background-color:#f9fafb;border:1px solid #e5e7eb;border-radius:.5rem;padding:.75rem}[data-theme=dark] .metric-box{background-color:#374151;border-color:#4b5563}.metric-label{text-transform:uppercase;letter-spacing:.05em;color:#6b7280;margin-bottom:.25rem;font-size:.75rem;display:block}[data-theme=dark] .metric-label{color:#9ca3af}.metric-value{color:#111827;font-size:.875rem;font-weight:600;display:block}[data-theme=dark] .metric-value{color:#f3f4f6}.table{border-collapse:collapse;background-color:#fff;border-radius:.5rem;width:100%;overflow:hidden;box-shadow:0 1px 2px #0000000d}[data-theme=dark] .table{background-color:#1f2937}.table thead{background-color:#f9fafb}[data-theme=dark] .table thead{background-color:#374151}.table th{text-align:left;text-transform:uppercase;letter-spacing:.05em;color:#6b7280;border-bottom:2px solid #e5e7eb;padding:.75rem 1rem;font-size:.75rem;font-weight:600}[data-theme=dark] .table th{color:#9ca3af;border-color:#4b5563}.table td{color:#111827;border-bottom:1px solid #e5e7eb;padding:.75rem 1rem}[data-theme=dark] .table td{color:#f3f4f6;border-color:#374151}.table tbody tr{transition:background-color .15s}.table tbody tr:hover{background-color:#f9fafb}[data-theme=dark] .table tbody tr:hover{background-color:#374151}.table tbody tr:nth-child(2n){background-color:#f9fafb}[data-theme=dark] .table tbody tr:nth-child(2n){background-color:#1f293780}.badge{border-radius:.25rem;padding:.25rem .5rem;font-size:.75rem;font-weight:500;display:inline-block}.badge-blue{color:#1e40af;background-color:#dbeafe}[data-theme=dark] .badge-blue{color:#93c5fd;background-color:#3b82f64d}.badge-green{color:#166534;background-color:#dcfce7}[data-theme=dark] .badge-green{color:#86efac;background-color:#22c55e4d}.badge-amber{color:#92400e;background-color:#fef3c7}[data-theme=dark] .badge-amber{color:#fcd34d;background-color:#f59e0b4d}.badge-red{color:#991b1b;background-color:#fee2e2}[data-theme=dark] .badge-red{color:#fca5a5;background-color:#ef44444d}.badge-purple{color:#7c3aed;background-color:#f3e8ff}[data-theme=dark] .badge-purple{color:#c4b5fd;background-color:#8b5cf64d}.badge-gray{color:#4b5563;background-color:#f3f4f6}[data-theme=dark] .badge-gray{color:#d1d5db;background-color:#374151}.code{background-color:#e5e7eb;border-radius:.25rem;padding:.125rem .25rem;font-family:ui-monospace,SFMono-Regular,SF Mono,Menlo,Consolas,monospace;font-size:.875rem}[data-theme=dark] .code{background-color:#374151}.modal-overlay{z-index:50;background-color:#00000080;justify-content:center;align-items:center;display:flex;position:fixed;inset:0}.modal-overlay.hidden{display:none}.modal{background-color:#fff;border-radius:.75rem;flex-direction:column;width:100%;max-width:56rem;max-height:90vh;margin:1rem;display:flex;box-shadow:0 25px 50px -12px #00000040}[data-theme=dark] .modal{background-color:#1f2937}.modal-header{border-bottom:1px solid #e5e7eb;justify-content:space-between;align-items:center;padding:1rem 1.5rem;display:flex}[data-theme=dark] .modal-header{border-color:#374151}.modal-title{color:#111827;font-size:1.125rem;font-weight:600}[data-theme=dark] .modal-title{color:#f3f4f6}.modal-subtitle{color:#6b7280;font-size:.875rem}[data-theme=dark] .modal-subtitle{color:#9ca3af}.modal-close{color:#9ca3af;cursor:pointer;background:0 0;border:none;padding:.5rem;transition:color .15s}.modal-close:hover{color:#4b5563}[data-theme=dark] .modal-close:hover{color:#e5e7eb}.modal-body{flex-direction:column;flex:1;padding:1.5rem;display:flex;overflow:hidden}.modal-footer{border-top:1px solid #e5e7eb;justify-content:flex-end;gap:.75rem;padding:1rem 1.5rem;display:flex}[data-theme=dark] .modal-footer{border-color:#374151}.dropzone{text-align:center;cursor:pointer;background-color:#f9fafb;border:2px dashed #d1d5db;border-radius:.75rem;padding:2rem;transition:all .2s}[data-theme=dark] .dropzone{background-color:#1f293780;border-color:#4b5563}.dropzone:hover{background-color:#eff6ff;border-color:#3b82f6;transform:scale(1.01);box-shadow:0 10px 15px -3px #0000001a}[data-theme=dark] .dropzone:hover{background-color:#3b82f61a}.dropzone.drag-over{background-color:#eff6ff;border-color:#3b82f6}[data-theme=dark] .dropzone.drag-over{background-color:#1e3a5f}.progress{background-color:#e5e7eb;border-radius:.375rem;height:.625rem;overflow:hidden}[data-theme=dark] .progress{background-color:#374151}.progress-bar{background:linear-gradient(45deg,#ffffff26 25%,#0000 25% 50%,#ffffff26 50% 75%,#0000 75%,#0000) 0 0/1rem 1rem;border-radius:.375rem;height:100%;transition:width .3s;animation:1s linear infinite progress-stripe}.spinner{border:4px solid #e5e7eb;border-top-color:#3b82f6;border-radius:50%;animation:1s linear infinite spin}.spinner-sm{border-width:2px;width:1.5rem;height:1.5rem}.spinner-md{width:2.5rem;height:2.5rem}.spinner-lg{width:3rem;height:3rem}@keyframes spin{to{transform:rotate(360deg)}}.tooltip-trigger{cursor:help;color:#6b7280;background-color:#e5e7eb;border-radius:9999px;justify-content:center;align-items:center;width:1rem;height:1rem;margin-left:.25rem;font-size:.65rem;display:inline-flex;position:relative}[data-theme=dark] .tooltip-trigger{color:#9ca3af;background-color:#4b5563}.tooltip-content{white-space:nowrap;opacity:0;visibility:hidden;z-index:100;color:#fff;background-color:#1f2937;border-radius:.375rem;max-width:300px;margin-bottom:.5rem;padding:.5rem .75rem;font-size:.75rem;font-weight:400;transition:opacity .2s,visibility .2s;position:absolute;bottom:100%;left:50%;transform:translate(-50%)}[data-theme=dark] .tooltip-content{color:#1f2937;background-color:#f3f4f6}.tooltip-content:after{content:"";border:6px solid #0000;border-top-color:#1f2937;position:absolute;top:100%;left:50%;transform:translate(-50%)}[data-theme=dark] .tooltip-content:after{border-top-color:#f3f4f6}.tooltip-multiline{white-space:normal;text-align:left;line-height:1.4}.tooltip-trigger:hover .tooltip-content,.tooltip-trigger:focus .tooltip-content{opacity:1;visibility:visible}.loading-overlay{z-index:9999;background:#00000080;justify-content:center;align-items:center;display:none;position:fixed;inset:0}.loading-overlay.active{display:flex}.file-upload__input{opacity:0;cursor:pointer;width:100%;height:100%;position:absolute;top:0;left:0}.file-upload__dropzone{position:relative}.file-upload__item{border-radius:.5rem;justify-content:space-between;align-items:center;margin-bottom:.5rem;padding:.75rem 1rem;display:flex}.file-upload__item-info{align-items:center;gap:.75rem;display:flex}.file-upload__item-icon{border-radius:.375rem;justify-content:center;align-items:center;width:2rem;height:2rem;display:flex}.file-upload__item-progress{border-radius:2px;height:4px;margin-top:.5rem;overflow:hidden}.file-upload__item-progress-bar{height:100%;transition:width .3s}details.error-details summary{list-style:none}details.error-details summary::-webkit-details-marker{display:none}details.error-details[open] .error-details__chevron{transform:rotate(180deg)}input:focus,select:focus,textarea:focus{outline:none;box-shadow:0 0 0 2px #fff,0 0 0 4px #3b82f6}[data-theme=dark] input:focus,[data-theme=dark] select:focus,[data-theme=dark] textarea:focus{box-shadow:0 0 0 2px #1f2937,0 0 0 4px #3b82f6}.modal-close:focus{outline:none}.modal-close:focus-visible{border-radius:.375rem;box-shadow:0 0 0 2px #fff,0 0 0 4px #3b82f6}[data-theme=dark] .modal-close:focus-visible{box-shadow:0 0 0 2px #1f2937,0 0 0 4px #3b82f6}.preview-tab:focus{outline:none}.preview-tab:focus-visible{border-radius:.25rem;box-shadow:0 0 0 2px #fff,0 0 0 4px #3b82f6}[data-theme=dark] .preview-tab:focus-visible{box-shadow:0 0 0 2px #1f2937,0 0 0 4px #3b82f6}a:focus{outline:none}a:focus-visible{border-radius:.25rem;box-shadow:0 0 0 2px #fff,0 0 0 4px #3b82f6}[data-theme=dark] a:focus-visible{box-shadow:0 0 0 2px #1f2937,0 0 0 4px #3b82f6}.dropzone:focus-within{border-color:#3b82f6;box-shadow:0 0 0 2px #fff,0 0 0 4px #3b82f6}[data-theme=dark] .dropzone:focus-within{box-shadow:0 0 0 2px #1f2937,0 0 0 4px #3b82f6}details.error-details summary:focus{outline:none}details.error-details summary:focus-visible{border-radius:.375rem;box-shadow:0 0 0 2px #fff,0 0 0 4px #3b82f6}[data-theme=dark] details.error-details summary:focus-visible{box-shadow:0 0 0 2px #1f2937,0 0 0 4px #3b82f6}.btn-blue:focus-visible{box-shadow:0 0 0 2px #fff,0 0 0 4px #fbbf24}[data-theme=dark] .btn-blue:focus-visible{box-shadow:0 0 0 2px #1f2937,0 0 0 4px #fbbf24}::-webkit-scrollbar{width:8px;height:8px}::-webkit-scrollbar-track{background:#f1f1f1;border-radius:4px}::-webkit-scrollbar-thumb{background:#c1c1c1;border-radius:4px}::-webkit-scrollbar-thumb:hover{background:#a1a1a1}[data-theme=dark] ::-webkit-scrollbar-track{background:#2d2d3f}[data-theme=dark] ::-webkit-scrollbar-thumb{background:#4a4a5a}[data-theme=dark] ::-webkit-scrollbar-thumb:hover{background:#5a5a6a}.text-muted{color:#6b7280}[data-theme=dark] .text-muted{color:#9ca3af}.flex-center{justify-content:center;align-items:center;display:flex}.container-70{width:100%;max-width:70vw;margin-left:auto;margin-right:auto}@media (max-width:768px){.container-70{max-width:95vw}}@property --tw-translate-x{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-y{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-z{syntax:"*";inherits:false;initial-value:0}@property --tw-space-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-gradient-position{syntax:"*";inherits:false}@property --tw-gradient-from{syntax:"";inherits:false;initial-value:#0000}@property --tw-gradient-via{syntax:"";inherits:false;initial-value:#0000}@property --tw-gradient-to{syntax:"";inherits:false;initial-value:#0000}@property --tw-gradient-stops{syntax:"*";inherits:false}@property --tw-gradient-via-stops{syntax:"*";inherits:false}@property --tw-gradient-from-position{syntax:"";inherits:false;initial-value:0%}@property --tw-gradient-via-position{syntax:"";inherits:false;initial-value:50%}@property --tw-gradient-to-position{syntax:"";inherits:false;initial-value:100%}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-tracking{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-duration{syntax:"*";inherits:false}@property --tw-scale-x{syntax:"*";inherits:false;initial-value:1}@property --tw-scale-y{syntax:"*";inherits:false;initial-value:1}@property --tw-scale-z{syntax:"*";inherits:false;initial-value:1}@keyframes pulse{50%{opacity:.5}} \ No newline at end of file +@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0;--tw-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-space-y-reverse:0;--tw-divide-y-reverse:0;--tw-border-style:solid;--tw-gradient-position:initial;--tw-gradient-from:#0000;--tw-gradient-via:#0000;--tw-gradient-to:#0000;--tw-gradient-stops:initial;--tw-gradient-via-stops:initial;--tw-gradient-from-position:0%;--tw-gradient-via-position:50%;--tw-gradient-to-position:100%;--tw-font-weight:initial;--tw-tracking:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-duration:initial;--tw-scale-x:1;--tw-scale-y:1;--tw-scale-z:1}}}@layer theme{:root,:host{--font-sans:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--font-mono:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;--color-red-50:oklch(97.1% .013 17.38);--color-red-100:oklch(93.6% .032 17.717);--color-red-200:oklch(88.5% .062 18.334);--color-red-300:oklch(80.8% .114 19.571);--color-red-400:oklch(70.4% .191 22.216);--color-red-500:oklch(63.7% .237 25.331);--color-red-600:oklch(57.7% .245 27.325);--color-red-700:oklch(50.5% .213 27.518);--color-red-800:oklch(44.4% .177 26.899);--color-red-900:oklch(39.6% .141 25.723);--color-amber-50:oklch(98.7% .022 95.277);--color-amber-100:oklch(96.2% .059 95.617);--color-amber-200:oklch(92.4% .12 95.746);--color-amber-300:oklch(87.9% .169 91.605);--color-amber-400:oklch(82.8% .189 84.429);--color-amber-500:oklch(76.9% .188 70.08);--color-amber-600:oklch(66.6% .179 58.318);--color-amber-700:oklch(55.5% .163 48.998);--color-amber-800:oklch(47.3% .137 46.201);--color-amber-900:oklch(41.4% .112 45.904);--color-yellow-50:oklch(98.7% .026 102.212);--color-yellow-100:oklch(97.3% .071 103.193);--color-yellow-200:oklch(94.5% .129 101.54);--color-yellow-300:oklch(90.5% .182 98.111);--color-yellow-500:oklch(79.5% .184 86.047);--color-yellow-600:oklch(68.1% .162 75.834);--color-yellow-800:oklch(47.6% .114 61.907);--color-yellow-900:oklch(42.1% .095 57.708);--color-green-50:oklch(98.2% .018 155.826);--color-green-100:oklch(96.2% .044 156.743);--color-green-200:oklch(92.5% .084 155.995);--color-green-300:oklch(87.1% .15 154.449);--color-green-400:oklch(79.2% .209 151.711);--color-green-500:oklch(72.3% .219 149.579);--color-green-600:oklch(62.7% .194 149.214);--color-green-700:oklch(52.7% .154 150.069);--color-green-800:oklch(44.8% .119 151.328);--color-green-900:oklch(39.3% .095 152.535);--color-cyan-100:oklch(95.6% .045 203.388);--color-cyan-200:oklch(91.7% .08 205.041);--color-cyan-500:oklch(71.5% .143 215.221);--color-cyan-600:oklch(60.9% .126 221.723);--color-cyan-900:oklch(39.8% .07 227.392);--color-blue-50:oklch(97% .014 254.604);--color-blue-100:oklch(93.2% .032 255.585);--color-blue-200:oklch(88.2% .059 254.128);--color-blue-300:oklch(80.9% .105 251.813);--color-blue-400:oklch(70.7% .165 254.624);--color-blue-500:oklch(62.3% .214 259.815);--color-blue-600:oklch(54.6% .245 262.881);--color-blue-700:oklch(48.8% .243 264.376);--color-blue-800:oklch(42.4% .199 265.638);--color-blue-900:oklch(37.9% .146 265.522);--color-indigo-100:oklch(93% .034 272.788);--color-indigo-500:oklch(58.5% .233 277.117);--color-indigo-600:oklch(51.1% .262 276.966);--color-indigo-900:oklch(35.9% .144 278.697);--color-purple-100:oklch(94.6% .033 307.174);--color-purple-200:oklch(90.2% .063 306.703);--color-purple-600:oklch(55.8% .288 302.321);--color-purple-800:oklch(43.8% .218 303.724);--color-purple-900:oklch(38.1% .176 304.987);--color-rose-900:oklch(41% .159 10.272);--color-gray-50:oklch(98.5% .002 247.839);--color-gray-100:oklch(96.7% .003 264.542);--color-gray-200:oklch(92.8% .006 264.531);--color-gray-300:oklch(87.2% .01 258.338);--color-gray-400:oklch(70.7% .022 261.325);--color-gray-500:oklch(55.1% .027 264.364);--color-gray-600:oklch(44.6% .03 256.802);--color-gray-700:oklch(37.3% .034 259.733);--color-gray-800:oklch(27.8% .033 256.848);--color-gray-900:oklch(21% .034 264.665);--color-black:#000;--color-white:#fff;--spacing:.25rem;--container-md:28rem;--container-lg:32rem;--container-2xl:42rem;--container-4xl:56rem;--container-7xl:80rem;--text-xs:.75rem;--text-xs--line-height:calc(1/.75);--text-sm:.875rem;--text-sm--line-height:calc(1.25/.875);--text-base:1rem;--text-base--line-height:calc(1.5/1);--text-lg:1.125rem;--text-lg--line-height:calc(1.75/1.125);--text-xl:1.25rem;--text-xl--line-height:calc(1.75/1.25);--text-2xl:1.5rem;--text-2xl--line-height:calc(2/1.5);--text-6xl:3.75rem;--text-6xl--line-height:1;--font-weight-medium:500;--font-weight-semibold:600;--font-weight-bold:700;--tracking-wide:.025em;--tracking-wider:.05em;--radius-md:.375rem;--radius-lg:.5rem;--radius-xl:.75rem;--animate-spin:spin 1s linear infinite;--animate-pulse:pulse 2s cubic-bezier(.4,0,.6,1)infinite;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4,0,.2,1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono);--animate-progress-stripe:progress-stripe 1s linear infinite}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab, red, red)){::placeholder{color:color-mix(in oklab,currentcolor 50%,transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}}@layer components;@layer utilities{.pointer-events-none{pointer-events:none}.visible{visibility:visible}.absolute{position:absolute}.fixed{position:fixed}.relative{position:relative}.static{position:static}.sticky{position:sticky}.inset-0{inset:calc(var(--spacing)*0)}.top-0{top:calc(var(--spacing)*0)}.z-50{z-index:50}.\!container{width:100%!important}@media (min-width:40rem){.\!container{max-width:40rem!important}}@media (min-width:48rem){.\!container{max-width:48rem!important}}@media (min-width:64rem){.\!container{max-width:64rem!important}}@media (min-width:80rem){.\!container{max-width:80rem!important}}@media (min-width:96rem){.\!container{max-width:96rem!important}}.container{width:100%}@media (min-width:40rem){.container{max-width:40rem}}@media (min-width:48rem){.container{max-width:48rem}}@media (min-width:64rem){.container{max-width:64rem}}@media (min-width:80rem){.container{max-width:80rem}}@media (min-width:96rem){.container{max-width:96rem}}.-mx-1\.5{margin-inline:calc(var(--spacing)*-1.5)}.mx-2{margin-inline:calc(var(--spacing)*2)}.mx-auto{margin-inline:auto}.-my-1\.5{margin-block:calc(var(--spacing)*-1.5)}.my-6{margin-block:calc(var(--spacing)*6)}.mt-1{margin-top:calc(var(--spacing)*1)}.mt-2{margin-top:calc(var(--spacing)*2)}.mt-3{margin-top:calc(var(--spacing)*3)}.mt-4{margin-top:calc(var(--spacing)*4)}.mr-1{margin-right:calc(var(--spacing)*1)}.mr-2{margin-right:calc(var(--spacing)*2)}.mb-1{margin-bottom:calc(var(--spacing)*1)}.mb-2{margin-bottom:calc(var(--spacing)*2)}.mb-3{margin-bottom:calc(var(--spacing)*3)}.mb-4{margin-bottom:calc(var(--spacing)*4)}.mb-6{margin-bottom:calc(var(--spacing)*6)}.ml-1{margin-left:calc(var(--spacing)*1)}.ml-2{margin-left:calc(var(--spacing)*2)}.ml-3{margin-left:calc(var(--spacing)*3)}.ml-4{margin-left:calc(var(--spacing)*4)}.ml-auto{margin-left:auto}.block{display:block}.contents{display:contents}.flex{display:flex}.grid{display:grid}.hidden{display:none}.inline{display:inline}.inline-block{display:inline-block}.inline-flex{display:inline-flex}.table{display:table}.h-2{height:calc(var(--spacing)*2)}.h-2\.5{height:calc(var(--spacing)*2.5)}.h-4{height:calc(var(--spacing)*4)}.h-5{height:calc(var(--spacing)*5)}.h-6{height:calc(var(--spacing)*6)}.h-8{height:calc(var(--spacing)*8)}.h-10{height:calc(var(--spacing)*10)}.h-12{height:calc(var(--spacing)*12)}.h-16{height:calc(var(--spacing)*16)}.h-24{height:calc(var(--spacing)*24)}.h-64{height:calc(var(--spacing)*64)}.h-full{height:100%}.max-h-\[60vh\]{max-height:60vh}.max-h-\[400px\]{max-height:400px}.min-h-\[60vh\]{min-height:60vh}.min-h-\[400px\]{min-height:400px}.min-h-full{min-height:100%}.min-h-screen{min-height:100vh}.w-2{width:calc(var(--spacing)*2)}.w-4{width:calc(var(--spacing)*4)}.w-5{width:calc(var(--spacing)*5)}.w-6{width:calc(var(--spacing)*6)}.w-8{width:calc(var(--spacing)*8)}.w-10{width:calc(var(--spacing)*10)}.w-12{width:calc(var(--spacing)*12)}.w-16{width:calc(var(--spacing)*16)}.w-24{width:calc(var(--spacing)*24)}.w-full{width:100%}.max-w-2xl{max-width:var(--container-2xl)}.max-w-4xl{max-width:var(--container-4xl)}.max-w-7xl{max-width:var(--container-7xl)}.max-w-\[90vw\]{max-width:90vw}.max-w-\[200px\]{max-width:200px}.max-w-lg{max-width:var(--container-lg)}.max-w-md{max-width:var(--container-md)}.min-w-0{min-width:calc(var(--spacing)*0)}.min-w-\[44px\]{min-width:44px}.flex-1{flex:1}.flex-shrink-0{flex-shrink:0}.-translate-y-1{--tw-translate-y:calc(var(--spacing)*-1);translate:var(--tw-translate-x)var(--tw-translate-y)}.rotate-180{rotate:180deg}.transform{transform:var(--tw-rotate-x,)var(--tw-rotate-y,)var(--tw-rotate-z,)var(--tw-skew-x,)var(--tw-skew-y,)}.animate-progress-stripe{animation:var(--animate-progress-stripe)}.animate-pulse{animation:var(--animate-pulse)}.animate-spin{animation:var(--animate-spin)}.cursor-help{cursor:help}.cursor-not-allowed{cursor:not-allowed}.cursor-pointer{cursor:pointer}.resize{resize:both}.list-inside{list-style-position:inside}.list-decimal{list-style-type:decimal}.list-disc{list-style-type:disc}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-center{align-items:center}.items-start{align-items:flex-start}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.justify-end{justify-content:flex-end}.gap-1{gap:calc(var(--spacing)*1)}.gap-2{gap:calc(var(--spacing)*2)}.gap-3{gap:calc(var(--spacing)*3)}.gap-4{gap:calc(var(--spacing)*4)}.gap-6{gap:calc(var(--spacing)*6)}:where(.space-y-1>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*1)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*1)*calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-2>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*2)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*2)*calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-3>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*3)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*3)*calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-4>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*4)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*4)*calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-6>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*6)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*6)*calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-8>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*8)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*8)*calc(1 - var(--tw-space-y-reverse)))}:where(.divide-y>:not(:last-child)){--tw-divide-y-reverse:0;border-bottom-style:var(--tw-border-style);border-top-style:var(--tw-border-style);border-top-width:calc(1px*var(--tw-divide-y-reverse));border-bottom-width:calc(1px*calc(1 - var(--tw-divide-y-reverse)))}:where(.divide-gray-200>:not(:last-child)){border-color:var(--color-gray-200)}.truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.overflow-auto{overflow:auto}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.rounded{border-radius:.25rem}.rounded-full{border-radius:3.40282e38px}.rounded-lg{border-radius:var(--radius-lg)}.rounded-md{border-radius:var(--radius-md)}.rounded-xl{border-radius:var(--radius-xl)}.border{border-style:var(--tw-border-style);border-width:1px}.border-2{border-style:var(--tw-border-style);border-width:2px}.border-4{border-style:var(--tw-border-style);border-width:4px}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.border-b-2{border-bottom-style:var(--tw-border-style);border-bottom-width:2px}.border-dashed{--tw-border-style:dashed;border-style:dashed}.border-amber-500{border-color:var(--color-amber-500)}.border-blue-200{border-color:var(--color-blue-200)}.border-blue-500{border-color:var(--color-blue-500)}.border-gray-200{border-color:var(--color-gray-200)}.border-gray-300{border-color:var(--color-gray-300)}.border-green-200{border-color:var(--color-green-200)}.border-red-200{border-color:var(--color-red-200)}.border-transparent{border-color:#0000}.border-white{border-color:var(--color-white)}.border-yellow-200{border-color:var(--color-yellow-200)}.border-t-blue-600{border-top-color:var(--color-blue-600)}.bg-amber-50{background-color:var(--color-amber-50)}.bg-amber-100{background-color:var(--color-amber-100)}.bg-amber-500{background-color:var(--color-amber-500)}.bg-black{background-color:var(--color-black)}.bg-black\/50{background-color:#00000080}@supports (color:color-mix(in lab, red, red)){.bg-black\/50{background-color:color-mix(in oklab,var(--color-black)50%,transparent)}}.bg-blue-50{background-color:var(--color-blue-50)}.bg-blue-100{background-color:var(--color-blue-100)}.bg-blue-600{background-color:var(--color-blue-600)}.bg-cyan-100{background-color:var(--color-cyan-100)}.bg-cyan-500{background-color:var(--color-cyan-500)}.bg-gray-50{background-color:var(--color-gray-50)}.bg-gray-100{background-color:var(--color-gray-100)}.bg-gray-200{background-color:var(--color-gray-200)}.bg-gray-900{background-color:var(--color-gray-900)}.bg-green-50{background-color:var(--color-green-50)}.bg-green-100{background-color:var(--color-green-100)}.bg-green-500{background-color:var(--color-green-500)}.bg-green-600{background-color:var(--color-green-600)}.bg-indigo-100{background-color:var(--color-indigo-100)}.bg-purple-100{background-color:var(--color-purple-100)}.bg-red-50{background-color:var(--color-red-50)}.bg-red-100{background-color:var(--color-red-100)}.bg-red-200{background-color:var(--color-red-200)}.bg-red-500{background-color:var(--color-red-500)}.bg-red-600{background-color:var(--color-red-600)}.bg-transparent{background-color:#0000}.bg-white{background-color:var(--color-white)}.bg-white\/90{background-color:#ffffffe6}@supports (color:color-mix(in lab, red, red)){.bg-white\/90{background-color:color-mix(in oklab,var(--color-white)90%,transparent)}}.bg-yellow-50{background-color:var(--color-yellow-50)}.bg-yellow-100{background-color:var(--color-yellow-100)}.bg-yellow-500{background-color:var(--color-yellow-500)}.bg-gradient-to-r{--tw-gradient-position:to right in oklab;background-image:linear-gradient(var(--tw-gradient-stops))}.from-blue-500{--tw-gradient-from:var(--color-blue-500);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-cyan-600{--tw-gradient-from:var(--color-cyan-600);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.to-blue-600{--tw-gradient-to:var(--color-blue-600);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.to-rose-900{--tw-gradient-to:var(--color-rose-900);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.bg-\[length\:1rem_1rem\]{background-size:1rem 1rem}.fill-current{fill:currentColor}.fill-indigo-500{fill:var(--color-indigo-500)}.p-0{padding:calc(var(--spacing)*0)}.p-0\.5{padding:calc(var(--spacing)*.5)}.p-1{padding:calc(var(--spacing)*1)}.p-1\.5{padding:calc(var(--spacing)*1.5)}.p-2{padding:calc(var(--spacing)*2)}.p-3{padding:calc(var(--spacing)*3)}.p-4{padding:calc(var(--spacing)*4)}.p-6{padding:calc(var(--spacing)*6)}.p-8{padding:calc(var(--spacing)*8)}.p-12{padding:calc(var(--spacing)*12)}.px-1{padding-inline:calc(var(--spacing)*1)}.px-1\.5{padding-inline:calc(var(--spacing)*1.5)}.px-2{padding-inline:calc(var(--spacing)*2)}.px-2\.5{padding-inline:calc(var(--spacing)*2.5)}.px-3{padding-inline:calc(var(--spacing)*3)}.px-4{padding-inline:calc(var(--spacing)*4)}.px-6{padding-inline:calc(var(--spacing)*6)}.px-8{padding-inline:calc(var(--spacing)*8)}.py-0{padding-block:calc(var(--spacing)*0)}.py-0\.5{padding-block:calc(var(--spacing)*.5)}.py-1{padding-block:calc(var(--spacing)*1)}.py-1\.5{padding-block:calc(var(--spacing)*1.5)}.py-2{padding-block:calc(var(--spacing)*2)}.py-3{padding-block:calc(var(--spacing)*3)}.py-4{padding-block:calc(var(--spacing)*4)}.py-6{padding-block:calc(var(--spacing)*6)}.py-8{padding-block:calc(var(--spacing)*8)}.py-12{padding-block:calc(var(--spacing)*12)}.pt-4{padding-top:calc(var(--spacing)*4)}.text-center{text-align:center}.text-left{text-align:left}.text-right{text-align:right}.font-mono{font-family:var(--font-mono)}.text-2xl{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height))}.text-6xl{font-size:var(--text-6xl);line-height:var(--tw-leading,var(--text-6xl--line-height))}.text-base{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}.text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xl{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.font-bold{--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold)}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.tracking-wide{--tw-tracking:var(--tracking-wide);letter-spacing:var(--tracking-wide)}.tracking-wider{--tw-tracking:var(--tracking-wider);letter-spacing:var(--tracking-wider)}.whitespace-nowrap{white-space:nowrap}.whitespace-pre-line{white-space:pre-line}.whitespace-pre-wrap{white-space:pre-wrap}.text-amber-600{color:var(--color-amber-600)}.text-amber-700{color:var(--color-amber-700)}.text-amber-800{color:var(--color-amber-800)}.text-blue-500{color:var(--color-blue-500)}.text-blue-600{color:var(--color-blue-600)}.text-blue-700{color:var(--color-blue-700)}.text-blue-800{color:var(--color-blue-800)}.text-cyan-100{color:var(--color-cyan-100)}.text-cyan-600{color:var(--color-cyan-600)}.text-gray-100{color:var(--color-gray-100)}.text-gray-400{color:var(--color-gray-400)}.text-gray-500{color:var(--color-gray-500)}.text-gray-600{color:var(--color-gray-600)}.text-gray-700{color:var(--color-gray-700)}.text-gray-800{color:var(--color-gray-800)}.text-gray-900{color:var(--color-gray-900)}.text-green-500{color:var(--color-green-500)}.text-green-600{color:var(--color-green-600)}.text-green-700{color:var(--color-green-700)}.text-green-800{color:var(--color-green-800)}.text-indigo-600{color:var(--color-indigo-600)}.text-purple-600{color:var(--color-purple-600)}.text-purple-800{color:var(--color-purple-800)}.text-red-500{color:var(--color-red-500)}.text-red-600{color:var(--color-red-600)}.text-red-700{color:var(--color-red-700)}.text-red-800{color:var(--color-red-800)}.text-white{color:var(--color-white)}.text-yellow-500{color:var(--color-yellow-500)}.text-yellow-600{color:var(--color-yellow-600)}.text-yellow-800{color:var(--color-yellow-800)}.uppercase{text-transform:uppercase}.italic{font-style:italic}.no-underline{text-decoration-line:none}.opacity-0{opacity:0}.opacity-25{opacity:.25}.opacity-50{opacity:.5}.opacity-75{opacity:.75}.opacity-80{opacity:.8}.opacity-100{opacity:1}.shadow{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a),0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-2xl{--tw-shadow:0 25px 50px -12px var(--tw-shadow-color,#00000040);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-lg{--tw-shadow:0 10px 15px -3px var(--tw-shadow-color,#0000001a),0 4px 6px -4px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-md{--tw-shadow:0 4px 6px -1px var(--tw-shadow-color,#0000001a),0 2px 4px -2px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-sm{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a),0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-xl{--tw-shadow:0 20px 25px -5px var(--tw-shadow-color,#0000001a),0 8px 10px -6px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.ring{--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(1px + var(--tw-ring-offset-width))var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.blur{--tw-blur:blur(8px);filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,)}.transition{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter,display,content-visibility,overlay,pointer-events;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-all{transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-colors{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-opacity{transition-property:opacity;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-transform{transition-property:transform,translate,scale,rotate;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.duration-150{--tw-duration:.15s;transition-duration:.15s}.duration-200{--tw-duration:.2s;transition-duration:.2s}.duration-300{--tw-duration:.3s;transition-duration:.3s}.duration-500{--tw-duration:.5s;transition-duration:.5s}.even\:bg-gray-50:nth-child(2n){background-color:var(--color-gray-50)}@media (hover:hover){.hover\:scale-105:hover{--tw-scale-x:105%;--tw-scale-y:105%;--tw-scale-z:105%;scale:var(--tw-scale-x)var(--tw-scale-y)}.hover\:border-gray-400:hover{border-color:var(--color-gray-400)}.hover\:bg-amber-600:hover{background-color:var(--color-amber-600)}.hover\:bg-black\/10:hover{background-color:#0000001a}@supports (color:color-mix(in lab, red, red)){.hover\:bg-black\/10:hover{background-color:color-mix(in oklab,var(--color-black)10%,transparent)}}.hover\:bg-blue-700:hover{background-color:var(--color-blue-700)}.hover\:bg-cyan-600:hover{background-color:var(--color-cyan-600)}.hover\:bg-gray-50:hover{background-color:var(--color-gray-50)}.hover\:bg-gray-100:hover{background-color:var(--color-gray-100)}.hover\:bg-gray-200:hover{background-color:var(--color-gray-200)}.hover\:bg-green-600:hover{background-color:var(--color-green-600)}.hover\:bg-green-700:hover{background-color:var(--color-green-700)}.hover\:bg-red-50:hover{background-color:var(--color-red-50)}.hover\:bg-red-700:hover{background-color:var(--color-red-700)}.hover\:text-gray-600:hover{color:var(--color-gray-600)}.hover\:text-gray-700:hover{color:var(--color-gray-700)}.hover\:text-red-700:hover{color:var(--color-red-700)}.hover\:underline:hover{text-decoration-line:underline}.hover\:opacity-90:hover{opacity:.9}}.focus\:ring-2:focus{--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(2px + var(--tw-ring-offset-width))var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.focus\:ring-blue-500:focus{--tw-ring-color:var(--color-blue-500)}.focus\:ring-offset-2:focus{--tw-ring-offset-width:2px;--tw-ring-offset-shadow:var(--tw-ring-inset,)0 0 0 var(--tw-ring-offset-width)var(--tw-ring-offset-color)}.focus\:outline-none:focus{--tw-outline-style:none;outline-style:none}.active\:scale-95:active{--tw-scale-x:95%;--tw-scale-y:95%;--tw-scale-z:95%;scale:var(--tw-scale-x)var(--tw-scale-y)}.disabled\:cursor-not-allowed:disabled{cursor:not-allowed}.disabled\:opacity-50:disabled{opacity:.5}@media (min-width:40rem){.sm\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.sm\:flex-row{flex-direction:row}.sm\:px-6{padding-inline:calc(var(--spacing)*6)}}@media (min-width:48rem){.md\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.md\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.md\:grid-cols-6{grid-template-columns:repeat(6,minmax(0,1fr))}}@media (min-width:64rem){.lg\:grid-cols-6{grid-template-columns:repeat(6,minmax(0,1fr))}.lg\:px-8{padding-inline:calc(var(--spacing)*8)}}:where(.dark\:divide-gray-700:where([data-theme=dark],[data-theme=dark] *)>:not(:last-child)){border-color:var(--color-gray-700)}.dark\:border-blue-800:where([data-theme=dark],[data-theme=dark] *){border-color:var(--color-blue-800)}.dark\:border-gray-600:where([data-theme=dark],[data-theme=dark] *){border-color:var(--color-gray-600)}.dark\:border-gray-700:where([data-theme=dark],[data-theme=dark] *){border-color:var(--color-gray-700)}.dark\:border-green-800:where([data-theme=dark],[data-theme=dark] *){border-color:var(--color-green-800)}.dark\:border-red-800:where([data-theme=dark],[data-theme=dark] *){border-color:var(--color-red-800)}.dark\:border-yellow-800:where([data-theme=dark],[data-theme=dark] *){border-color:var(--color-yellow-800)}.dark\:bg-amber-900\/20:where([data-theme=dark],[data-theme=dark] *){background-color:#7b330633}@supports (color:color-mix(in lab, red, red)){.dark\:bg-amber-900\/20:where([data-theme=dark],[data-theme=dark] *){background-color:color-mix(in oklab,var(--color-amber-900)20%,transparent)}}.dark\:bg-amber-900\/30:where([data-theme=dark],[data-theme=dark] *){background-color:#7b33064d}@supports (color:color-mix(in lab, red, red)){.dark\:bg-amber-900\/30:where([data-theme=dark],[data-theme=dark] *){background-color:color-mix(in oklab,var(--color-amber-900)30%,transparent)}}.dark\:bg-amber-900\/50:where([data-theme=dark],[data-theme=dark] *){background-color:#7b330680}@supports (color:color-mix(in lab, red, red)){.dark\:bg-amber-900\/50:where([data-theme=dark],[data-theme=dark] *){background-color:color-mix(in oklab,var(--color-amber-900)50%,transparent)}}.dark\:bg-blue-800:where([data-theme=dark],[data-theme=dark] *){background-color:var(--color-blue-800)}.dark\:bg-blue-800\/30:where([data-theme=dark],[data-theme=dark] *){background-color:#193cb84d}@supports (color:color-mix(in lab, red, red)){.dark\:bg-blue-800\/30:where([data-theme=dark],[data-theme=dark] *){background-color:color-mix(in oklab,var(--color-blue-800)30%,transparent)}}.dark\:bg-blue-900\/20:where([data-theme=dark],[data-theme=dark] *){background-color:#1c398e33}@supports (color:color-mix(in lab, red, red)){.dark\:bg-blue-900\/20:where([data-theme=dark],[data-theme=dark] *){background-color:color-mix(in oklab,var(--color-blue-900)20%,transparent)}}.dark\:bg-blue-900\/30:where([data-theme=dark],[data-theme=dark] *){background-color:#1c398e4d}@supports (color:color-mix(in lab, red, red)){.dark\:bg-blue-900\/30:where([data-theme=dark],[data-theme=dark] *){background-color:color-mix(in oklab,var(--color-blue-900)30%,transparent)}}.dark\:bg-blue-900\/50:where([data-theme=dark],[data-theme=dark] *){background-color:#1c398e80}@supports (color:color-mix(in lab, red, red)){.dark\:bg-blue-900\/50:where([data-theme=dark],[data-theme=dark] *){background-color:color-mix(in oklab,var(--color-blue-900)50%,transparent)}}.dark\:bg-cyan-900\/30:where([data-theme=dark],[data-theme=dark] *){background-color:#104e644d}@supports (color:color-mix(in lab, red, red)){.dark\:bg-cyan-900\/30:where([data-theme=dark],[data-theme=dark] *){background-color:color-mix(in oklab,var(--color-cyan-900)30%,transparent)}}.dark\:bg-gray-600:where([data-theme=dark],[data-theme=dark] *){background-color:var(--color-gray-600)}.dark\:bg-gray-700:where([data-theme=dark],[data-theme=dark] *){background-color:var(--color-gray-700)}.dark\:bg-gray-800:where([data-theme=dark],[data-theme=dark] *){background-color:var(--color-gray-800)}.dark\:bg-gray-900:where([data-theme=dark],[data-theme=dark] *){background-color:var(--color-gray-900)}.dark\:bg-green-900\/20:where([data-theme=dark],[data-theme=dark] *){background-color:#0d542b33}@supports (color:color-mix(in lab, red, red)){.dark\:bg-green-900\/20:where([data-theme=dark],[data-theme=dark] *){background-color:color-mix(in oklab,var(--color-green-900)20%,transparent)}}.dark\:bg-green-900\/30:where([data-theme=dark],[data-theme=dark] *){background-color:#0d542b4d}@supports (color:color-mix(in lab, red, red)){.dark\:bg-green-900\/30:where([data-theme=dark],[data-theme=dark] *){background-color:color-mix(in oklab,var(--color-green-900)30%,transparent)}}.dark\:bg-green-900\/40:where([data-theme=dark],[data-theme=dark] *){background-color:#0d542b66}@supports (color:color-mix(in lab, red, red)){.dark\:bg-green-900\/40:where([data-theme=dark],[data-theme=dark] *){background-color:color-mix(in oklab,var(--color-green-900)40%,transparent)}}.dark\:bg-green-900\/50:where([data-theme=dark],[data-theme=dark] *){background-color:#0d542b80}@supports (color:color-mix(in lab, red, red)){.dark\:bg-green-900\/50:where([data-theme=dark],[data-theme=dark] *){background-color:color-mix(in oklab,var(--color-green-900)50%,transparent)}}.dark\:bg-indigo-900\/30:where([data-theme=dark],[data-theme=dark] *){background-color:#312c854d}@supports (color:color-mix(in lab, red, red)){.dark\:bg-indigo-900\/30:where([data-theme=dark],[data-theme=dark] *){background-color:color-mix(in oklab,var(--color-indigo-900)30%,transparent)}}.dark\:bg-purple-900\/30:where([data-theme=dark],[data-theme=dark] *){background-color:#59168b4d}@supports (color:color-mix(in lab, red, red)){.dark\:bg-purple-900\/30:where([data-theme=dark],[data-theme=dark] *){background-color:color-mix(in oklab,var(--color-purple-900)30%,transparent)}}.dark\:bg-purple-900\/50:where([data-theme=dark],[data-theme=dark] *){background-color:#59168b80}@supports (color:color-mix(in lab, red, red)){.dark\:bg-purple-900\/50:where([data-theme=dark],[data-theme=dark] *){background-color:color-mix(in oklab,var(--color-purple-900)50%,transparent)}}.dark\:bg-red-800:where([data-theme=dark],[data-theme=dark] *){background-color:var(--color-red-800)}.dark\:bg-red-900\/20:where([data-theme=dark],[data-theme=dark] *){background-color:#82181a33}@supports (color:color-mix(in lab, red, red)){.dark\:bg-red-900\/20:where([data-theme=dark],[data-theme=dark] *){background-color:color-mix(in oklab,var(--color-red-900)20%,transparent)}}.dark\:bg-red-900\/30:where([data-theme=dark],[data-theme=dark] *){background-color:#82181a4d}@supports (color:color-mix(in lab, red, red)){.dark\:bg-red-900\/30:where([data-theme=dark],[data-theme=dark] *){background-color:color-mix(in oklab,var(--color-red-900)30%,transparent)}}.dark\:bg-red-900\/40:where([data-theme=dark],[data-theme=dark] *){background-color:#82181a66}@supports (color:color-mix(in lab, red, red)){.dark\:bg-red-900\/40:where([data-theme=dark],[data-theme=dark] *){background-color:color-mix(in oklab,var(--color-red-900)40%,transparent)}}.dark\:bg-red-900\/50:where([data-theme=dark],[data-theme=dark] *){background-color:#82181a80}@supports (color:color-mix(in lab, red, red)){.dark\:bg-red-900\/50:where([data-theme=dark],[data-theme=dark] *){background-color:color-mix(in oklab,var(--color-red-900)50%,transparent)}}.dark\:bg-yellow-900\/20:where([data-theme=dark],[data-theme=dark] *){background-color:#733e0a33}@supports (color:color-mix(in lab, red, red)){.dark\:bg-yellow-900\/20:where([data-theme=dark],[data-theme=dark] *){background-color:color-mix(in oklab,var(--color-yellow-900)20%,transparent)}}.dark\:bg-yellow-900\/30:where([data-theme=dark],[data-theme=dark] *){background-color:#733e0a4d}@supports (color:color-mix(in lab, red, red)){.dark\:bg-yellow-900\/30:where([data-theme=dark],[data-theme=dark] *){background-color:color-mix(in oklab,var(--color-yellow-900)30%,transparent)}}.dark\:text-amber-200:where([data-theme=dark],[data-theme=dark] *){color:var(--color-amber-200)}.dark\:text-amber-300:where([data-theme=dark],[data-theme=dark] *){color:var(--color-amber-300)}.dark\:text-amber-400:where([data-theme=dark],[data-theme=dark] *){color:var(--color-amber-400)}.dark\:text-blue-200:where([data-theme=dark],[data-theme=dark] *){color:var(--color-blue-200)}.dark\:text-blue-300:where([data-theme=dark],[data-theme=dark] *){color:var(--color-blue-300)}.dark\:text-blue-400:where([data-theme=dark],[data-theme=dark] *){color:var(--color-blue-400)}.dark\:text-cyan-200:where([data-theme=dark],[data-theme=dark] *){color:var(--color-cyan-200)}.dark\:text-gray-100:where([data-theme=dark],[data-theme=dark] *){color:var(--color-gray-100)}.dark\:text-gray-200:where([data-theme=dark],[data-theme=dark] *){color:var(--color-gray-200)}.dark\:text-gray-300:where([data-theme=dark],[data-theme=dark] *){color:var(--color-gray-300)}.dark\:text-gray-400:where([data-theme=dark],[data-theme=dark] *){color:var(--color-gray-400)}.dark\:text-gray-500:where([data-theme=dark],[data-theme=dark] *){color:var(--color-gray-500)}.dark\:text-green-100:where([data-theme=dark],[data-theme=dark] *){color:var(--color-green-100)}.dark\:text-green-200:where([data-theme=dark],[data-theme=dark] *){color:var(--color-green-200)}.dark\:text-green-300:where([data-theme=dark],[data-theme=dark] *){color:var(--color-green-300)}.dark\:text-green-400:where([data-theme=dark],[data-theme=dark] *){color:var(--color-green-400)}.dark\:text-green-500:where([data-theme=dark],[data-theme=dark] *){color:var(--color-green-500)}.dark\:text-purple-200:where([data-theme=dark],[data-theme=dark] *){color:var(--color-purple-200)}.dark\:text-red-200:where([data-theme=dark],[data-theme=dark] *){color:var(--color-red-200)}.dark\:text-red-300:where([data-theme=dark],[data-theme=dark] *){color:var(--color-red-300)}.dark\:text-red-400:where([data-theme=dark],[data-theme=dark] *){color:var(--color-red-400)}.dark\:text-white:where([data-theme=dark],[data-theme=dark] *){color:var(--color-white)}.dark\:text-yellow-300:where([data-theme=dark],[data-theme=dark] *){color:var(--color-yellow-300)}.dark\:even\:bg-gray-800\/50:where([data-theme=dark],[data-theme=dark] *):nth-child(2n){background-color:#1e293980}@supports (color:color-mix(in lab, red, red)){.dark\:even\:bg-gray-800\/50:where([data-theme=dark],[data-theme=dark] *):nth-child(2n){background-color:color-mix(in oklab,var(--color-gray-800)50%,transparent)}}@media (hover:hover){.dark\:hover\:border-gray-500:where([data-theme=dark],[data-theme=dark] *):hover{border-color:var(--color-gray-500)}.dark\:hover\:bg-gray-600:where([data-theme=dark],[data-theme=dark] *):hover{background-color:var(--color-gray-600)}.dark\:hover\:bg-gray-700:where([data-theme=dark],[data-theme=dark] *):hover{background-color:var(--color-gray-700)}.dark\:hover\:bg-gray-700\/50:where([data-theme=dark],[data-theme=dark] *):hover{background-color:#36415380}@supports (color:color-mix(in lab, red, red)){.dark\:hover\:bg-gray-700\/50:where([data-theme=dark],[data-theme=dark] *):hover{background-color:color-mix(in oklab,var(--color-gray-700)50%,transparent)}}.dark\:hover\:bg-gray-800:where([data-theme=dark],[data-theme=dark] *):hover{background-color:var(--color-gray-800)}.dark\:hover\:bg-red-900\/20:where([data-theme=dark],[data-theme=dark] *):hover{background-color:#82181a33}@supports (color:color-mix(in lab, red, red)){.dark\:hover\:bg-red-900\/20:where([data-theme=dark],[data-theme=dark] *):hover{background-color:color-mix(in oklab,var(--color-red-900)20%,transparent)}}.dark\:hover\:text-gray-200:where([data-theme=dark],[data-theme=dark] *):hover{color:var(--color-gray-200)}.dark\:hover\:text-gray-300:where([data-theme=dark],[data-theme=dark] *):hover{color:var(--color-gray-300)}}}@keyframes progress-stripe{0%{background-position:1rem 0}to{background-position:0 0}}.icon{vertical-align:middle;fill:currentColor;width:1em;height:1em;display:inline-block}.icon-lg{width:1.5em;height:1.5em}*,:before,:after{transition:background-color .3s,border-color .3s,color .3s}.btn{text-align:center;cursor:pointer;border:none;border-radius:.375rem;justify-content:center;align-items:center;padding:.5rem 1rem;font-size:.875rem;font-weight:500;line-height:1.25rem;text-decoration:none;transition:all .15s;display:inline-flex}.btn:hover{transform:scale(1.05);box-shadow:0 10px 15px -3px #0000001a,0 4px 6px -4px #0000001a}.btn:active{transform:scale(.95)}.btn:focus{outline-offset:2px;outline:2px solid #0000}.btn:focus-visible{box-shadow:0 0 0 2px #fff,0 0 0 4px #3b82f6}.btn-sm{padding:.375rem .75rem;font-size:.75rem}.btn-icon{vertical-align:middle;min-width:2.25rem;min-height:2.25rem;padding:.5rem .75rem}.btn-blue{color:#fff;background-color:#2563eb}.btn-blue:hover{background-color:#1d4ed8}.btn-green{color:#fff;background-color:#22c55e}.btn-green:hover{background-color:#16a34a}.btn-red{color:#fff;background-color:#ef4444}.btn-red:hover{background-color:#dc2626}.btn-amber{color:#fff;background-color:#f59e0b}.btn-amber:hover{background-color:#d97706}.btn-cyan{color:#fff;background-color:#06b6d4}.btn-cyan:hover{background-color:#0891b2}.btn-purple{color:#fff;background-color:#a855f7}.btn-purple:hover{background-color:#9333ea}.btn-indigo{color:#fff;background-color:#6366f1}.btn-indigo:hover{background-color:#4f46e5}.btn-gray{color:#fff;background-color:#6b7280}.btn-gray:hover{background-color:#4b5563}.btn-secondary{color:#374151;background-color:#e5e7eb}.btn-secondary:hover{background-color:#d1d5db}[data-theme=dark] .btn-secondary{color:#e5e7eb;background-color:#374151}[data-theme=dark] .btn-secondary:hover{background-color:#4b5563}.btn-danger-ghost{color:#b91c1c;background-color:#fee2e2}.btn-danger-ghost:hover{background-color:#fecaca}[data-theme=dark] .btn-danger-ghost{color:#fca5a5;background-color:#7f1d1d4d}[data-theme=dark] .btn-danger-ghost:hover{background-color:#7f1d1d80}.alert{border-style:solid;border-width:1px;border-radius:.5rem;margin-bottom:1rem;padding:.75rem 1rem;font-size:.875rem;line-height:1.25rem}.alert-success{color:#166534;background-color:#dcfce7;border-color:#86efac}[data-theme=dark] .alert-success{color:#bbf7d0;background-color:#22c55e33;border-color:#166534}.alert-error{color:#991b1b;background-color:#fee2e2;border-color:#fca5a5}[data-theme=dark] .alert-error{color:#fecaca;background-color:#ef444433;border-color:#7f1d1d}.alert-warning{color:#92400e;background-color:#fef3c7;border-color:#fcd34d}[data-theme=dark] .alert-warning{color:#fde68a;background-color:#f59e0b33;border-color:#78350f}.alert-info{color:#155e75;background-color:#ecfeff;border-color:#a5f3fc}[data-theme=dark] .alert-info{color:#a5f3fc;background-color:#06b6d433;border-color:#164e63}.alert-lg{border-radius:.75rem;margin-bottom:1.5rem;padding:1.5rem}.card{background-color:#fff;border:1px solid #e5e7eb;border-radius:.5rem;box-shadow:0 1px 2px #0000000d}[data-theme=dark] .card{background-color:#1f2937;border-color:#374151}.card-body{padding:1.25rem}.card-header{text-transform:uppercase;letter-spacing:.05em;color:#6b7280;border-bottom:1px solid #e5e7eb;padding:.75rem 1.5rem;font-size:.75rem;font-weight:600}[data-theme=dark] .card-header{color:#9ca3af;border-color:#374151}.info-box{color:#fff;background:linear-gradient(90deg,#0891b2,#0e7490);border-radius:.75rem;margin-bottom:1.5rem;padding:1rem 1.5rem}[data-theme=dark] .info-box{background:linear-gradient(90deg,#155e75,#164e63)}.stat-box{text-align:center;background-color:#fff;border:1px solid #e5e7eb;border-radius:.5rem;padding:1.25rem;transition:all .2s;box-shadow:0 1px 2px #0000000d}[data-theme=dark] .stat-box{background-color:#1f2937;border-color:#374151}.stat-box:hover{transform:translateY(-.25rem);box-shadow:0 10px 15px -3px #0000001a,0 4px 6px -4px #0000001a}.stat-value{color:#111827;font-size:1.875rem;font-weight:700}[data-theme=dark] .stat-value{color:#f3f4f6}.stat-label{text-transform:uppercase;letter-spacing:.05em;color:#6b7280;margin-top:.5rem;font-size:.75rem}[data-theme=dark] .stat-label{color:#9ca3af}.metric-box{text-align:center;background-color:#f9fafb;border:1px solid #e5e7eb;border-radius:.5rem;padding:.75rem}[data-theme=dark] .metric-box{background-color:#374151;border-color:#4b5563}.metric-label{text-transform:uppercase;letter-spacing:.05em;color:#6b7280;margin-bottom:.25rem;font-size:.75rem;display:block}[data-theme=dark] .metric-label{color:#9ca3af}.metric-value{color:#111827;font-size:.875rem;font-weight:600;display:block}[data-theme=dark] .metric-value{color:#f3f4f6}.table{border-collapse:collapse;background-color:#fff;border-radius:.5rem;width:100%;overflow:hidden;box-shadow:0 1px 2px #0000000d}[data-theme=dark] .table{background-color:#1f2937}.table thead{background-color:#f9fafb}[data-theme=dark] .table thead{background-color:#374151}.table th{text-align:left;text-transform:uppercase;letter-spacing:.05em;color:#6b7280;border-bottom:2px solid #e5e7eb;padding:.75rem 1rem;font-size:.75rem;font-weight:600}[data-theme=dark] .table th{color:#9ca3af;border-color:#4b5563}.table td{color:#111827;border-bottom:1px solid #e5e7eb;padding:.75rem 1rem}[data-theme=dark] .table td{color:#f3f4f6;border-color:#374151}.table tbody tr{transition:background-color .15s}.table tbody tr:hover{background-color:#f9fafb}[data-theme=dark] .table tbody tr:hover{background-color:#374151}.table tbody tr:nth-child(2n){background-color:#f9fafb}[data-theme=dark] .table tbody tr:nth-child(2n){background-color:#1f293780}.badge{border-radius:.25rem;padding:.25rem .5rem;font-size:.75rem;font-weight:500;display:inline-block}.badge-blue{color:#1e40af;background-color:#dbeafe}[data-theme=dark] .badge-blue{color:#93c5fd;background-color:#3b82f64d}.badge-green{color:#166534;background-color:#dcfce7}[data-theme=dark] .badge-green{color:#86efac;background-color:#22c55e4d}.badge-amber{color:#92400e;background-color:#fef3c7}[data-theme=dark] .badge-amber{color:#fcd34d;background-color:#f59e0b4d}.badge-red{color:#991b1b;background-color:#fee2e2}[data-theme=dark] .badge-red{color:#fca5a5;background-color:#ef44444d}.badge-purple{color:#7c3aed;background-color:#f3e8ff}[data-theme=dark] .badge-purple{color:#c4b5fd;background-color:#8b5cf64d}.badge-gray{color:#4b5563;background-color:#f3f4f6}[data-theme=dark] .badge-gray{color:#d1d5db;background-color:#374151}.code{background-color:#e5e7eb;border-radius:.25rem;padding:.125rem .25rem;font-family:ui-monospace,SFMono-Regular,SF Mono,Menlo,Consolas,monospace;font-size:.875rem}[data-theme=dark] .code{background-color:#374151}.modal-overlay{z-index:50;background-color:#00000080;justify-content:center;align-items:center;display:flex;position:fixed;inset:0}.modal-overlay.hidden{display:none}.modal{background-color:#fff;border-radius:.75rem;flex-direction:column;width:100%;max-width:56rem;max-height:90vh;margin:1rem;display:flex;box-shadow:0 25px 50px -12px #00000040}[data-theme=dark] .modal{background-color:#1f2937}.modal-header{border-bottom:1px solid #e5e7eb;justify-content:space-between;align-items:center;padding:1rem 1.5rem;display:flex}[data-theme=dark] .modal-header{border-color:#374151}.modal-title{color:#111827;font-size:1.125rem;font-weight:600}[data-theme=dark] .modal-title{color:#f3f4f6}.modal-subtitle{color:#6b7280;font-size:.875rem}[data-theme=dark] .modal-subtitle{color:#9ca3af}.modal-close{color:#9ca3af;cursor:pointer;background:0 0;border:none;padding:.5rem;transition:color .15s}.modal-close:hover{color:#4b5563}[data-theme=dark] .modal-close:hover{color:#e5e7eb}.modal-body{flex-direction:column;flex:1;padding:1.5rem;display:flex;overflow:hidden}.modal-footer{border-top:1px solid #e5e7eb;justify-content:flex-end;gap:.75rem;padding:1rem 1.5rem;display:flex}[data-theme=dark] .modal-footer{border-color:#374151}.dropzone{text-align:center;cursor:pointer;background-color:#f9fafb;border:2px dashed #d1d5db;border-radius:.75rem;padding:2rem;transition:all .2s}[data-theme=dark] .dropzone{background-color:#1f293780;border-color:#4b5563}.dropzone:hover{background-color:#eff6ff;border-color:#3b82f6;transform:scale(1.01);box-shadow:0 10px 15px -3px #0000001a}[data-theme=dark] .dropzone:hover{background-color:#3b82f61a}.dropzone.drag-over{background-color:#eff6ff;border-color:#3b82f6}[data-theme=dark] .dropzone.drag-over{background-color:#1e3a5f}.progress{background-color:#e5e7eb;border-radius:.375rem;height:.625rem;overflow:hidden}[data-theme=dark] .progress{background-color:#374151}.progress-bar{background:linear-gradient(45deg,#ffffff26 25%,#0000 25% 50%,#ffffff26 50% 75%,#0000 75%,#0000) 0 0/1rem 1rem;border-radius:.375rem;height:100%;transition:width .3s;animation:1s linear infinite progress-stripe}.spinner{border:4px solid #e5e7eb;border-top-color:#3b82f6;border-radius:50%;animation:1s linear infinite spin}.spinner-sm{border-width:2px;width:1.5rem;height:1.5rem}.spinner-md{width:2.5rem;height:2.5rem}.spinner-lg{width:3rem;height:3rem}@keyframes spin{to{transform:rotate(360deg)}}.tooltip-trigger{cursor:help;color:#6b7280;background-color:#e5e7eb;border-radius:9999px;justify-content:center;align-items:center;width:1rem;height:1rem;margin-left:.25rem;font-size:.65rem;display:inline-flex;position:relative}[data-theme=dark] .tooltip-trigger{color:#9ca3af;background-color:#4b5563}.tooltip-content{white-space:nowrap;opacity:0;visibility:hidden;z-index:100;color:#fff;background-color:#1f2937;border-radius:.375rem;max-width:300px;margin-bottom:.5rem;padding:.5rem .75rem;font-size:.75rem;font-weight:400;transition:opacity .2s,visibility .2s;position:absolute;bottom:100%;left:50%;transform:translate(-50%)}[data-theme=dark] .tooltip-content{color:#1f2937;background-color:#f3f4f6}.tooltip-content:after{content:"";border:6px solid #0000;border-top-color:#1f2937;position:absolute;top:100%;left:50%;transform:translate(-50%)}[data-theme=dark] .tooltip-content:after{border-top-color:#f3f4f6}.tooltip-multiline{white-space:normal;text-align:left;line-height:1.4}.tooltip-trigger:hover .tooltip-content,.tooltip-trigger:focus .tooltip-content{opacity:1;visibility:visible}.loading-overlay{z-index:9999;background:#00000080;justify-content:center;align-items:center;display:none;position:fixed;inset:0}.loading-overlay.active{display:flex}.file-upload__input{opacity:0;cursor:pointer;width:100%;height:100%;position:absolute;top:0;left:0}.file-upload__dropzone{position:relative}.file-upload__item{border-radius:.5rem;justify-content:space-between;align-items:center;margin-bottom:.5rem;padding:.75rem 1rem;display:flex}.file-upload__item-info{align-items:center;gap:.75rem;display:flex}.file-upload__item-icon{border-radius:.375rem;justify-content:center;align-items:center;width:2rem;height:2rem;display:flex}.file-upload__item-progress{border-radius:2px;height:4px;margin-top:.5rem;overflow:hidden}.file-upload__item-progress-bar{height:100%;transition:width .3s}details.error-details summary{list-style:none}details.error-details summary::-webkit-details-marker{display:none}details.error-details[open] .error-details__chevron{transform:rotate(180deg)}input:focus,select:focus,textarea:focus{outline:none;box-shadow:0 0 0 2px #fff,0 0 0 4px #3b82f6}[data-theme=dark] input:focus,[data-theme=dark] select:focus,[data-theme=dark] textarea:focus{box-shadow:0 0 0 2px #1f2937,0 0 0 4px #3b82f6}.modal-close:focus{outline:none}.modal-close:focus-visible{border-radius:.375rem;box-shadow:0 0 0 2px #fff,0 0 0 4px #3b82f6}[data-theme=dark] .modal-close:focus-visible{box-shadow:0 0 0 2px #1f2937,0 0 0 4px #3b82f6}.preview-tab:focus{outline:none}.preview-tab:focus-visible{border-radius:.25rem;box-shadow:0 0 0 2px #fff,0 0 0 4px #3b82f6}[data-theme=dark] .preview-tab:focus-visible{box-shadow:0 0 0 2px #1f2937,0 0 0 4px #3b82f6}a:focus{outline:none}a:focus-visible{border-radius:.25rem;box-shadow:0 0 0 2px #fff,0 0 0 4px #3b82f6}[data-theme=dark] a:focus-visible{box-shadow:0 0 0 2px #1f2937,0 0 0 4px #3b82f6}.dropzone:focus-within{border-color:#3b82f6;box-shadow:0 0 0 2px #fff,0 0 0 4px #3b82f6}[data-theme=dark] .dropzone:focus-within{box-shadow:0 0 0 2px #1f2937,0 0 0 4px #3b82f6}details.error-details summary:focus{outline:none}details.error-details summary:focus-visible{border-radius:.375rem;box-shadow:0 0 0 2px #fff,0 0 0 4px #3b82f6}[data-theme=dark] details.error-details summary:focus-visible{box-shadow:0 0 0 2px #1f2937,0 0 0 4px #3b82f6}.btn-blue:focus-visible{box-shadow:0 0 0 2px #fff,0 0 0 4px #fbbf24}[data-theme=dark] .btn-blue:focus-visible{box-shadow:0 0 0 2px #1f2937,0 0 0 4px #fbbf24}::-webkit-scrollbar{width:8px;height:8px}::-webkit-scrollbar-track{background:#f1f1f1;border-radius:4px}::-webkit-scrollbar-thumb{background:#c1c1c1;border-radius:4px}::-webkit-scrollbar-thumb:hover{background:#a1a1a1}[data-theme=dark] ::-webkit-scrollbar-track{background:#2d2d3f}[data-theme=dark] ::-webkit-scrollbar-thumb{background:#4a4a5a}[data-theme=dark] ::-webkit-scrollbar-thumb:hover{background:#5a5a6a}.warning-box{background-color:#fffbeb;border:1px solid #fcd34d;border-radius:.75rem;margin-bottom:1.5rem;padding:1.5rem}[data-theme=dark] .warning-box{background-color:#f59e0b26;border-color:#92400e}.warning-icon{color:#f59e0b}[data-theme=dark] .warning-icon{color:#fbbf24}.warning-details-list{background-color:#fef3c7;border-radius:.5rem;max-height:16rem;padding:.75rem;overflow-y:auto}[data-theme=dark] .warning-details-list{background-color:#f59e0b33}.warning-details-item{border-bottom:1px solid #fcd34d;padding:.5rem 0}.warning-details-item:last-child{border-bottom:none}[data-theme=dark] .warning-details-item{border-color:#92400e}.warning-line-badge{color:#92400e;background-color:#fde68a;border-radius:.25rem;margin-right:.5rem;padding:.125rem .375rem;font-family:ui-monospace,monospace;font-size:.75rem}[data-theme=dark] .warning-line-badge{color:#fef3c7;background-color:#92400e}.warning-message{color:#92400e;font-size:.875rem}[data-theme=dark] .warning-message{color:#fef3c7}.warning-title{color:#92400e;font-size:1.125rem;font-weight:600}[data-theme=dark] .warning-title{color:#fde68a}.warning-subtitle{color:#b45309;margin-top:.25rem;font-size:.875rem}[data-theme=dark] .warning-subtitle{color:#fcd34d}.warning-summary{cursor:pointer;color:#b45309;align-items:center;gap:.5rem;font-size:.875rem;font-weight:500;display:flex}.warning-summary:hover{color:#92400e}[data-theme=dark] .warning-summary{color:#fcd34d}[data-theme=dark] .warning-summary:hover{color:#fde68a}.warning-limit-notice{color:#d97706;margin-top:.5rem;font-size:.75rem;font-style:italic}[data-theme=dark] .warning-limit-notice{color:#fbbf24}.text-muted{color:#6b7280}[data-theme=dark] .text-muted{color:#9ca3af}.flex-center{justify-content:center;align-items:center;display:flex}.container-70{width:100%;max-width:70vw;margin-left:auto;margin-right:auto}@media (max-width:768px){.container-70{max-width:95vw}}@property --tw-translate-x{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-y{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-z{syntax:"*";inherits:false;initial-value:0}@property --tw-rotate-x{syntax:"*";inherits:false}@property --tw-rotate-y{syntax:"*";inherits:false}@property --tw-rotate-z{syntax:"*";inherits:false}@property --tw-skew-x{syntax:"*";inherits:false}@property --tw-skew-y{syntax:"*";inherits:false}@property --tw-space-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-divide-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-gradient-position{syntax:"*";inherits:false}@property --tw-gradient-from{syntax:"";inherits:false;initial-value:#0000}@property --tw-gradient-via{syntax:"";inherits:false;initial-value:#0000}@property --tw-gradient-to{syntax:"";inherits:false;initial-value:#0000}@property --tw-gradient-stops{syntax:"*";inherits:false}@property --tw-gradient-via-stops{syntax:"*";inherits:false}@property --tw-gradient-from-position{syntax:"";inherits:false;initial-value:0%}@property --tw-gradient-via-position{syntax:"";inherits:false;initial-value:50%}@property --tw-gradient-to-position{syntax:"";inherits:false;initial-value:100%}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-tracking{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false}@property --tw-duration{syntax:"*";inherits:false}@property --tw-scale-x{syntax:"*";inherits:false;initial-value:1}@property --tw-scale-y{syntax:"*";inherits:false;initial-value:1}@property --tw-scale-z{syntax:"*";inherits:false;initial-value:1}@keyframes pulse{50%{opacity:.5}} \ No newline at end of file diff --git a/assets/src/css/tailwind.css b/assets/src/css/tailwind.css index 3234c81..9e8247a 100644 --- a/assets/src/css/tailwind.css +++ b/assets/src/css/tailwind.css @@ -1005,6 +1005,120 @@ details.error-details summary:focus-visible { background: #5a5a6a; } +/* ======================================== + WARNING DETAILS (SSE Import Warnings) + ======================================== */ + +.warning-box { + background-color: #fffbeb; + border: 1px solid #fcd34d; + border-radius: 0.75rem; + padding: 1.5rem; + margin-bottom: 1.5rem; +} +[data-theme="dark"] .warning-box { + background-color: rgba(245, 158, 11, 0.15); + border-color: #92400e; +} + +.warning-icon { + color: #f59e0b; +} +[data-theme="dark"] .warning-icon { + color: #fbbf24; +} + +.warning-details-list { + background-color: #fef3c7; + border-radius: 0.5rem; + padding: 0.75rem; + max-height: 16rem; + overflow-y: auto; +} +[data-theme="dark"] .warning-details-list { + background-color: rgba(245, 158, 11, 0.2); +} + +.warning-details-item { + padding: 0.5rem 0; + border-bottom: 1px solid #fcd34d; +} +.warning-details-item:last-child { + border-bottom: none; +} +[data-theme="dark"] .warning-details-item { + border-color: #92400e; +} + +.warning-line-badge { + font-family: ui-monospace, monospace; + font-size: 0.75rem; + background-color: #fde68a; + color: #92400e; + padding: 0.125rem 0.375rem; + border-radius: 0.25rem; + margin-right: 0.5rem; +} +[data-theme="dark"] .warning-line-badge { + background-color: #92400e; + color: #fef3c7; +} + +.warning-message { + font-size: 0.875rem; + color: #92400e; +} +[data-theme="dark"] .warning-message { + color: #fef3c7; +} + +.warning-title { + font-size: 1.125rem; + font-weight: 600; + color: #92400e; +} +[data-theme="dark"] .warning-title { + color: #fde68a; +} + +.warning-subtitle { + font-size: 0.875rem; + color: #b45309; + margin-top: 0.25rem; +} +[data-theme="dark"] .warning-subtitle { + color: #fcd34d; +} + +.warning-summary { + cursor: pointer; + display: flex; + align-items: center; + gap: 0.5rem; + font-size: 0.875rem; + font-weight: 500; + color: #b45309; +} +.warning-summary:hover { + color: #92400e; +} +[data-theme="dark"] .warning-summary { + color: #fcd34d; +} +[data-theme="dark"] .warning-summary:hover { + color: #fde68a; +} + +.warning-limit-notice { + margin-top: 0.5rem; + font-size: 0.75rem; + font-style: italic; + color: #d97706; +} +[data-theme="dark"] .warning-limit-notice { + color: #fbbf24; +} + /* ======================================== UTILITY CLASSES ======================================== */ diff --git a/src/Services/AjaxService.php b/src/Services/AjaxService.php index 2f7f4d1..356abed 100644 --- a/src/Services/AjaxService.php +++ b/src/Services/AjaxService.php @@ -186,39 +186,39 @@ function displaySuccessInPage(stats, warnings, warningsCount, warningsLimitReach var warningsHtml = ''; if (hasWarnings) { var warningItems = warnings.map(function(w) { - return '
  • ' + - 'Line ' + w.line + '' + - '' + escapeHtml(w.message.split('\\n')[0]) + '' + + return '
  • ' + + 'Line ' + w.line + '' + + '' + escapeHtml(w.message.split('\\n')[0]) + '' + '
  • '; }).join(''); var limitNotice = ''; if (warningsLimitReached) { - limitNotice = '
    ' + + limitNotice = '
    ' + '⚠️ Warning limit reached (' + maxWarnings + '). Additional errors may have occurred but were not recorded.' + '
    '; } - warningsHtml = '
    ' + + warningsHtml = '
    ' + '
    ' + '
    ' + - '' + + '' + '' + '' + '
    ' + '
    ' + - '

    ' + displayWarningsCount + ' Warning' + (displayWarningsCount !== 1 ? 's' : '') + '

    ' + - '
    Some SQL statements failed but import continued (continue_on_error enabled).
    ' + + '

    ' + displayWarningsCount + ' Warning' + (displayWarningsCount !== 1 ? 's' : '') + '

    ' + + '
    Some SQL statements failed but import continued (continue_on_error enabled).
    ' + '
    ' + '
    ' + '
    ' + - '' + + '' + '' + '' + '' + 'Show Warning Details' + '' + - '
      ' + warningItems + '
    ' + + '
      ' + warningItems + '
    ' + limitNotice + '
    ' + '
    '; From 5ba3a897031dd78540f2630bc21c0e585c9b26e5 Mon Sep 17 00:00:00 2001 From: bd cannes Date: Sat, 28 Feb 2026 18:38:59 +0100 Subject: [PATCH 08/22] Add 'sse_files' to validActions whitelist in Request::determineAction() Sync the action whitelist with Application::setupRoutes() so that ?action=sse_files requests are no longer silently dropped to 'home'. Co-Authored-By: Claude Opus 4.6 --- src/Core/Request.php | 690 +++++++++++++++++++++---------------------- 1 file changed, 345 insertions(+), 345 deletions(-) diff --git a/src/Core/Request.php b/src/Core/Request.php index a501d02..fce3481 100644 --- a/src/Core/Request.php +++ b/src/Core/Request.php @@ -1,345 +1,345 @@ - - */ - private array $get; - - /** - * Sanitized POST data. - * @var array - */ - private array $post; - - /** - * FILES data. - * @var array - */ - private array $files; - - /** - * SERVER data. - * @var array - */ - private array $server; - - /** - * Requested action. - * @var string - */ - private string $action; - - /** - * Constructor - Initializes the request with superglobals. - */ - public function __construct() - { - $this->get = $this->sanitizeInput($_GET); - $this->post = $this->sanitizeInput($_POST); - $this->files = $_FILES; - $this->server = $_SERVER; - $this->action = $this->determineAction(); - } - - /** - * Sanitizes input data securely. - * - * Unlike the original which removed too many characters, - * this version preserves valid UTF-8 characters while - * removing dangerous control characters. - * - * @param array $data Data to sanitize. - * @return array Sanitized data. - */ - private function sanitizeInput(array $data): array - { - $sanitized = []; - - foreach ($data as $key => $value) { - // Sanitize key - $cleanKey = $this->sanitizeKey($key); - - if (is_array($value)) { - $sanitized[$cleanKey] = $this->sanitizeInput($value); - } else { - $sanitized[$cleanKey] = $this->sanitizeValue((string) $value); - } - } - - return $sanitized; - } - - /** - * Sanitizes a parameter key. - * - * @param string $key Key to sanitize. - * @return string Sanitized key. - */ - private function sanitizeKey(string $key): string - { - // Keys should only contain alphanumeric characters and underscore - return preg_replace('/[^a-zA-Z0-9_]/', '', $key) ?? ''; - } - - /** - * Sanitizes a parameter value. - * - * Preserves valid UTF-8 characters, only removes - * control characters (except tab, newline, carriage return). - * - * @param string $value Value to sanitize. - * @return string Sanitized value. - */ - private function sanitizeValue(string $value): string - { - // Remove control characters except \t, \n, \r - $value = preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/', '', $value) ?? ''; - - // Remove null byte sequences (injection) - $value = str_replace("\0", '', $value); - - return $value; - } - - /** - * Determines the action to execute based on request parameters. - * - * Priority: explicit action > form actions > parameter-based actions - * - * @return string Action name. - */ - private function determineAction(): string - { - // 1. Explicit action parameter takes highest priority (for SSE, AJAX, etc.) - if ($this->has('action')) { - $action = $this->input('action', ''); - // Validate against known actions to prevent injection - $validActions = ['home', 'upload', 'delete', 'import', 'start_import', 'stop_import', 'sse_import', 'drop_restart', 'restart_import', 'preview', 'history', 'files_list']; - if (in_array($action, $validActions, true)) { - return $action; - } - } - - // 2. Form-based actions (button submissions) - if ($this->has('uploadbutton')) { - return 'upload'; - } - - if ($this->has('delete')) { - return 'delete'; - } - - // 3. POST with fn parameter = start import - if ($this->has('fn') && ($_SERVER['REQUEST_METHOD'] ?? '') === 'POST') { - return 'start_import'; - } - - return 'home'; - } - - /** - * Gets a GET value. - * - * @param string $key Parameter key. - * @param mixed $default Default value. - * @return mixed Parameter value. - */ - public function get(string $key, mixed $default = null): mixed - { - return $this->get[$key] ?? $default; - } - - /** - * Gets a POST value. - * - * @param string $key Parameter key. - * @param mixed $default Default value. - * @return mixed Parameter value. - */ - public function post(string $key, mixed $default = null): mixed - { - return $this->post[$key] ?? $default; - } - - /** - * Gets a GET or POST value (GET takes priority). - * - * @param string $key Parameter key. - * @param mixed $default Default value. - * @return mixed Parameter value. - */ - public function input(string $key, mixed $default = null): mixed - { - return $this->get[$key] ?? $this->post[$key] ?? $default; - } - - /** - * Checks if a parameter exists (GET or POST). - * - * @param string $key Parameter key. - * @return bool True if parameter exists. - */ - public function has(string $key): bool - { - return isset($this->get[$key]) || isset($this->post[$key]); - } - - /** - * Gets an integer from parameters. - * - * Safely converts input to integer, rejecting scientific notation - * and values that would overflow PHP's integer range. - * - * @param string $key Parameter key. - * @param int $default Default value. - * @return int Integer value. - */ - public function getInt(string $key, int $default = 0): int - { - $value = $this->input($key); - - if ($value === null) { - return $default; - } - - // Only accept simple integer or decimal strings, reject scientific notation - // This prevents 1e10 being accepted as 10000000000 - if (!is_numeric($value) || preg_match('/[eE]/', (string) $value)) { - return $default; - } - - $floatValue = (float) $value; - - // Check for integer overflow - if ($floatValue > PHP_INT_MAX || $floatValue < PHP_INT_MIN) { - return $default; - } - - return (int) floor($floatValue); - } - - /** - * Gets information about an uploaded file. - * - * @param string $key File key. - * @return array|null File information or null. - */ - public function file(string $key): ?array - { - return $this->files[$key] ?? null; - } - - /** - * Checks if a file has been uploaded correctly. - * - * @param string $key File key. - * @return bool True if file is valid. - */ - public function hasValidFile(string $key): bool - { - $file = $this->file($key); - - if ($file === null) { - return false; - } - - return isset($file['tmp_name']) - && is_uploaded_file($file['tmp_name']) - && ($file['error'] ?? UPLOAD_ERR_NO_FILE) === UPLOAD_ERR_OK; - } - - /** - * Gets the requested action. - * - * @return string Action name. - */ - public function getAction(): string - { - return $this->action; - } - - /** - * Gets a SERVER value. - * - * @param string $key Parameter key. - * @param mixed $default Default value. - * @return mixed Parameter value. - */ - public function server(string $key, mixed $default = null): mixed - { - return $this->server[$key] ?? $default; - } - - /** - * Gets the PHP script name. - * - * @return string Script name. - */ - public function getScriptName(): string - { - return basename($this->server('SCRIPT_FILENAME', 'index.php')); - } - - /** - * Gets the script URI (base path without index.php). - * - * @return string Script URI (e.g., '/bigdump' instead of '/bigdump/index.php'). - */ - public function getScriptUri(): string - { - $phpSelf = $this->server('PHP_SELF', '/index.php'); - // Remove /index.php suffix to get clean base URL - $uri = preg_replace('#/index\.php$#', '', $phpSelf); - - // If empty (app at root), return empty string - callers will handle query params - // e.g., getScriptUri() . '?action=import' = '?action=import' (correct) - return $uri === '' ? '' : $uri; - } - - /** - * Checks if the request is an AJAX request. - * - * @return bool True if AJAX request. - */ - public function isAjax(): bool - { - return $this->has('ajaxrequest') - || strtolower($this->server('HTTP_X_REQUESTED_WITH', '')) === 'xmlhttprequest'; - } - - /** - * Gets all GET data. - * - * @return array GET data. - */ - public function getAllGet(): array - { - return $this->get; - } - - /** - * Gets all POST data. - * - * @return array POST data. - */ - public function getAllPost(): array - { - return $this->post; - } -} + + */ + private array $get; + + /** + * Sanitized POST data. + * @var array + */ + private array $post; + + /** + * FILES data. + * @var array + */ + private array $files; + + /** + * SERVER data. + * @var array + */ + private array $server; + + /** + * Requested action. + * @var string + */ + private string $action; + + /** + * Constructor - Initializes the request with superglobals. + */ + public function __construct() + { + $this->get = $this->sanitizeInput($_GET); + $this->post = $this->sanitizeInput($_POST); + $this->files = $_FILES; + $this->server = $_SERVER; + $this->action = $this->determineAction(); + } + + /** + * Sanitizes input data securely. + * + * Unlike the original which removed too many characters, + * this version preserves valid UTF-8 characters while + * removing dangerous control characters. + * + * @param array $data Data to sanitize. + * @return array Sanitized data. + */ + private function sanitizeInput(array $data): array + { + $sanitized = []; + + foreach ($data as $key => $value) { + // Sanitize key + $cleanKey = $this->sanitizeKey($key); + + if (is_array($value)) { + $sanitized[$cleanKey] = $this->sanitizeInput($value); + } else { + $sanitized[$cleanKey] = $this->sanitizeValue((string) $value); + } + } + + return $sanitized; + } + + /** + * Sanitizes a parameter key. + * + * @param string $key Key to sanitize. + * @return string Sanitized key. + */ + private function sanitizeKey(string $key): string + { + // Keys should only contain alphanumeric characters and underscore + return preg_replace('/[^a-zA-Z0-9_]/', '', $key) ?? ''; + } + + /** + * Sanitizes a parameter value. + * + * Preserves valid UTF-8 characters, only removes + * control characters (except tab, newline, carriage return). + * + * @param string $value Value to sanitize. + * @return string Sanitized value. + */ + private function sanitizeValue(string $value): string + { + // Remove control characters except \t, \n, \r + $value = preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/', '', $value) ?? ''; + + // Remove null byte sequences (injection) + $value = str_replace("\0", '', $value); + + return $value; + } + + /** + * Determines the action to execute based on request parameters. + * + * Priority: explicit action > form actions > parameter-based actions + * + * @return string Action name. + */ + private function determineAction(): string + { + // 1. Explicit action parameter takes highest priority (for SSE, AJAX, etc.) + if ($this->has('action')) { + $action = $this->input('action', ''); + // Validate against known actions to prevent injection + $validActions = ['home', 'upload', 'delete', 'import', 'start_import', 'stop_import', 'sse_import', 'drop_restart', 'restart_import', 'preview', 'history', 'files_list', 'sse_files']; + if (in_array($action, $validActions, true)) { + return $action; + } + } + + // 2. Form-based actions (button submissions) + if ($this->has('uploadbutton')) { + return 'upload'; + } + + if ($this->has('delete')) { + return 'delete'; + } + + // 3. POST with fn parameter = start import + if ($this->has('fn') && ($_SERVER['REQUEST_METHOD'] ?? '') === 'POST') { + return 'start_import'; + } + + return 'home'; + } + + /** + * Gets a GET value. + * + * @param string $key Parameter key. + * @param mixed $default Default value. + * @return mixed Parameter value. + */ + public function get(string $key, mixed $default = null): mixed + { + return $this->get[$key] ?? $default; + } + + /** + * Gets a POST value. + * + * @param string $key Parameter key. + * @param mixed $default Default value. + * @return mixed Parameter value. + */ + public function post(string $key, mixed $default = null): mixed + { + return $this->post[$key] ?? $default; + } + + /** + * Gets a GET or POST value (GET takes priority). + * + * @param string $key Parameter key. + * @param mixed $default Default value. + * @return mixed Parameter value. + */ + public function input(string $key, mixed $default = null): mixed + { + return $this->get[$key] ?? $this->post[$key] ?? $default; + } + + /** + * Checks if a parameter exists (GET or POST). + * + * @param string $key Parameter key. + * @return bool True if parameter exists. + */ + public function has(string $key): bool + { + return isset($this->get[$key]) || isset($this->post[$key]); + } + + /** + * Gets an integer from parameters. + * + * Safely converts input to integer, rejecting scientific notation + * and values that would overflow PHP's integer range. + * + * @param string $key Parameter key. + * @param int $default Default value. + * @return int Integer value. + */ + public function getInt(string $key, int $default = 0): int + { + $value = $this->input($key); + + if ($value === null) { + return $default; + } + + // Only accept simple integer or decimal strings, reject scientific notation + // This prevents 1e10 being accepted as 10000000000 + if (!is_numeric($value) || preg_match('/[eE]/', (string) $value)) { + return $default; + } + + $floatValue = (float) $value; + + // Check for integer overflow + if ($floatValue > PHP_INT_MAX || $floatValue < PHP_INT_MIN) { + return $default; + } + + return (int) floor($floatValue); + } + + /** + * Gets information about an uploaded file. + * + * @param string $key File key. + * @return array|null File information or null. + */ + public function file(string $key): ?array + { + return $this->files[$key] ?? null; + } + + /** + * Checks if a file has been uploaded correctly. + * + * @param string $key File key. + * @return bool True if file is valid. + */ + public function hasValidFile(string $key): bool + { + $file = $this->file($key); + + if ($file === null) { + return false; + } + + return isset($file['tmp_name']) + && is_uploaded_file($file['tmp_name']) + && ($file['error'] ?? UPLOAD_ERR_NO_FILE) === UPLOAD_ERR_OK; + } + + /** + * Gets the requested action. + * + * @return string Action name. + */ + public function getAction(): string + { + return $this->action; + } + + /** + * Gets a SERVER value. + * + * @param string $key Parameter key. + * @param mixed $default Default value. + * @return mixed Parameter value. + */ + public function server(string $key, mixed $default = null): mixed + { + return $this->server[$key] ?? $default; + } + + /** + * Gets the PHP script name. + * + * @return string Script name. + */ + public function getScriptName(): string + { + return basename($this->server('SCRIPT_FILENAME', 'index.php')); + } + + /** + * Gets the script URI (base path without index.php). + * + * @return string Script URI (e.g., '/bigdump' instead of '/bigdump/index.php'). + */ + public function getScriptUri(): string + { + $phpSelf = $this->server('PHP_SELF', '/index.php'); + // Remove /index.php suffix to get clean base URL + $uri = preg_replace('#/index\.php$#', '', $phpSelf); + + // If empty (app at root), return empty string - callers will handle query params + // e.g., getScriptUri() . '?action=import' = '?action=import' (correct) + return $uri === '' ? '' : $uri; + } + + /** + * Checks if the request is an AJAX request. + * + * @return bool True if AJAX request. + */ + public function isAjax(): bool + { + return $this->has('ajaxrequest') + || strtolower($this->server('HTTP_X_REQUESTED_WITH', '')) === 'xmlhttprequest'; + } + + /** + * Gets all GET data. + * + * @return array GET data. + */ + public function getAllGet(): array + { + return $this->get; + } + + /** + * Gets all POST data. + * + * @return array POST data. + */ + public function getAllPost(): array + { + return $this->post; + } +} From 6e3044fb3c31c25414893e56eb33f01facf4bd20 Mon Sep 17 00:00:00 2001 From: bd cannes Date: Sat, 28 Feb 2026 18:43:13 +0100 Subject: [PATCH 09/22] Fix delete button: replace broken POST form with GET link matching controller The form sent 'filename' but BigDumpController::delete() reads 'delete'. Replace with to match both the controller and the pattern already used in filepolling.js. Co-Authored-By: Claude Opus 4.6 --- templates/home.php | 885 ++++++++++++++++++++++----------------------- 1 file changed, 441 insertions(+), 444 deletions(-) diff --git a/templates/home.php b/templates/home.php index 3d5e6a6..825565b 100644 --- a/templates/home.php +++ b/templates/home.php @@ -1,444 +1,441 @@ - - - - - - -
    - Test Mode Enabled - Queries will be parsed but not executed. -
    - - - - -
    e($uploadResult['message']) ?>
    - -
    e($uploadResult['message']) ?>
    - - - - - -
    e($deleteResult['message']) ?>
    - -
    e($deleteResult['message']) ?>
    - - - - - -
    - Database not configured!
    - Please edit config/config.php and set your database credentials. -
    - -
    - Database connection failed!
    - e($connectionInfo['message']) ?> -
    - -
    - Connected to e($dbName) ?> at e($dbServer) ?>
    - Connection charset: e($connectionInfo['charset']) ?> - (Your dump file must use the same charset) -
    - - - -
    - Predefined file: e($predefinedFile) ?>
    -
    - - -
    -
    - - -
    -

    Available Dump Files

    - -
    - - -
    - No dump files found in e($uploadDir) ?>
    - Upload a .sql, .gz or .csv file using the form below, or via FTP. -
    - - -
    - - - - - - - - - - - - - - - - - - - - - -
    FilenameSizeDate AddedTypeActions
    e($file['name']) ?>formatBytes($file['size']) ?>e($file['date']) ?> - 'badge badge-blue', - 'GZip' => 'badge badge-purple', - 'BZ2' => 'badge badge-purple', - 'CSV' => 'badge badge-green', - default => 'badge badge-blue' - }; - ?> - e($file['type']) ?> - - - - - -
    - - -
    - - not supported - - -
    - - - -
    -
    -
    - -
    - -

    Upload Dump Files

    - - -
    - Upload disabled. Directory e($uploadDir) ?> is not writable.
    - Set permissions to 755 or 777, or upload files via FTP. -
    - - -

    - Maximum file size: formatBytes($uploadMaxSize) ?> - (php.ini: upload_max_filesize, post_max_size) - • Allowed types:
    - For larger files, use FTP to upload directly to e($uploadDir) ?> -

    - -
    - -
    -
    - - - -
    -
    - Click to upload or drag and drop -
    -
    - SQL, GZip or CSV files up to formatBytes($uploadMaxSize) ?> -
    - -
    - - -
    - - - - - - -
    - - - - - - -
    -
    -
    -
    Preparing import...
    -
    Loading file
    -
    -
    - - - - - - + + + + + + +
    + Test Mode Enabled - Queries will be parsed but not executed. +
    + + + + +
    e($uploadResult['message']) ?>
    + +
    e($uploadResult['message']) ?>
    + + + + + +
    e($deleteResult['message']) ?>
    + +
    e($deleteResult['message']) ?>
    + + + + + +
    + Database not configured!
    + Please edit config/config.php and set your database credentials. +
    + +
    + Database connection failed!
    + e($connectionInfo['message']) ?> +
    + +
    + Connected to e($dbName) ?> at e($dbServer) ?>
    + Connection charset: e($connectionInfo['charset']) ?> + (Your dump file must use the same charset) +
    + + + +
    + Predefined file: e($predefinedFile) ?>
    +
    + + +
    +
    + + +
    +

    Available Dump Files

    + +
    + + +
    + No dump files found in e($uploadDir) ?>
    + Upload a .sql, .gz or .csv file using the form below, or via FTP. +
    + + +
    + + + + + + + + + + + + + + + + + + + + + +
    FilenameSizeDate AddedTypeActions
    e($file['name']) ?>formatBytes($file['size']) ?>e($file['date']) ?> + 'badge badge-blue', + 'GZip' => 'badge badge-purple', + 'BZ2' => 'badge badge-purple', + 'CSV' => 'badge badge-green', + default => 'badge badge-blue' + }; + ?> + e($file['type']) ?> + + + + + +
    + + +
    + + not supported + + + + + +
    +
    + +
    + +

    Upload Dump Files

    + + +
    + Upload disabled. Directory e($uploadDir) ?> is not writable.
    + Set permissions to 755 or 777, or upload files via FTP. +
    + + +

    + Maximum file size: formatBytes($uploadMaxSize) ?> + (php.ini: upload_max_filesize, post_max_size) + • Allowed types:
    + For larger files, use FTP to upload directly to e($uploadDir) ?> +

    + +
    + +
    +
    + + + +
    +
    + Click to upload or drag and drop +
    +
    + SQL, GZip or CSV files up to formatBytes($uploadMaxSize) ?> +
    + +
    + + +
    + + + + + + +
    + + + + + + +
    +
    +
    +
    Preparing import...
    +
    Loading file
    +
    +
    + + + + + + From e4acd3162ecf744a11b3409f95b4f844bfc2b125 Mon Sep 17 00:00:00 2001 From: bd cannes Date: Sat, 28 Feb 2026 18:48:18 +0100 Subject: [PATCH 10/22] Fix getScriptUri() to strip .phar entry points in addition to index.php The regex now matches both /index.php and /anything.phar so that PHAR-mode requests get a clean base URI for link generation. Adds RequestTest.php with 8 tests covering index.php, .phar, nested paths, and non-matching paths. Co-Authored-By: Claude Opus 4.6 --- src/Core/Request.php | 6 +-- tests/RequestTest.php | 100 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 103 insertions(+), 3 deletions(-) create mode 100644 tests/RequestTest.php diff --git a/src/Core/Request.php b/src/Core/Request.php index fce3481..2e29e44 100644 --- a/src/Core/Request.php +++ b/src/Core/Request.php @@ -297,15 +297,15 @@ public function getScriptName(): string } /** - * Gets the script URI (base path without index.php). + * Gets the script URI (base path without index.php or .phar entry point). * * @return string Script URI (e.g., '/bigdump' instead of '/bigdump/index.php'). */ public function getScriptUri(): string { $phpSelf = $this->server('PHP_SELF', '/index.php'); - // Remove /index.php suffix to get clean base URL - $uri = preg_replace('#/index\.php$#', '', $phpSelf); + // Remove /index.php or /anything.phar suffix to get clean base URL + $uri = preg_replace('#/(index\.php|[^/]+\.phar)$#', '', $phpSelf); // If empty (app at root), return empty string - callers will handle query params // e.g., getScriptUri() . '?action=import' = '?action=import' (correct) diff --git a/tests/RequestTest.php b/tests/RequestTest.php new file mode 100644 index 0000000..9dd2916 --- /dev/null +++ b/tests/RequestTest.php @@ -0,0 +1,100 @@ +test('getScriptUri() strips /index.php from subdirectory path', function () use ($runner) { + $request = makeRequestWithPhpSelf('/bigdump/index.php'); + $runner->assertEquals('/bigdump', $request->getScriptUri()); +}); + +// Test 2: index.php at root +$runner->test('getScriptUri() returns empty string for /index.php at root', function () use ($runner) { + $request = makeRequestWithPhpSelf('/index.php'); + $runner->assertEquals('', $request->getScriptUri()); +}); + +// Test 3: PHAR file at subdirectory +$runner->test('getScriptUri() strips /bigdump.phar from subdirectory path', function () use ($runner) { + $request = makeRequestWithPhpSelf('/tools/bigdump.phar'); + $runner->assertEquals('/tools', $request->getScriptUri()); +}); + +// Test 4: PHAR file at root +$runner->test('getScriptUri() returns empty string for /bigdump.phar at root', function () use ($runner) { + $request = makeRequestWithPhpSelf('/bigdump.phar'); + $runner->assertEquals('', $request->getScriptUri()); +}); + +// Test 5: PHAR with different name +$runner->test('getScriptUri() strips any .phar filename', function () use ($runner) { + $request = makeRequestWithPhpSelf('/subdir/my-app-v2.28.phar'); + $runner->assertEquals('/subdir', $request->getScriptUri()); +}); + +// Test 6: Deep nested path with index.php +$runner->test('getScriptUri() handles deep nested paths with index.php', function () use ($runner) { + $request = makeRequestWithPhpSelf('/a/b/c/index.php'); + $runner->assertEquals('/a/b/c', $request->getScriptUri()); +}); + +// Test 7: Deep nested path with .phar +$runner->test('getScriptUri() handles deep nested paths with .phar', function () use ($runner) { + $request = makeRequestWithPhpSelf('/a/b/c/dump.phar'); + $runner->assertEquals('/a/b/c', $request->getScriptUri()); +}); + +// Test 8: Path without index.php or .phar is left untouched +$runner->test('getScriptUri() leaves non-matching path untouched', function () use ($runner) { + $request = makeRequestWithPhpSelf('/bigdump/other.php'); + $runner->assertEquals('/bigdump/other.php', $request->getScriptUri()); +}); + +echo "\n"; +exit($runner->summary()); From 5411eedc8b16efeac40f54763619cb0f33f78626 Mon Sep 17 00:00:00 2001 From: bd cannes Date: Sat, 28 Feb 2026 18:49:21 +0100 Subject: [PATCH 11/22] Fix PHAR home link: use $scriptName instead of $scriptUri $scriptUri resolves to the parent directory (/tools) which breaks navigation in PHAR mode. $scriptName points to the PHAR file itself (bigdump.phar), giving a correct home link. Co-Authored-By: Claude Opus 4.6 --- templates/layout_phar.php | 244 +++++++++++++++++++------------------- 1 file changed, 122 insertions(+), 122 deletions(-) diff --git a/templates/layout_phar.php b/templates/layout_phar.php index 0f7ed0c..dbc96f4 100644 --- a/templates/layout_phar.php +++ b/templates/layout_phar.php @@ -1,122 +1,122 @@ - - - - - - - - - - BigDump v<?= $view->e($version) ?> - MySQL Dump Importer (PHAR Mode) - - - - - - - -
    - getInlinedIcons() ?> -
    - -
    - -
    - -
    - - -
    - content() ?> -
    - - -
    - BigDump ve($version) ?> - PHAR Edition
    - Original by Alexey Ozerov (2003) | Refactored with improved stability and security | Made by w3spi5 (wespify.com) -
    -
    - - - - - + + + + + + + + + + BigDump v<?= $view->e($version) ?> - MySQL Dump Importer (PHAR Mode) + + + + + + + +
    + getInlinedIcons() ?> +
    + +
    + +
    + +
    + + +
    + content() ?> +
    + + +
    + BigDump ve($version) ?> - PHAR Edition
    + Original by Alexey Ozerov (2003) | Refactored with improved stability and security | Made by w3spi5 (wespify.com) +
    +
    + + + + + From f38f3514dd04879ed2902a76ecfee6affbebb8d9 Mon Sep 17 00:00:00 2001 From: bd cannes Date: Sun, 1 Mar 2026 01:40:07 +0100 Subject: [PATCH 12/22] Add View::iconRef() helper for PHAR-compatible SVG icon references In PHAR mode icons are inlined in the DOM, so elements must reference fragments (#id) instead of external files (assets/icons2.svg#id). - Add iconRef(string $id) to View.php - Replace all 15 hardcoded icon paths in home.php with the helper Co-Authored-By: Claude Opus 4.6 --- src/Core/View.php | 14 ++++++++++++++ templates/home.php | 28 ++++++++++++++-------------- 2 files changed, 28 insertions(+), 14 deletions(-) diff --git a/src/Core/View.php b/src/Core/View.php index 9c23751..01e7c15 100644 --- a/src/Core/View.php +++ b/src/Core/View.php @@ -453,4 +453,18 @@ public function has(string $key): bool { return isset($this->data[$key]) && $this->data[$key] !== ''; } + + /** + * Returns the correct SVG icon href for a element. + * + * In PHAR mode, icons are inlined in the DOM so we reference by fragment only (#id). + * In normal mode, we reference the external sprite file (assets/icons2.svg#id). + * + * @param string $id Icon identifier (e.g., 'eye', 'trash', 'spinner'). + * @return string The href value for a element. + */ + public function iconRef(string $id): string + { + return $this->isPharMode ? '#' . $id : 'assets/icons2.svg#' . $id; + } } diff --git a/templates/home.php b/templates/home.php index 825565b..d8c2716 100644 --- a/templates/home.php +++ b/templates/home.php @@ -88,7 +88,7 @@

    Available Dump Files

    @@ -147,13 +147,13 @@ onclick="previewFile('escapeJs($file['name']) ?>')" class="btn btn-icon btn-purple" title="Preview SQL content"> - +
    + onsubmit="this.querySelector('button').disabled=true; this.querySelector('button').classList.add('opacity-50','cursor-not-allowed'); this.querySelector('.btn-icon-swap').innerHTML='iconRef('spinner') ?>">'; this.querySelector('.btn-icon-swap').classList.add('animate-spin');">
    @@ -161,9 +161,9 @@ class="btn btn-icon btn-purple" - + @@ -279,7 +279,7 @@ class="hidden"
    @@ -296,7 +296,7 @@ class="hidden" @@ -326,10 +326,10 @@ class="hidden"
    @@ -349,7 +349,7 @@ class="hidden"
    @@ -368,12 +368,12 @@ class="hidden" @@ -431,7 +431,7 @@ class="hidden" ' + + '' + '
    ' + '
    ' + '
    ' + queriesDone + '
    ' + From a213539a774294a49e3ccc047312acfe97465900 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C6=90=C9=94=C4=B1s3?= <8407711+w3spi5@users.noreply.github.com> Date: Wed, 28 Jan 2026 14:18:11 +0100 Subject: [PATCH 14/22] Some design and bugs corrections --- assets/dist/bigdump.min.js | 2 +- assets/dist/celebration.min.js | 466 ++++++++++++++++++++++++++++ assets/dist/favicon-animator.min.js | 145 +++++++++ assets/dist/filepolling.min.js | 2 +- assets/js/bigdump.js | 43 +++ assets/js/filepolling.js | 21 +- assets/src/js/bigdump.js | 43 +++ assets/src/js/filepolling.js | 21 +- src/Core/Application.php | 2 +- src/Core/View.php | 2 + src/Models/FileHandler.php | 10 +- src/Services/AjaxService.php | 46 +-- templates/error.php | 6 +- templates/home.php | 2 +- templates/import.php | 9 +- templates/layout.php | 2 + 16 files changed, 788 insertions(+), 34 deletions(-) create mode 100644 assets/dist/celebration.min.js create mode 100644 assets/dist/favicon-animator.min.js diff --git a/assets/dist/bigdump.min.js b/assets/dist/bigdump.min.js index bc05420..98fa28c 100644 --- a/assets/dist/bigdump.min.js +++ b/assets/dist/bigdump.min.js @@ -1 +1 @@ -(function(){"use strict";function o(){var e=document.getElementById("loadingOverlay");e&&document.querySelectorAll("form").forEach(function(t){t.querySelector('input[name="fn"]')&&t.addEventListener("submit",function(i){e.classList.add("active");var n=t.querySelector('input[name="fn"]');if(n){var u=n.value,r=e.querySelector(".loading-subtext");r&&(r.textContent="Loading "+u)}})})}function d(){var e=document.getElementById("darkModeToggle");e&&e.addEventListener("click",function(){var t=document.documentElement,i=t.getAttribute("data-theme")||"light",n=i==="dark"?"light":"dark";t.setAttribute("data-theme",n),localStorage.setItem("bigdump-theme",n)})}function a(){o(),d()}document.readyState==="loading"?document.addEventListener("DOMContentLoaded",a):a()})(); +(function(){"use strict";function o(){var e=document.getElementById("loadingOverlay");e&&document.querySelectorAll("form").forEach(function(t){t.querySelector('input[name="fn"]')&&t.addEventListener("submit",function(i){e.classList.add("active");var n=t.querySelector('input[name="fn"]');if(n){var u=n.value,r=e.querySelector(".loading-subtext");r&&(r.textContent="Loading "+u)}})})}function d(){var e=document.getElementById("darkModeToggle");e&&e.addEventListener("click",function(){var t=document.documentElement,i=t.getAttribute("data-theme")||"light",n=i==="dark"?"light":"dark";t.setAttribute("data-theme",n),localStorage.setItem("bigdump-theme",n)})}function c(e,t){e.disabled=!0,e.classList.add("opacity-50","cursor-not-allowed"),e.innerHTML=''+t}function b(){document.querySelectorAll("form").forEach(function(e){var t=e.querySelector('input[name="fn"]'),i=e.querySelector('input[name="action"]'),n=e.querySelector('button[type="submit"]');n&&(t&&(!i||i.value!=="delete")&&e.addEventListener("submit",function(){c(n,"Importing...")}),i&&i.value==="delete"&&e.addEventListener("submit",function(u){c(n,"Deleting...")}))})}function a(){o(),d(),b()}document.readyState==="loading"?document.addEventListener("DOMContentLoaded",a):a()})(); diff --git a/assets/dist/celebration.min.js b/assets/dist/celebration.min.js new file mode 100644 index 0000000..5d80a74 --- /dev/null +++ b/assets/dist/celebration.min.js @@ -0,0 +1,466 @@ +/** + * BigDump - Celebration Effects Module (Enhanced) + * Creates realistic fireworks and confetti animations on import completion. + * Features: particle trails, glow effects, 3D confetti rotation, air resistance. + */ +(function() { + 'use strict'; + + var canvas = null; + var ctx = null; + var animationId = null; + var particles = []; + var confetti = []; + var trails = []; + var isRunning = false; + var startTime = 0; + var fireworksDuration = 20000; // 20 seconds of fireworks + var confettiForever = true; // Confetti falls indefinitely + + // Vibrant colors for fireworks + var fireworkPalettes = [ + ['#FF6B6B', '#FF8E8E', '#FFB4B4'], // Red + ['#4ECDC4', '#7EDCD6', '#A8EBE7'], // Teal + ['#45B7D1', '#6FC9DE', '#99DBEB'], // Blue + ['#FFD93D', '#FFE469', '#FFEF9A'], // Yellow + ['#6BCB77', '#8DD896', '#AFE5B5'], // Green + ['#FF8C00', '#FFA533', '#FFBE66'], // Orange + ['#DA70D6', '#E494E0', '#EEB8EA'], // Orchid + ['#9B59B6', '#B07CC6', '#C59FD6'], // Purple + ['#E74C3C', '#ED7669', '#F3A096'], // Coral + ['#3498DB', '#5DADE2', '#85C1E9'] // Sky blue + ]; + + var confettiColors = [ + '#FF6B6B', '#4ECDC4', '#45B7D1', '#FFD93D', '#6BCB77', + '#FF8C00', '#DA70D6', '#9B59B6', '#E74C3C', '#3498DB', + '#F39C12', '#1ABC9C', '#E91E63', '#00BCD4', '#8BC34A' + ]; + + /** + * Enhanced Particle class with trails and glow + */ + function Particle(x, y, color, velocity, options) { + options = options || {}; + this.x = x; + this.y = y; + this.color = color; + this.velocity = velocity; + this.gravity = options.gravity || 0.06; + this.decay = options.decay || 0.015; + this.alpha = 1; + this.size = options.size || 3; + this.hasTrail = options.hasTrail !== false; + this.glowSize = options.glowSize || this.size * 2; + this.friction = options.friction || 0.98; + this.trail = []; + this.maxTrailLength = options.trailLength || 5; + } + + Particle.prototype.update = function() { + // Store trail position + if (this.hasTrail && this.alpha > 0.3) { + this.trail.push({ x: this.x, y: this.y, alpha: this.alpha }); + if (this.trail.length > this.maxTrailLength) { + this.trail.shift(); + } + } + + this.velocity.x *= this.friction; + this.velocity.y *= this.friction; + this.velocity.y += this.gravity; + this.x += this.velocity.x; + this.y += this.velocity.y; + this.alpha -= this.decay; + return this.alpha > 0; + }; + + Particle.prototype.draw = function(ctx) { + // Draw trail + if (this.hasTrail && this.trail.length > 0) { + for (var i = 0; i < this.trail.length; i++) { + var t = this.trail[i]; + var trailAlpha = (i / this.trail.length) * this.alpha * 0.5; + var trailSize = this.size * (0.3 + (i / this.trail.length) * 0.5); + ctx.save(); + ctx.globalAlpha = trailAlpha; + ctx.beginPath(); + ctx.arc(t.x, t.y, trailSize, 0, Math.PI * 2); + ctx.fillStyle = this.color; + ctx.fill(); + ctx.restore(); + } + } + + // Draw glow + ctx.save(); + ctx.globalAlpha = this.alpha * 0.4; + ctx.beginPath(); + ctx.arc(this.x, this.y, this.glowSize, 0, Math.PI * 2); + var gradient = ctx.createRadialGradient(this.x, this.y, 0, this.x, this.y, this.glowSize); + gradient.addColorStop(0, this.color); + gradient.addColorStop(1, 'transparent'); + ctx.fillStyle = gradient; + ctx.fill(); + ctx.restore(); + + // Draw core + ctx.save(); + ctx.globalAlpha = this.alpha; + ctx.beginPath(); + ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2); + ctx.fillStyle = this.color; + ctx.fill(); + + // Bright center + ctx.beginPath(); + ctx.arc(this.x, this.y, this.size * 0.5, 0, Math.PI * 2); + ctx.fillStyle = '#FFFFFF'; + ctx.globalAlpha = this.alpha * 0.8; + ctx.fill(); + ctx.restore(); + }; + + /** + * Enhanced Confetti with 3D rotation and air resistance + */ + function ConfettiPiece(x, y, color) { + this.x = x; + this.y = y; + this.color = color; + this.width = Math.random() * 12 + 6; + this.height = Math.random() * 8 + 4; + this.velocity = { + x: (Math.random() - 0.5) * 8, + y: Math.random() * 2 + 1 + }; + // 3D rotation angles + this.rotationX = Math.random() * 360; + this.rotationY = Math.random() * 360; + this.rotationZ = Math.random() * 360; + this.rotationSpeedX = (Math.random() - 0.5) * 8; + this.rotationSpeedY = (Math.random() - 0.5) * 12; + this.rotationSpeedZ = (Math.random() - 0.5) * 6; + // Oscillation (swaying) + this.oscillationSpeed = Math.random() * 0.08 + 0.03; + this.oscillationDistance = Math.random() * 60 + 30; + this.initialX = x; + this.time = Math.random() * 100; + // Air resistance + this.drag = 0.98 + Math.random() * 0.015; + this.terminalVelocity = 4 + Math.random() * 2; + } + + ConfettiPiece.prototype.update = function(canvasHeight) { + this.time += this.oscillationSpeed; + + // Horizontal oscillation (swaying in wind) + this.x = this.initialX + Math.sin(this.time) * this.oscillationDistance; + this.initialX += this.velocity.x * 0.1; // Drift + + // Apply drag + this.velocity.x *= this.drag; + this.velocity.y += 0.08; // Gravity + + // Terminal velocity + if (this.velocity.y > this.terminalVelocity) { + this.velocity.y = this.terminalVelocity; + } + + this.y += this.velocity.y; + + // Tumbling rotation + this.rotationX += this.rotationSpeedX; + this.rotationY += this.rotationSpeedY; + this.rotationZ += this.rotationSpeedZ; + + // Slow down rotation over time + this.rotationSpeedX *= 0.999; + this.rotationSpeedY *= 0.999; + + return this.y < canvasHeight + 50; + }; + + ConfettiPiece.prototype.draw = function(ctx) { + ctx.save(); + ctx.translate(this.x, this.y); + + // Apply 3D-like rotation (fake perspective) + var scaleX = Math.cos(this.rotationY * Math.PI / 180); + var scaleY = Math.cos(this.rotationX * Math.PI / 180); + ctx.rotate(this.rotationZ * Math.PI / 180); + ctx.scale(scaleX, scaleY); + + // Draw confetti piece with slight shadow + ctx.shadowColor = 'rgba(0,0,0,0.2)'; + ctx.shadowBlur = 2; + ctx.shadowOffsetY = 1; + + ctx.fillStyle = this.color; + ctx.fillRect(-this.width / 2, -this.height / 2, this.width, this.height); + + // Add shine effect + ctx.fillStyle = 'rgba(255,255,255,0.3)'; + ctx.fillRect(-this.width / 2, -this.height / 2, this.width * 0.4, this.height); + + ctx.restore(); + }; + + /** + * Create an enhanced firework explosion + */ + function createFirework(x, y) { + var palette = fireworkPalettes[Math.floor(Math.random() * fireworkPalettes.length)]; + var particleCount = 60 + Math.floor(Math.random() * 40); + var burstSpeed = 4 + Math.random() * 3; + + // Main explosion particles + for (var i = 0; i < particleCount; i++) { + var angle = (Math.PI * 2 / particleCount) * i + (Math.random() - 0.5) * 0.2; + var speed = burstSpeed * (0.6 + Math.random() * 0.8); + var colorIndex = Math.floor(Math.random() * palette.length); + var velocity = { + x: Math.cos(angle) * speed, + y: Math.sin(angle) * speed + }; + particles.push(new Particle(x, y, palette[colorIndex], velocity, { + gravity: 0.05, + decay: 0.010 + Math.random() * 0.008, + size: 2 + Math.random() * 2, + trailLength: 4, + glowSize: 6 + })); + } + + // Inner ring (brighter, faster decay) + var innerCount = 20 + Math.floor(Math.random() * 15); + for (var j = 0; j < innerCount; j++) { + var innerAngle = Math.random() * Math.PI * 2; + var innerSpeed = burstSpeed * 1.3 * Math.random(); + var innerVelocity = { + x: Math.cos(innerAngle) * innerSpeed, + y: Math.sin(innerAngle) * innerSpeed + }; + particles.push(new Particle(x, y, '#FFFFFF', innerVelocity, { + gravity: 0.04, + decay: 0.025, + size: 1.5, + trailLength: 3, + glowSize: 4, + hasTrail: true + })); + } + + // Sparkle particles (twinkle effect) + var sparkleCount = 15 + Math.floor(Math.random() * 10); + for (var k = 0; k < sparkleCount; k++) { + var sparkAngle = Math.random() * Math.PI * 2; + var sparkSpeed = burstSpeed * 0.3 + Math.random() * burstSpeed * 0.8; + var sparkVelocity = { + x: Math.cos(sparkAngle) * sparkSpeed, + y: Math.sin(sparkAngle) * sparkSpeed + }; + particles.push(new Particle(x, y, '#FFFACD', sparkVelocity, { + gravity: 0.08, + decay: 0.03 + Math.random() * 0.02, + size: 1, + glowSize: 3, + hasTrail: false + })); + } + } + + /** + * Create confetti burst from top + */ + function createConfettiBurst(count) { + if (!canvas) return; + + for (var i = 0; i < count; i++) { + var x = Math.random() * canvas.width; + var color = confettiColors[Math.floor(Math.random() * confettiColors.length)]; + confetti.push(new ConfettiPiece(x, -20 - Math.random() * 50, color)); + } + } + + /** + * Create confetti cannon from sides + */ + function createConfettiCannon(fromLeft) { + if (!canvas) return; + + var startX = fromLeft ? -10 : canvas.width + 10; + var angle = fromLeft ? -Math.PI / 4 : -Math.PI * 3 / 4; + + for (var i = 0; i < 30; i++) { + var spread = (Math.random() - 0.5) * 0.5; + var speed = 8 + Math.random() * 6; + var color = confettiColors[Math.floor(Math.random() * confettiColors.length)]; + var piece = new ConfettiPiece(startX, canvas.height * 0.7, color); + piece.velocity.x = Math.cos(angle + spread) * speed; + piece.velocity.y = Math.sin(angle + spread) * speed; + piece.initialX = startX; + confetti.push(piece); + } + } + + /** + * Initialize canvas + */ + function init() { + if (canvas) return; + + canvas = document.createElement('canvas'); + canvas.id = 'celebration-canvas'; + canvas.style.cssText = 'position: fixed; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; z-index: 9999;'; + document.body.appendChild(canvas); + + ctx = canvas.getContext('2d'); + resizeCanvas(); + + window.addEventListener('resize', resizeCanvas); + } + + /** + * Resize canvas to match window + */ + function resizeCanvas() { + if (!canvas) return; + canvas.width = window.innerWidth; + canvas.height = window.innerHeight; + } + + /** + * Animation loop + */ + function animate() { + if (!isRunning) return; + + var elapsed = Date.now() - startTime; + + // Clear canvas with slight fade for trail effect + ctx.fillStyle = 'rgba(0, 0, 0, 0.1)'; + ctx.fillRect(0, 0, canvas.width, canvas.height); + ctx.clearRect(0, 0, canvas.width, canvas.height); + + // Update and draw particles + particles = particles.filter(function(p) { + var alive = p.update(); + if (alive) p.draw(ctx); + return alive; + }); + + // Update and draw confetti + confetti = confetti.filter(function(c) { + var alive = c.update(canvas.height); + if (alive) c.draw(ctx); + return alive; + }); + + // Create new fireworks periodically for 20 seconds + if (elapsed < fireworksDuration) { + // More frequent fireworks at start, gradually decreasing + var fireworkChance = elapsed < 5000 ? 0.08 : (elapsed < 10000 ? 0.05 : 0.03); + if (Math.random() < fireworkChance) { + var x = Math.random() * canvas.width * 0.7 + canvas.width * 0.15; + var y = Math.random() * canvas.height * 0.4 + canvas.height * 0.08; + createFirework(x, y); + } + } + + // Add confetti continuously forever (or until stopped) + if (confettiForever && Math.random() < 0.15) { + createConfettiBurst(1); + } + + // Confetti cannons at specific intervals during first 15 seconds + if (elapsed < 15000) { + var cannonInterval = 2000; // Every 2 seconds + var cannonWindow = 30; + if (elapsed % cannonInterval < cannonWindow && Math.random() < 0.5) { + createConfettiCannon(Math.random() < 0.5); + } + } + + // Continue animation as long as running (confetti forever!) + if (isRunning) { + animationId = requestAnimationFrame(animate); + } else { + cleanup(); + } + } + + /** + * Clean up canvas and reset state + */ + function cleanup() { + isRunning = false; + if (animationId) { + cancelAnimationFrame(animationId); + animationId = null; + } + if (canvas && canvas.parentNode) { + canvas.parentNode.removeChild(canvas); + } + canvas = null; + ctx = null; + particles = []; + confetti = []; + trails = []; + window.removeEventListener('resize', resizeCanvas); + } + + /** + * Start the celebration effects + */ + function start() { + if (isRunning) return; + + init(); + isRunning = true; + startTime = Date.now(); + particles = []; + confetti = []; + trails = []; + + // Initial burst of fireworks (staggered) + var fireworkPositions = [ + { x: 0.3, y: 0.25, delay: 0 }, + { x: 0.7, y: 0.2, delay: 150 }, + { x: 0.5, y: 0.15, delay: 300 }, + { x: 0.25, y: 0.3, delay: 450 }, + { x: 0.75, y: 0.28, delay: 600 } + ]; + + fireworkPositions.forEach(function(pos) { + setTimeout(function() { + if (!isRunning || !canvas) return; + createFirework(canvas.width * pos.x, canvas.height * pos.y); + }, pos.delay); + }); + + // Initial confetti burst + createConfettiBurst(80); + + animate(); + console.log('Celebration started!'); + } + + /** + * Stop the celebration effects immediately + */ + function stop() { + if (!isRunning) return; + cleanup(); + console.log('Celebration stopped'); + } + + // Expose API + window.BigDump = window.BigDump || {}; + window.BigDump.celebration = { + start: start, + stop: stop, + isRunning: function() { return isRunning; } + }; + +})(); diff --git a/assets/dist/favicon-animator.min.js b/assets/dist/favicon-animator.min.js new file mode 100644 index 0000000..7a03b6c --- /dev/null +++ b/assets/dist/favicon-animator.min.js @@ -0,0 +1,145 @@ +/** + * BigDump - Animated Favicon Module + * Creates a bouncing arrow animation during import process. + * Uses Canvas API to dynamically generate favicon frames. + */ +(function() { + 'use strict'; + + var canvas = null; + var ctx = null; + var animationId = null; + var originalFavicon = null; + var linkElement = null; + var frame = 0; + var isAnimating = false; + + // Animation settings + var SIZE = 32; // Favicon size + var ARROW_COLOR = '#3B82F6'; // Blue-500 + var BG_COLOR = '#1E3A5F'; // Dark blue background + var BOUNCE_HEIGHT = 6; // Pixels to bounce + var FRAME_RATE = 60; // ms between frames + + /** + * Initialize canvas and get original favicon + */ + function init() { + if (canvas) return; + + canvas = document.createElement('canvas'); + canvas.width = SIZE; + canvas.height = SIZE; + ctx = canvas.getContext('2d'); + + // Find and store original favicon + linkElement = document.querySelector('link[rel="icon"][type="image/png"]') || + document.querySelector('link[rel="icon"]'); + + if (linkElement) { + originalFavicon = linkElement.href; + } + } + + /** + * Draw a frame of the bouncing arrow animation + * @param {number} offset - Vertical bounce offset + */ + function drawFrame(offset) { + // Clear canvas + ctx.clearRect(0, 0, SIZE, SIZE); + + // Draw circular background + ctx.beginPath(); + ctx.arc(SIZE / 2, SIZE / 2, SIZE / 2 - 1, 0, Math.PI * 2); + ctx.fillStyle = BG_COLOR; + ctx.fill(); + + // Calculate arrow position with bounce + var centerX = SIZE / 2; + var centerY = SIZE / 2 + offset; + + // Draw downward arrow + ctx.beginPath(); + ctx.strokeStyle = ARROW_COLOR; + ctx.fillStyle = ARROW_COLOR; + ctx.lineWidth = 3; + ctx.lineCap = 'round'; + ctx.lineJoin = 'round'; + + // Arrow shaft + ctx.moveTo(centerX, centerY - 8); + ctx.lineTo(centerX, centerY + 4); + ctx.stroke(); + + // Arrow head (triangle) + ctx.beginPath(); + ctx.moveTo(centerX - 6, centerY); + ctx.lineTo(centerX, centerY + 8); + ctx.lineTo(centerX + 6, centerY); + ctx.closePath(); + ctx.fill(); + + // Update favicon + if (linkElement) { + linkElement.href = canvas.toDataURL('image/png'); + } + } + + /** + * Animation loop using eased bounce + */ + function animate() { + if (!isAnimating) return; + + // Eased bounce: sin wave for smooth up/down motion + var bounce = Math.sin(frame * 0.15) * BOUNCE_HEIGHT; + drawFrame(bounce); + + frame++; + animationId = setTimeout(animate, FRAME_RATE); + } + + /** + * Start the favicon animation + */ + function start() { + if (isAnimating) return; + + init(); + isAnimating = true; + frame = 0; + animate(); + console.log('Favicon animation started'); + } + + /** + * Stop the animation and restore original favicon + */ + function stop() { + if (!isAnimating) return; + + isAnimating = false; + + if (animationId) { + clearTimeout(animationId); + animationId = null; + } + + // Restore original favicon + if (linkElement && originalFavicon) { + linkElement.href = originalFavicon; + } + + console.log('Favicon animation stopped'); + } + + // Expose API + window.BigDump = window.BigDump || {}; + window.BigDump.faviconAnimator = { + start: start, + stop: stop, + isAnimating: function() { return isAnimating; } + }; + +})(); diff --git a/assets/dist/filepolling.min.js b/assets/dist/filepolling.min.js index e2dca40..aaca51e 100644 --- a/assets/dist/filepolling.min.js +++ b/assets/dist/filepolling.min.js @@ -1 +1 @@ -(function(){"use strict";var C=document.getElementById("bigdump-config");if(!C)return;var B=JSON.parse(C.dataset.dbConfigured||"false"),M=JSON.parse(C.dataset.gzipSupported||"false"),G=4e3,p=null,g=new Set,f=new Set;function O(e){switch(e){case"SQL":return"bg-blue-100 dark:bg-blue-900/50 text-blue-800 dark:text-blue-200";case"GZip":return"bg-purple-100 dark:bg-purple-900/50 text-purple-800 dark:text-purple-200";case"CSV":return"bg-green-100 dark:bg-green-900/50 text-green-800 dark:text-green-200";default:return"bg-blue-100 dark:bg-blue-900/50 text-blue-800 dark:text-blue-200"}}function Z(e){var t=e.split(".").pop().toLowerCase();return t==="sql"?"SQL":t==="gz"?"GZip":t==="csv"?"CSV":"SQL"}function R(){var e=document.getElementById("previewModal"),t=document.getElementById("historyModal");return e&&!e.classList.contains("hidden")||t&&!t.classList.contains("hidden")}function z(e){var t=document.getElementById("noFilesMessage"),r=document.getElementById("filesTableContainer");!t||!r||(e?(t.classList.remove("hidden"),r.classList.add("hidden")):(t.classList.add("hidden"),r.classList.remove("hidden")))}function T(e,t,r){var n=document.createElement("tr");n.className="hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors duration-150",n.setAttribute("data-filename",e.name),t&&(n.classList.add("bg-green-50","dark:bg-green-900/20","animate-pulse"),setTimeout(function(){n.classList.remove("bg-green-50","dark:bg-green-900/20","animate-pulse")},3e3)),r&&n.classList.add("bg-amber-50","dark:bg-amber-900/20");var u=B&&(e.type!=="GZip"||M),a="px-4 py-3 border-b border-gray-200 dark:border-gray-700 text-gray-900 dark:text-gray-100",i=document.createElement("td");i.className=a;var l=document.createElement("strong");if(l.textContent=e.name,i.appendChild(l),r){var v=document.createElement("span");v.className="ml-2 text-amber-600 dark:text-amber-400 text-xs";var x=document.createElementNS("http://www.w3.org/2000/svg","svg");x.setAttribute("class","icon w-4 h-4 fill-current inline-block animate-spin");var P=document.createElementNS("http://www.w3.org/2000/svg","use");P.setAttributeNS("http://www.w3.org/1999/xlink","href","assets/icons.svg#spinner"),x.appendChild(P),v.appendChild(x),v.appendChild(document.createTextNode(" Uploading...")),i.appendChild(v)}else if(t){var E=document.createElement("span");E.className="ml-2 px-1.5 py-0.5 bg-green-500 text-white text-xs rounded font-medium",E.textContent="NEW",i.appendChild(E)}n.appendChild(i);var y=document.createElement("td");y.className=a,y.textContent=r?"--":e.sizeFormatted,n.appendChild(y);var N=document.createElement("td");N.className=a,N.textContent=r?"--":e.date,n.appendChild(N);var F=document.createElement("td");F.className=a;var k=document.createElement("span");k.className="px-2 py-1 rounded text-xs font-medium "+O(e.type),k.textContent=e.type,F.appendChild(k),n.appendChild(F);var d=document.createElement("td");if(d.className=a+" text-center",r){var S=document.createElement("span");S.className="text-gray-400",S.textContent="--",d.appendChild(S)}else{if(u){var o=document.createElement("button");o.type="button",o.className="btn btn-icon btn-purple",o.title="Preview SQL content",o.onclick=function(){window.previewFile(e.name)};var L=document.createElementNS("http://www.w3.org/2000/svg","svg");L.setAttribute("class","icon w-4 h-4 fill-current");var A=document.createElementNS("http://www.w3.org/2000/svg","use");A.setAttributeNS("http://www.w3.org/1999/xlink","href","assets/icons.svg#eye"),L.appendChild(A),o.appendChild(L),d.appendChild(o),d.appendChild(document.createTextNode(" "));var s=document.createElement("form");s.method="post",s.action="",s.style.display="inline";var b=document.createElement("input");b.type="hidden",b.name="fn",b.value=e.name,s.appendChild(b);var w=document.createElement("button");w.type="submit",w.className="btn btn-green",w.textContent="Import",s.appendChild(w),d.appendChild(s),d.appendChild(document.createTextNode(" "))}else if(e.type==="GZip"){var D=document.createElement("span");D.className="text-muted",D.textContent="GZip not supported",d.appendChild(D),d.appendChild(document.createTextNode(" "))}var m=document.createElement("a");m.href="?delete="+encodeURIComponent(e.name),m.className="btn btn-red",m.textContent="Delete",m.onclick=function(){return confirm("Delete "+e.name+"?")},d.appendChild(m)}return n.appendChild(d),n}function Q(e,t){var r=document.getElementById("fileTableBody");if(r){var n=new Set(t.map(function(a){return a.name})),u=document.createDocumentFragment();f.forEach(function(a){if(!e.find(function(l){return l.name===a})){var i={name:a,type:Z(a),sizeFormatted:"--",date:"--"};u.appendChild(T(i,!1,!0))}}),e.forEach(function(a){var i=n.has(a.name),l=f.has(a.name);u.appendChild(T(a,i,l))}),r.replaceChildren(u)}}function U(){document.querySelectorAll("#fileTableBody tr[data-filename]").forEach(function(e){g.add(e.getAttribute("data-filename"))})}function h(){U(),p=setInterval(c,G)}function I(){p&&(clearInterval(p),p=null)}function c(){R()||fetch("/files/list").then(function(e){return e.json()}).then(function(e){if(e.success){var t=new Set(e.files.map(function(n){return n.name})),r=e.files.filter(function(n){return!g.has(n.name)});Q(e.files,r),g=t,z(e.files.length===0)}}).catch(function(e){console.error("File polling error:",e)})}function V(e){f.add(e),c()}function _(e){f.delete(e),g.add(e),c()}document.addEventListener("visibilitychange",function(){document.hidden?I():h()}),document.readyState==="loading"?document.addEventListener("DOMContentLoaded",h):h(),window.BigDump=window.BigDump||{},window.BigDump.refreshFileList=c,window.BigDump.trackUploadStart=V,window.BigDump.trackUploadEnd=_,window.BigDump.startFilePolling=h,window.BigDump.stopFilePolling=I,window.refreshFileList=c})(); +(function(){"use strict";var C=document.getElementById("bigdump-config");if(!C)return;var B=JSON.parse(C.dataset.dbConfigured||"false"),M=JSON.parse(C.dataset.gzipSupported||"false"),G=4e3,p=null,g=new Set,f=new Set;function O(e){switch(e){case"SQL":return"bg-blue-100 dark:bg-blue-900/50 text-blue-800 dark:text-blue-200";case"GZip":return"bg-purple-100 dark:bg-purple-900/50 text-purple-800 dark:text-purple-200";case"CSV":return"bg-green-100 dark:bg-green-900/50 text-green-800 dark:text-green-200";default:return"bg-blue-100 dark:bg-blue-900/50 text-blue-800 dark:text-blue-200"}}function Z(e){var t=e.split(".").pop().toLowerCase();return t==="sql"?"SQL":t==="gz"?"GZip":t==="csv"?"CSV":"SQL"}function R(){var e=document.getElementById("previewModal"),t=document.getElementById("historyModal");return e&&!e.classList.contains("hidden")||t&&!t.classList.contains("hidden")}function z(e){var t=document.getElementById("noFilesMessage"),r=document.getElementById("filesTableContainer");!t||!r||(e?(t.classList.remove("hidden"),r.classList.add("hidden")):(t.classList.add("hidden"),r.classList.remove("hidden")))}function T(e,t,r){var n=document.createElement("tr");n.className="hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors duration-150",n.setAttribute("data-filename",e.name),t&&(n.classList.add("bg-green-50","dark:bg-green-900/20","animate-pulse"),setTimeout(function(){n.classList.remove("bg-green-50","dark:bg-green-900/20","animate-pulse")},3e3)),r&&n.classList.add("bg-amber-50","dark:bg-amber-900/20");var u=B&&(e.type!=="GZip"||M),a="px-4 py-3 border-b border-gray-200 dark:border-gray-700 text-gray-900 dark:text-gray-100",i=document.createElement("td");i.className=a;var l=document.createElement("strong");if(l.textContent=e.name,i.appendChild(l),r){var v=document.createElement("span");v.className="ml-2 text-amber-600 dark:text-amber-400 text-xs";var x=document.createElementNS("http://www.w3.org/2000/svg","svg");x.setAttribute("class","icon w-4 h-4 fill-current inline-block animate-spin");var P=document.createElementNS("http://www.w3.org/2000/svg","use");P.setAttributeNS("http://www.w3.org/1999/xlink","href","assets/icons.svg#spinner"),x.appendChild(P),v.appendChild(x),v.appendChild(document.createTextNode(" Uploading...")),i.appendChild(v)}else if(t){var E=document.createElement("span");E.className="ml-2 px-1.5 py-0.5 bg-green-500 text-white text-xs rounded font-medium",E.textContent="NEW",i.appendChild(E)}n.appendChild(i);var y=document.createElement("td");y.className=a,y.textContent=r?"--":e.sizeFormatted,n.appendChild(y);var N=document.createElement("td");N.className=a,N.textContent=r?"--":e.date,n.appendChild(N);var F=document.createElement("td");F.className=a;var k=document.createElement("span");k.className="px-2 py-1 rounded text-xs font-medium "+O(e.type),k.textContent=e.type,F.appendChild(k),n.appendChild(F);var d=document.createElement("td");if(d.className=a+" text-center",r){var S=document.createElement("span");S.className="text-gray-400",S.textContent="--",d.appendChild(S)}else{if(u){var o=document.createElement("button");o.type="button",o.className="btn btn-icon btn-purple",o.title="Preview SQL content",o.onclick=function(){window.previewFile(e.name)};var L=document.createElementNS("http://www.w3.org/2000/svg","svg");L.setAttribute("class","icon w-4 h-4 fill-current");var A=document.createElementNS("http://www.w3.org/2000/svg","use");A.setAttributeNS("http://www.w3.org/1999/xlink","href","assets/icons.svg#eye"),L.appendChild(A),o.appendChild(L),d.appendChild(o),d.appendChild(document.createTextNode(" "));var s=document.createElement("form");s.method="post",s.action="",s.style.display="inline";var b=document.createElement("input");b.type="hidden",b.name="fn",b.value=e.name,s.appendChild(b);var w=document.createElement("button");w.type="submit",w.className="btn btn-green",w.textContent="Import",s.onsubmit=function(){return w.disabled=!0,w.classList.add("opacity-50","cursor-not-allowed"),w.innerHTML='Importing...',!0},s.appendChild(w),d.appendChild(s),d.appendChild(document.createTextNode(" "))}else if(e.type==="GZip"){var D=document.createElement("span");D.className="text-muted",D.textContent="GZip not supported",d.appendChild(D),d.appendChild(document.createTextNode(" "))}var m=document.createElement("a");m.href="?delete="+encodeURIComponent(e.name),m.className="btn btn-red",m.textContent="Delete",m.onclick=function(j){return m.classList.contains("disabled")?(j.preventDefault(),!1):confirm("Delete "+e.name+"?")?(m.classList.add("disabled","opacity-50","cursor-not-allowed","pointer-events-none"),m.innerHTML='Deleting...',!0):!1},d.appendChild(m)}return n.appendChild(d),n}function Q(e,t){var r=document.getElementById("fileTableBody");if(r){var n=new Set(t.map(function(a){return a.name})),u=document.createDocumentFragment();f.forEach(function(a){if(!e.find(function(l){return l.name===a})){var i={name:a,type:Z(a),sizeFormatted:"--",date:"--"};u.appendChild(T(i,!1,!0))}}),e.forEach(function(a){var i=n.has(a.name),l=f.has(a.name);u.appendChild(T(a,i,l))}),r.replaceChildren(u)}}function _(){document.querySelectorAll("#fileTableBody tr[data-filename]").forEach(function(e){g.add(e.getAttribute("data-filename"))})}function h(){_(),p=setInterval(c,G)}function I(){p&&(clearInterval(p),p=null)}function c(){R()||fetch("/files/list").then(function(e){return e.json()}).then(function(e){if(e.success){var t=new Set(e.files.map(function(n){return n.name})),r=e.files.filter(function(n){return!g.has(n.name)});Q(e.files,r),g=t,z(e.files.length===0)}}).catch(function(e){console.error("File polling error:",e)})}function U(e){f.add(e),c()}function V(e){f.delete(e),g.add(e),c()}document.addEventListener("visibilitychange",function(){document.hidden?I():h()}),document.readyState==="loading"?document.addEventListener("DOMContentLoaded",h):h(),window.BigDump=window.BigDump||{},window.BigDump.refreshFileList=c,window.BigDump.trackUploadStart=U,window.BigDump.trackUploadEnd=V,window.BigDump.startFilePolling=h,window.BigDump.stopFilePolling=I,window.refreshFileList=c})(); diff --git a/assets/js/bigdump.js b/assets/js/bigdump.js index a9252a7..f556a4c 100644 --- a/assets/js/bigdump.js +++ b/assets/js/bigdump.js @@ -34,6 +34,48 @@ }); } + /** + * Disable a button with loading state to prevent double-click + * @param {HTMLElement} button - The button element + * @param {string} loadingText - Text to show while loading + */ + function disableButtonWithLoading(button, loadingText) { + button.disabled = true; + button.classList.add('opacity-50', 'cursor-not-allowed'); + button.innerHTML = '' + loadingText; + } + + /** + * Initialize button disable behavior for import and delete forms + * Prevents double-click by disabling buttons after first click + */ + function initButtonDisable() { + // Handle import forms (forms with 'fn' input but no 'action' input) + document.querySelectorAll('form').forEach(function(form) { + var fnInput = form.querySelector('input[name="fn"]'); + var actionInput = form.querySelector('input[name="action"]'); + var submitBtn = form.querySelector('button[type="submit"]'); + + if (!submitBtn) return; + + // Import forms: have 'fn' but no 'action=delete' + if (fnInput && (!actionInput || actionInput.value !== 'delete')) { + form.addEventListener('submit', function() { + disableButtonWithLoading(submitBtn, 'Importing...'); + }); + } + + // Delete forms: have 'action=delete' + if (actionInput && actionInput.value === 'delete') { + form.addEventListener('submit', function(e) { + // The confirm is already handled by onsubmit in HTML + // If we reach here, user confirmed, so disable button + disableButtonWithLoading(submitBtn, 'Deleting...'); + }); + } + }); + } + /** * Initialize dark mode toggle */ @@ -56,6 +98,7 @@ function init() { initLoadingOverlay(); initDarkModeToggle(); + initButtonDisable(); } // Initialize when DOM is ready diff --git a/assets/js/filepolling.js b/assets/js/filepolling.js index 6f76eb1..b8587da 100644 --- a/assets/js/filepolling.js +++ b/assets/js/filepolling.js @@ -197,6 +197,13 @@ importBtn.type = 'submit'; importBtn.className = 'btn btn-green'; importBtn.textContent = 'Import'; + // Disable button on click to prevent double submission + form.onsubmit = function() { + importBtn.disabled = true; + importBtn.classList.add('opacity-50', 'cursor-not-allowed'); + importBtn.innerHTML = 'Importing...'; + return true; // Allow form submission + }; form.appendChild(importBtn); td5.appendChild(form); td5.appendChild(document.createTextNode(' ')); @@ -213,7 +220,19 @@ deleteLink.href = '?delete=' + encodeURIComponent(file.name); deleteLink.className = 'btn btn-red'; deleteLink.textContent = 'Delete'; - deleteLink.onclick = function() { return confirm('Delete ' + file.name + '?'); }; + deleteLink.onclick = function(e) { + if (deleteLink.classList.contains('disabled')) { + e.preventDefault(); + return false; + } + if (!confirm('Delete ' + file.name + '?')) { + return false; + } + // Disable button on click to prevent double action + deleteLink.classList.add('disabled', 'opacity-50', 'cursor-not-allowed', 'pointer-events-none'); + deleteLink.innerHTML = 'Deleting...'; + return true; + }; td5.appendChild(deleteLink); } diff --git a/assets/src/js/bigdump.js b/assets/src/js/bigdump.js index a9252a7..f556a4c 100644 --- a/assets/src/js/bigdump.js +++ b/assets/src/js/bigdump.js @@ -34,6 +34,48 @@ }); } + /** + * Disable a button with loading state to prevent double-click + * @param {HTMLElement} button - The button element + * @param {string} loadingText - Text to show while loading + */ + function disableButtonWithLoading(button, loadingText) { + button.disabled = true; + button.classList.add('opacity-50', 'cursor-not-allowed'); + button.innerHTML = '' + loadingText; + } + + /** + * Initialize button disable behavior for import and delete forms + * Prevents double-click by disabling buttons after first click + */ + function initButtonDisable() { + // Handle import forms (forms with 'fn' input but no 'action' input) + document.querySelectorAll('form').forEach(function(form) { + var fnInput = form.querySelector('input[name="fn"]'); + var actionInput = form.querySelector('input[name="action"]'); + var submitBtn = form.querySelector('button[type="submit"]'); + + if (!submitBtn) return; + + // Import forms: have 'fn' but no 'action=delete' + if (fnInput && (!actionInput || actionInput.value !== 'delete')) { + form.addEventListener('submit', function() { + disableButtonWithLoading(submitBtn, 'Importing...'); + }); + } + + // Delete forms: have 'action=delete' + if (actionInput && actionInput.value === 'delete') { + form.addEventListener('submit', function(e) { + // The confirm is already handled by onsubmit in HTML + // If we reach here, user confirmed, so disable button + disableButtonWithLoading(submitBtn, 'Deleting...'); + }); + } + }); + } + /** * Initialize dark mode toggle */ @@ -56,6 +98,7 @@ function init() { initLoadingOverlay(); initDarkModeToggle(); + initButtonDisable(); } // Initialize when DOM is ready diff --git a/assets/src/js/filepolling.js b/assets/src/js/filepolling.js index b2a5397..77a1438 100644 --- a/assets/src/js/filepolling.js +++ b/assets/src/js/filepolling.js @@ -203,6 +203,13 @@ importBtn.type = 'submit'; importBtn.className = 'btn btn-green'; importBtn.textContent = 'Import'; + // Disable button on click to prevent double submission + form.onsubmit = function() { + importBtn.disabled = true; + importBtn.classList.add('opacity-50', 'cursor-not-allowed'); + importBtn.innerHTML = 'Importing...'; + return true; // Allow form submission + }; form.appendChild(importBtn); td5.appendChild(form); td5.appendChild(document.createTextNode(' ')); @@ -219,7 +226,19 @@ deleteLink.href = '?delete=' + encodeURIComponent(file.name); deleteLink.className = 'btn btn-red'; deleteLink.textContent = 'Delete'; - deleteLink.onclick = function() { return confirm('Delete ' + file.name + '?'); }; + deleteLink.onclick = function(e) { + if (deleteLink.classList.contains('disabled')) { + e.preventDefault(); + return false; + } + if (!confirm('Delete ' + file.name + '?')) { + return false; + } + // Disable button on click to prevent double action + deleteLink.classList.add('disabled', 'opacity-50', 'cursor-not-allowed', 'pointer-events-none'); + deleteLink.innerHTML = 'Deleting...'; + return true; + }; td5.appendChild(deleteLink); } diff --git a/src/Core/Application.php b/src/Core/Application.php index f1ca042..6f7c0ee 100644 --- a/src/Core/Application.php +++ b/src/Core/Application.php @@ -23,7 +23,7 @@ class Application /** * Application version. */ - public const VERSION = '2.27'; + public const VERSION = '2.28'; /** * Configuration instance. diff --git a/src/Core/View.php b/src/Core/View.php index 60c3a7b..9c23751 100644 --- a/src/Core/View.php +++ b/src/Core/View.php @@ -313,6 +313,8 @@ private function loadInlinedAssets(): void 'modal.min.js', 'filepolling.min.js', 'fileupload.min.js', + 'celebration.min.js', + 'favicon-animator.min.js', ]; foreach ($jsOrder as $jsFile) { diff --git a/src/Models/FileHandler.php b/src/Models/FileHandler.php index 0388193..d41d2a5 100644 --- a/src/Models/FileHandler.php +++ b/src/Models/FileHandler.php @@ -259,7 +259,7 @@ private function ensureUploadDir(): void /** * Lists available dump files * - * @return array List of files + * @return array List of files sorted by date (newest first) */ public function listFiles(): array { @@ -297,10 +297,12 @@ public function listFiles(): array continue; } + $mtime = filemtime($filepath) ?: 0; $files[] = [ 'name' => $filename, 'size' => filesize($filepath) ?: 0, - 'date' => date('Y-m-d H:i:s', filemtime($filepath) ?: 0), + 'date' => date('Y-m-d H:i:s', $mtime), + 'timestamp' => $mtime, 'type' => $this->getFileType($extension), 'path' => $filepath, ]; @@ -308,8 +310,8 @@ public function listFiles(): array closedir($handle); - // Sort by name - usort($files, fn($a, $b) => strcasecmp($a['name'], $b['name'])); + // Sort by modification date (newest first) + usort($files, fn($a, $b) => $b['timestamp'] <=> $a['timestamp']); return $files; } diff --git a/src/Services/AjaxService.php b/src/Services/AjaxService.php index e79cff0..e449e9b 100644 --- a/src/Services/AjaxService.php +++ b/src/Services/AjaxService.php @@ -135,8 +135,11 @@ function displayErrorInPage(message, stats) { helpText + '
    ' + actionButtons + - 'Resume' + - 'Back to Home' + + '
    ' + + '' + + '' + + '
    ' + + 'Back to home' + '
    '; // Find main content area and insert error at the beginning @@ -251,28 +254,28 @@ function displaySuccessInPage(stats, warnings, warningsCount, warningsLimitReach '
    ' + '
    ' + '
    ' + - '
    ' + - '
    ' + queriesDone + '
    ' + - '
    Requêtes
    ' + + '
    ' + + '
    ' + queriesDone + '
    ' + + '
    Queries
    ' + '
    ' + - '
    ' + - '
    ' + linesDone + '
    ' + - '
    Lignes
    ' + + '
    ' + + '
    ' + linesDone + '
    ' + + '
    Lines
    ' + '
    ' + - '
    ' + - '
    ' + bytesDone + '
    ' + - '
    Traité
    ' + + '
    ' + + '
    ' + bytesDone + '
    ' + + '
    Processed
    ' + '
    ' + - '
    ' + - '
    ' + elapsedTime + '
    ' + - '
    Durée
    ' + + '
    ' + + '
    ' + elapsedTime + '
    ' + + '
    Duration
    ' + '
    ' + '
    ' + '
    ' + warningsHtml + ''; // Find main content area and replace content @@ -312,6 +315,7 @@ function escapeHtml(text) { // Elapsed timer variables var elapsedTimerInterval = null; + var timerStarted = false; // Prevent timer reset on SSE reconnect /** * Formats elapsed seconds into HH:MM:SS format. @@ -336,10 +340,14 @@ function updateElapsedTimer() { /** * Starts the elapsed timer (call once when import begins). + * Will not reset if already started (prevents reset on SSE reconnect). */ function startElapsedTimer() { - // Reset start time - startTime = Date.now(); + // Only reset start time on first call (not on SSE reconnect) + if (!timerStarted) { + startTime = Date.now(); + timerStarted = true; + } // Clear any existing interval if (elapsedTimerInterval) { clearInterval(elapsedTimerInterval); @@ -357,6 +365,8 @@ function stopElapsedTimer() { clearInterval(elapsedTimerInterval); elapsedTimerInterval = null; } + // Reset flag so next import can start fresh + timerStarted = false; } /** diff --git a/templates/error.php b/templates/error.php index aca9db6..94cc5c0 100644 --- a/templates/error.php +++ b/templates/error.php @@ -37,7 +37,7 @@

    How to fix it:

      -
    1. Click "Back to Home" below
    2. +
    3. Click "Back to home" below
    4. Select your SQL dump file from the file list
    5. Click "Start Import" to begin
    6. Wait for the import to complete without refreshing the page
    7. @@ -70,7 +70,7 @@

      How to fix it:

        -
      1. Click "Back to Home" below
      2. +
      3. Click "Back to home" below
      4. Check the file list to see available files
      5. Upload your SQL dump file again if needed
      6. Make sure the file has .sql, .gz, or .csv extension
      7. @@ -86,6 +86,6 @@ diff --git a/templates/home.php b/templates/home.php index 4b8fbcd..dca55b8 100644 --- a/templates/home.php +++ b/templates/home.php @@ -105,7 +105,7 @@ Filename Size - Date + Date Added Type Actions diff --git a/templates/import.php b/templates/import.php index 7748198..dadcfb6 100644 --- a/templates/import.php +++ b/templates/import.php @@ -342,7 +342,7 @@ hasError()): ?> @@ -388,8 +388,11 @@ class="btn btn-amber" or - Start Over (resume) - Back to Home +
        + + +
        + Back to home (DROP old tables before restarting) diff --git a/templates/layout.php b/templates/layout.php index 2221d5c..5fc8dad 100644 --- a/templates/layout.php +++ b/templates/layout.php @@ -99,5 +99,7 @@ + + From 7af7e3c8c66c383c920f2e2cc9958841a84d4b3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C6=90=C9=94=C4=B1s3?= <8407711+w3spi5@users.noreply.github.com> Date: Wed, 28 Jan 2026 15:12:02 +0100 Subject: [PATCH 15/22] add icon buttons with spinner animation on click --- assets/dist/bigdump.min.js | 2 +- assets/dist/filepolling.min.js | 2 +- assets/{icons.svg => icons2.svg} | 3 +++ assets/js/bigdump.js | 33 +++++++++-------------- assets/js/filepolling.js | 46 +++++++++++++++++++++++--------- assets/src/js/bigdump.js | 33 +++++++++-------------- assets/src/js/filepolling.js | 40 ++++++++++++++++++--------- assets/src/js/history.js | 13 ++++----- assets/src/js/preview.js | 2 +- src/Services/AjaxService.php | 42 ++++++++++++++++++++++++----- templates/home.php | 35 +++++++++++++----------- templates/layout.php | 4 +-- 12 files changed, 156 insertions(+), 99 deletions(-) rename assets/{icons.svg => icons2.svg} (92%) diff --git a/assets/dist/bigdump.min.js b/assets/dist/bigdump.min.js index 98fa28c..a8ff830 100644 --- a/assets/dist/bigdump.min.js +++ b/assets/dist/bigdump.min.js @@ -1 +1 @@ -(function(){"use strict";function o(){var e=document.getElementById("loadingOverlay");e&&document.querySelectorAll("form").forEach(function(t){t.querySelector('input[name="fn"]')&&t.addEventListener("submit",function(i){e.classList.add("active");var n=t.querySelector('input[name="fn"]');if(n){var u=n.value,r=e.querySelector(".loading-subtext");r&&(r.textContent="Loading "+u)}})})}function d(){var e=document.getElementById("darkModeToggle");e&&e.addEventListener("click",function(){var t=document.documentElement,i=t.getAttribute("data-theme")||"light",n=i==="dark"?"light":"dark";t.setAttribute("data-theme",n),localStorage.setItem("bigdump-theme",n)})}function c(e,t){e.disabled=!0,e.classList.add("opacity-50","cursor-not-allowed"),e.innerHTML=''+t}function b(){document.querySelectorAll("form").forEach(function(e){var t=e.querySelector('input[name="fn"]'),i=e.querySelector('input[name="action"]'),n=e.querySelector('button[type="submit"]');n&&(t&&(!i||i.value!=="delete")&&e.addEventListener("submit",function(){c(n,"Importing...")}),i&&i.value==="delete"&&e.addEventListener("submit",function(u){c(n,"Deleting...")}))})}function a(){o(),d(),b()}document.readyState==="loading"?document.addEventListener("DOMContentLoaded",a):a()})(); +!function(){"use strict";function e(){var e,t;(e=document.getElementById("loadingOverlay"))&&document.querySelectorAll("form").forEach(function(t){t.querySelector('input[name="fn"]')&&t.addEventListener("submit",function(n){e.classList.add("active");var a=t.querySelector('input[name="fn"]');if(a){var o=a.value,r=e.querySelector(".loading-subtext");r&&(r.textContent="Loading "+o)}})}),(t=document.getElementById("darkModeToggle"))&&t.addEventListener("click",function(){var e=document.documentElement,t="dark"===(e.getAttribute("data-theme")||"light")?"light":"dark";e.setAttribute("data-theme",t),localStorage.setItem("bigdump-theme",t)}),document.querySelectorAll("form").forEach(function(e){var t=e.querySelector('button[type="submit"]');t&&t.querySelector(".btn-icon-swap")&&e.addEventListener("submit",function(){!function(e){e.disabled=!0,e.classList.add("opacity-50","cursor-not-allowed");var t=e.querySelector(".btn-icon-swap");t&&(t.innerHTML='',t.classList.add("animate-spin"))}(t)})})}"loading"===document.readyState?document.addEventListener("DOMContentLoaded",e):e()}(); \ No newline at end of file diff --git a/assets/dist/filepolling.min.js b/assets/dist/filepolling.min.js index aaca51e..7e64a07 100644 --- a/assets/dist/filepolling.min.js +++ b/assets/dist/filepolling.min.js @@ -1 +1 @@ -(function(){"use strict";var C=document.getElementById("bigdump-config");if(!C)return;var B=JSON.parse(C.dataset.dbConfigured||"false"),M=JSON.parse(C.dataset.gzipSupported||"false"),G=4e3,p=null,g=new Set,f=new Set;function O(e){switch(e){case"SQL":return"bg-blue-100 dark:bg-blue-900/50 text-blue-800 dark:text-blue-200";case"GZip":return"bg-purple-100 dark:bg-purple-900/50 text-purple-800 dark:text-purple-200";case"CSV":return"bg-green-100 dark:bg-green-900/50 text-green-800 dark:text-green-200";default:return"bg-blue-100 dark:bg-blue-900/50 text-blue-800 dark:text-blue-200"}}function Z(e){var t=e.split(".").pop().toLowerCase();return t==="sql"?"SQL":t==="gz"?"GZip":t==="csv"?"CSV":"SQL"}function R(){var e=document.getElementById("previewModal"),t=document.getElementById("historyModal");return e&&!e.classList.contains("hidden")||t&&!t.classList.contains("hidden")}function z(e){var t=document.getElementById("noFilesMessage"),r=document.getElementById("filesTableContainer");!t||!r||(e?(t.classList.remove("hidden"),r.classList.add("hidden")):(t.classList.add("hidden"),r.classList.remove("hidden")))}function T(e,t,r){var n=document.createElement("tr");n.className="hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors duration-150",n.setAttribute("data-filename",e.name),t&&(n.classList.add("bg-green-50","dark:bg-green-900/20","animate-pulse"),setTimeout(function(){n.classList.remove("bg-green-50","dark:bg-green-900/20","animate-pulse")},3e3)),r&&n.classList.add("bg-amber-50","dark:bg-amber-900/20");var u=B&&(e.type!=="GZip"||M),a="px-4 py-3 border-b border-gray-200 dark:border-gray-700 text-gray-900 dark:text-gray-100",i=document.createElement("td");i.className=a;var l=document.createElement("strong");if(l.textContent=e.name,i.appendChild(l),r){var v=document.createElement("span");v.className="ml-2 text-amber-600 dark:text-amber-400 text-xs";var x=document.createElementNS("http://www.w3.org/2000/svg","svg");x.setAttribute("class","icon w-4 h-4 fill-current inline-block animate-spin");var P=document.createElementNS("http://www.w3.org/2000/svg","use");P.setAttributeNS("http://www.w3.org/1999/xlink","href","assets/icons.svg#spinner"),x.appendChild(P),v.appendChild(x),v.appendChild(document.createTextNode(" Uploading...")),i.appendChild(v)}else if(t){var E=document.createElement("span");E.className="ml-2 px-1.5 py-0.5 bg-green-500 text-white text-xs rounded font-medium",E.textContent="NEW",i.appendChild(E)}n.appendChild(i);var y=document.createElement("td");y.className=a,y.textContent=r?"--":e.sizeFormatted,n.appendChild(y);var N=document.createElement("td");N.className=a,N.textContent=r?"--":e.date,n.appendChild(N);var F=document.createElement("td");F.className=a;var k=document.createElement("span");k.className="px-2 py-1 rounded text-xs font-medium "+O(e.type),k.textContent=e.type,F.appendChild(k),n.appendChild(F);var d=document.createElement("td");if(d.className=a+" text-center",r){var S=document.createElement("span");S.className="text-gray-400",S.textContent="--",d.appendChild(S)}else{if(u){var o=document.createElement("button");o.type="button",o.className="btn btn-icon btn-purple",o.title="Preview SQL content",o.onclick=function(){window.previewFile(e.name)};var L=document.createElementNS("http://www.w3.org/2000/svg","svg");L.setAttribute("class","icon w-4 h-4 fill-current");var A=document.createElementNS("http://www.w3.org/2000/svg","use");A.setAttributeNS("http://www.w3.org/1999/xlink","href","assets/icons.svg#eye"),L.appendChild(A),o.appendChild(L),d.appendChild(o),d.appendChild(document.createTextNode(" "));var s=document.createElement("form");s.method="post",s.action="",s.style.display="inline";var b=document.createElement("input");b.type="hidden",b.name="fn",b.value=e.name,s.appendChild(b);var w=document.createElement("button");w.type="submit",w.className="btn btn-green",w.textContent="Import",s.onsubmit=function(){return w.disabled=!0,w.classList.add("opacity-50","cursor-not-allowed"),w.innerHTML='Importing...',!0},s.appendChild(w),d.appendChild(s),d.appendChild(document.createTextNode(" "))}else if(e.type==="GZip"){var D=document.createElement("span");D.className="text-muted",D.textContent="GZip not supported",d.appendChild(D),d.appendChild(document.createTextNode(" "))}var m=document.createElement("a");m.href="?delete="+encodeURIComponent(e.name),m.className="btn btn-red",m.textContent="Delete",m.onclick=function(j){return m.classList.contains("disabled")?(j.preventDefault(),!1):confirm("Delete "+e.name+"?")?(m.classList.add("disabled","opacity-50","cursor-not-allowed","pointer-events-none"),m.innerHTML='Deleting...',!0):!1},d.appendChild(m)}return n.appendChild(d),n}function Q(e,t){var r=document.getElementById("fileTableBody");if(r){var n=new Set(t.map(function(a){return a.name})),u=document.createDocumentFragment();f.forEach(function(a){if(!e.find(function(l){return l.name===a})){var i={name:a,type:Z(a),sizeFormatted:"--",date:"--"};u.appendChild(T(i,!1,!0))}}),e.forEach(function(a){var i=n.has(a.name),l=f.has(a.name);u.appendChild(T(a,i,l))}),r.replaceChildren(u)}}function _(){document.querySelectorAll("#fileTableBody tr[data-filename]").forEach(function(e){g.add(e.getAttribute("data-filename"))})}function h(){_(),p=setInterval(c,G)}function I(){p&&(clearInterval(p),p=null)}function c(){R()||fetch("/files/list").then(function(e){return e.json()}).then(function(e){if(e.success){var t=new Set(e.files.map(function(n){return n.name})),r=e.files.filter(function(n){return!g.has(n.name)});Q(e.files,r),g=t,z(e.files.length===0)}}).catch(function(e){console.error("File polling error:",e)})}function U(e){f.add(e),c()}function V(e){f.delete(e),g.add(e),c()}document.addEventListener("visibilitychange",function(){document.hidden?I():h()}),document.readyState==="loading"?document.addEventListener("DOMContentLoaded",h):h(),window.BigDump=window.BigDump||{},window.BigDump.refreshFileList=c,window.BigDump.trackUploadStart=U,window.BigDump.trackUploadEnd=V,window.BigDump.startFilePolling=h,window.BigDump.stopFilePolling=I,window.refreshFileList=c})(); +!function(){"use strict";var e=document.getElementById("bigdump-config");if(e){var t=JSON.parse(e.dataset.dbConfigured||"false"),n=JSON.parse(e.dataset.gzipSupported||"false"),a=null,r=new Set,d=new Set;document.addEventListener("visibilitychange",function(){document.hidden?o():l()}),"loading"===document.readyState?document.addEventListener("DOMContentLoaded",l):l(),window.BigDump=window.BigDump||{},window.BigDump.refreshFileList=c,window.BigDump.trackUploadStart=function(e){d.add(e),c()},window.BigDump.trackUploadEnd=function(e){d.delete(e),r.add(e),c()},window.BigDump.startFilePolling=l,window.BigDump.stopFilePolling=o,window.refreshFileList=c}function i(e){var t=e.split(".").pop().toLowerCase();return"sql"===t?"SQL":"gz"===t?"GZip":"csv"===t?"CSV":"SQL"}function s(e,a,r){var d=document.createElement("tr");d.className="hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors duration-150",d.setAttribute("data-filename",e.name),a&&(d.classList.add("bg-green-50","dark:bg-green-900/20","animate-pulse"),setTimeout(function(){d.classList.remove("bg-green-50","dark:bg-green-900/20","animate-pulse")},3e3)),r&&d.classList.add("bg-amber-50","dark:bg-amber-900/20");var i=t&&("GZip"!==e.type||n),s="px-4 py-3 border-b border-gray-200 dark:border-gray-700 text-gray-900 dark:text-gray-100",l=document.createElement("td");l.className=s;var o=document.createElement("strong");if(o.textContent=e.name,l.appendChild(o),r){var c=document.createElement("span");c.className="ml-2 text-amber-600 dark:text-amber-400 text-xs";var p=document.createElementNS("http://www.w3.org/2000/svg","svg");p.setAttribute("class","icon w-4 h-4 fill-current inline-block animate-spin");var m=document.createElementNS("http://www.w3.org/2000/svg","use");m.setAttributeNS("http://www.w3.org/1999/xlink","href","assets/icons2.svg#spinner"),p.appendChild(m),c.appendChild(p),c.appendChild(document.createTextNode(" Uploading...")),l.appendChild(c)}else if(a){var u=document.createElement("span");u.className="ml-2 px-1.5 py-0.5 bg-green-500 text-white text-xs rounded font-medium",u.textContent="NEW",l.appendChild(u)}d.appendChild(l);var g=document.createElement("td");g.className=s,g.textContent=r?"--":e.sizeFormatted,d.appendChild(g);var w=document.createElement("td");w.className=s,w.textContent=r?"--":e.date,d.appendChild(w);var h=document.createElement("td");h.className=s;var f=document.createElement("span");f.className="px-2 py-1 rounded text-xs font-medium "+function(e){switch(e){case"SQL":default:return"bg-blue-100 dark:bg-blue-900/50 text-blue-800 dark:text-blue-200";case"GZip":return"bg-purple-100 dark:bg-purple-900/50 text-purple-800 dark:text-purple-200";case"CSV":return"bg-green-100 dark:bg-green-900/50 text-green-800 dark:text-green-200"}}(e.type),f.textContent=e.type,h.appendChild(f),d.appendChild(h);var v=document.createElement("td");if(v.className=s+" text-center",r){var b=document.createElement("span");b.className="text-gray-400",b.textContent="--",v.appendChild(b)}else{if(i){var C=document.createElement("button");C.type="button",C.className="btn btn-icon btn-purple",C.title="Preview SQL content",C.onclick=function(){window.previewFile(e.name)};var x=document.createElementNS("http://www.w3.org/2000/svg","svg");x.setAttribute("class","icon w-4 h-4 fill-current");var E=document.createElementNS("http://www.w3.org/2000/svg","use");E.setAttributeNS("http://www.w3.org/1999/xlink","href","assets/icons2.svg#eye"),x.appendChild(E),C.appendChild(x),v.appendChild(C),v.appendChild(document.createTextNode(" "));var N=document.createElement("form");N.method="post",N.action="",N.style.cssText="display:inline; vertical-align:middle";var y=document.createElement("input");y.type="hidden",y.name="fn",y.value=e.name,N.appendChild(y);var S=document.createElement("button");S.type="submit",S.className="btn btn-green";var k=document.createElementNS("http://www.w3.org/2000/svg","svg");k.setAttribute("class","icon w-4 h-4 fill-current mr-1");var L=document.createElementNS("http://www.w3.org/2000/svg","use");L.setAttributeNS("http://www.w3.org/1999/xlink","href","assets/icons2.svg#download"),k.appendChild(L),S.appendChild(k),S.appendChild(document.createTextNode("Import")),N.onsubmit=function(){return S.disabled=!0,S.classList.add("opacity-50","cursor-not-allowed"),k.classList.add("animate-spin"),L.setAttributeNS("http://www.w3.org/1999/xlink","href","assets/icons2.svg#spinner"),!0},N.appendChild(S),v.appendChild(N),v.appendChild(document.createTextNode(" "))}else if("GZip"===e.type){var B=document.createElement("span");B.className="text-muted",B.textContent="GZip not supported",v.appendChild(B),v.appendChild(document.createTextNode(" "))}var A=document.createElement("a");A.href="?delete="+encodeURIComponent(e.name),A.className="btn btn-icon btn-red",A.title="Delete file";var D=document.createElementNS("http://www.w3.org/2000/svg","svg");D.setAttribute("class","icon w-4 h-4 fill-current");var F=document.createElementNS("http://www.w3.org/2000/svg","use");F.setAttributeNS("http://www.w3.org/1999/xlink","href","assets/icons2.svg#trash"),D.appendChild(F),A.appendChild(D),A.onclick=function(t){return A.classList.contains("disabled")?(t.preventDefault(),!1):!!confirm("Delete "+e.name+"?")&&(A.classList.add("disabled","opacity-50","cursor-not-allowed","pointer-events-none"),D.classList.add("animate-spin"),F.setAttributeNS("http://www.w3.org/1999/xlink","href","assets/icons2.svg#spinner"),!0)},v.appendChild(A)}return d.appendChild(v),d}function l(){document.querySelectorAll("#fileTableBody tr[data-filename]").forEach(function(e){r.add(e.getAttribute("data-filename"))}),a=setInterval(c,4e3)}function o(){a&&(clearInterval(a),a=null)}function c(){var e,t;(e=document.getElementById("previewModal"),t=document.getElementById("historyModal"),e&&!e.classList.contains("hidden")||t&&!t.classList.contains("hidden"))||fetch("/files/list").then(function(e){return e.json()}).then(function(e){if(e.success){var t,n,a,l=new Set(e.files.map(function(e){return e.name})),o=e.files.filter(function(e){return!r.has(e.name)});!function(e,t){var n=document.getElementById("fileTableBody");if(n){var a=new Set(t.map(function(e){return e.name})),r=document.createDocumentFragment();d.forEach(function(t){if(!e.find(function(e){return e.name===t})){var n={name:t,type:i(t),sizeFormatted:"--",date:"--"};r.appendChild(s(n,!1,!0))}}),e.forEach(function(e){var t=a.has(e.name),n=d.has(e.name);r.appendChild(s(e,t,n))}),n.replaceChildren(r)}}(e.files,o),r=l,t=0===e.files.length,n=document.getElementById("noFilesMessage"),a=document.getElementById("filesTableContainer"),n&&a&&(t?(n.classList.remove("hidden"),a.classList.add("hidden")):(n.classList.add("hidden"),a.classList.remove("hidden")))}}).catch(function(e){console.error("File polling error:",e)})}}(); \ No newline at end of file diff --git a/assets/icons.svg b/assets/icons2.svg similarity index 92% rename from assets/icons.svg rename to assets/icons2.svg index a66a408..74a4294 100644 --- a/assets/icons.svg +++ b/assets/icons2.svg @@ -51,4 +51,7 @@ + + + diff --git a/assets/js/bigdump.js b/assets/js/bigdump.js index f556a4c..f40f9dc 100644 --- a/assets/js/bigdump.js +++ b/assets/js/bigdump.js @@ -35,14 +35,18 @@ } /** - * Disable a button with loading state to prevent double-click + * Disable a button and swap its icon to a spinner * @param {HTMLElement} button - The button element - * @param {string} loadingText - Text to show while loading */ - function disableButtonWithLoading(button, loadingText) { + function disableButtonWithSpinner(button) { button.disabled = true; button.classList.add('opacity-50', 'cursor-not-allowed'); - button.innerHTML = '' + loadingText; + // Find the swappable icon and change it to spinner + var iconSwap = button.querySelector('.btn-icon-swap'); + if (iconSwap) { + iconSwap.innerHTML = ''; + iconSwap.classList.add('animate-spin'); + } } /** @@ -50,27 +54,16 @@ * Prevents double-click by disabling buttons after first click */ function initButtonDisable() { - // Handle import forms (forms with 'fn' input but no 'action' input) + // Handle forms with swappable icons (new style) document.querySelectorAll('form').forEach(function(form) { - var fnInput = form.querySelector('input[name="fn"]'); - var actionInput = form.querySelector('input[name="action"]'); var submitBtn = form.querySelector('button[type="submit"]'); - if (!submitBtn) return; - // Import forms: have 'fn' but no 'action=delete' - if (fnInput && (!actionInput || actionInput.value !== 'delete')) { + // Only add handler if button has swappable icon + var iconSwap = submitBtn.querySelector('.btn-icon-swap'); + if (iconSwap) { form.addEventListener('submit', function() { - disableButtonWithLoading(submitBtn, 'Importing...'); - }); - } - - // Delete forms: have 'action=delete' - if (actionInput && actionInput.value === 'delete') { - form.addEventListener('submit', function(e) { - // The confirm is already handled by onsubmit in HTML - // If we reach here, user confirmed, so disable button - disableButtonWithLoading(submitBtn, 'Deleting...'); + disableButtonWithSpinner(submitBtn); }); } }); diff --git a/assets/js/filepolling.js b/assets/js/filepolling.js index b8587da..82500f9 100644 --- a/assets/js/filepolling.js +++ b/assets/js/filepolling.js @@ -126,8 +126,11 @@ if (isUploading) { var uploadSpan = document.createElement('span'); uploadSpan.className = 'ml-2 text-amber-600 dark:text-amber-400 text-xs'; - var spinner = document.createElement('i'); - spinner.className = 'fa-solid fa-spinner fa-spin'; + var spinner = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); + spinner.setAttribute('class', 'icon w-4 h-4 fill-current inline-block animate-spin'); + var spinnerUse = document.createElementNS('http://www.w3.org/2000/svg', 'use'); + spinnerUse.setAttributeNS('http://www.w3.org/1999/xlink', 'href', 'assets/icons2.svg#spinner'); + spinner.appendChild(spinnerUse); uploadSpan.appendChild(spinner); uploadSpan.appendChild(document.createTextNode(' Uploading...')); td1.appendChild(uploadSpan); @@ -177,8 +180,11 @@ previewBtn.className = 'btn btn-icon btn-purple'; previewBtn.title = 'Preview SQL content'; previewBtn.onclick = function() { window.previewFile(file.name); }; - var eyeIcon = document.createElement('i'); - eyeIcon.className = 'fa-solid fa-eye'; + var eyeIcon = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); + eyeIcon.setAttribute('class', 'icon w-4 h-4 fill-current'); + var useEl = document.createElementNS('http://www.w3.org/2000/svg', 'use'); + useEl.setAttributeNS('http://www.w3.org/1999/xlink', 'href', 'assets/icons2.svg#eye'); + eyeIcon.appendChild(useEl); previewBtn.appendChild(eyeIcon); td5.appendChild(previewBtn); td5.appendChild(document.createTextNode(' ')); @@ -187,7 +193,7 @@ var form = document.createElement('form'); form.method = 'post'; form.action = ''; - form.style.display = 'inline'; + form.style.cssText = 'display:inline; vertical-align:middle'; var hiddenInput = document.createElement('input'); hiddenInput.type = 'hidden'; hiddenInput.name = 'fn'; @@ -196,12 +202,21 @@ var importBtn = document.createElement('button'); importBtn.type = 'submit'; importBtn.className = 'btn btn-green'; - importBtn.textContent = 'Import'; - // Disable button on click to prevent double submission + // Add database icon before text + var dbIcon = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); + dbIcon.setAttribute('class', 'icon w-4 h-4 fill-current mr-1'); + var dbUse = document.createElementNS('http://www.w3.org/2000/svg', 'use'); + dbUse.setAttributeNS('http://www.w3.org/1999/xlink', 'href', 'assets/icons2.svg#download'); + dbIcon.appendChild(dbUse); + importBtn.appendChild(dbIcon); + importBtn.appendChild(document.createTextNode('Import')); + // Disable button on click and swap icon to spinner form.onsubmit = function() { importBtn.disabled = true; importBtn.classList.add('opacity-50', 'cursor-not-allowed'); - importBtn.innerHTML = 'Importing...'; + // Swap icon to spinner + dbIcon.classList.add('animate-spin'); + dbUse.setAttributeNS('http://www.w3.org/1999/xlink', 'href', 'assets/icons2.svg#spinner'); return true; // Allow form submission }; form.appendChild(importBtn); @@ -218,8 +233,14 @@ // Delete button var deleteLink = document.createElement('a'); deleteLink.href = '?delete=' + encodeURIComponent(file.name); - deleteLink.className = 'btn btn-red'; - deleteLink.textContent = 'Delete'; + deleteLink.className = 'btn btn-icon btn-red'; + deleteLink.title = 'Delete file'; + var trashIcon = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); + trashIcon.setAttribute('class', 'icon w-4 h-4 fill-current'); + var trashUse = document.createElementNS('http://www.w3.org/2000/svg', 'use'); + trashUse.setAttributeNS('http://www.w3.org/1999/xlink', 'href', 'assets/icons2.svg#trash'); + trashIcon.appendChild(trashUse); + deleteLink.appendChild(trashIcon); deleteLink.onclick = function(e) { if (deleteLink.classList.contains('disabled')) { e.preventDefault(); @@ -228,9 +249,10 @@ if (!confirm('Delete ' + file.name + '?')) { return false; } - // Disable button on click to prevent double action + // Disable button and swap icon to spinner deleteLink.classList.add('disabled', 'opacity-50', 'cursor-not-allowed', 'pointer-events-none'); - deleteLink.innerHTML = 'Deleting...'; + trashIcon.classList.add('animate-spin'); + trashUse.setAttributeNS('http://www.w3.org/1999/xlink', 'href', 'assets/icons2.svg#spinner'); return true; }; td5.appendChild(deleteLink); diff --git a/assets/src/js/bigdump.js b/assets/src/js/bigdump.js index f556a4c..f40f9dc 100644 --- a/assets/src/js/bigdump.js +++ b/assets/src/js/bigdump.js @@ -35,14 +35,18 @@ } /** - * Disable a button with loading state to prevent double-click + * Disable a button and swap its icon to a spinner * @param {HTMLElement} button - The button element - * @param {string} loadingText - Text to show while loading */ - function disableButtonWithLoading(button, loadingText) { + function disableButtonWithSpinner(button) { button.disabled = true; button.classList.add('opacity-50', 'cursor-not-allowed'); - button.innerHTML = '' + loadingText; + // Find the swappable icon and change it to spinner + var iconSwap = button.querySelector('.btn-icon-swap'); + if (iconSwap) { + iconSwap.innerHTML = ''; + iconSwap.classList.add('animate-spin'); + } } /** @@ -50,27 +54,16 @@ * Prevents double-click by disabling buttons after first click */ function initButtonDisable() { - // Handle import forms (forms with 'fn' input but no 'action' input) + // Handle forms with swappable icons (new style) document.querySelectorAll('form').forEach(function(form) { - var fnInput = form.querySelector('input[name="fn"]'); - var actionInput = form.querySelector('input[name="action"]'); var submitBtn = form.querySelector('button[type="submit"]'); - if (!submitBtn) return; - // Import forms: have 'fn' but no 'action=delete' - if (fnInput && (!actionInput || actionInput.value !== 'delete')) { + // Only add handler if button has swappable icon + var iconSwap = submitBtn.querySelector('.btn-icon-swap'); + if (iconSwap) { form.addEventListener('submit', function() { - disableButtonWithLoading(submitBtn, 'Importing...'); - }); - } - - // Delete forms: have 'action=delete' - if (actionInput && actionInput.value === 'delete') { - form.addEventListener('submit', function(e) { - // The confirm is already handled by onsubmit in HTML - // If we reach here, user confirmed, so disable button - disableButtonWithLoading(submitBtn, 'Deleting...'); + disableButtonWithSpinner(submitBtn); }); } }); diff --git a/assets/src/js/filepolling.js b/assets/src/js/filepolling.js index 77a1438..82500f9 100644 --- a/assets/src/js/filepolling.js +++ b/assets/src/js/filepolling.js @@ -129,7 +129,7 @@ var spinner = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); spinner.setAttribute('class', 'icon w-4 h-4 fill-current inline-block animate-spin'); var spinnerUse = document.createElementNS('http://www.w3.org/2000/svg', 'use'); - spinnerUse.setAttributeNS('http://www.w3.org/1999/xlink', 'href', 'assets/icons.svg#spinner'); + spinnerUse.setAttributeNS('http://www.w3.org/1999/xlink', 'href', 'assets/icons2.svg#spinner'); spinner.appendChild(spinnerUse); uploadSpan.appendChild(spinner); uploadSpan.appendChild(document.createTextNode(' Uploading...')); @@ -182,9 +182,9 @@ previewBtn.onclick = function() { window.previewFile(file.name); }; var eyeIcon = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); eyeIcon.setAttribute('class', 'icon w-4 h-4 fill-current'); - var eyeUse = document.createElementNS('http://www.w3.org/2000/svg', 'use'); - eyeUse.setAttributeNS('http://www.w3.org/1999/xlink', 'href', 'assets/icons.svg#eye'); - eyeIcon.appendChild(eyeUse); + var useEl = document.createElementNS('http://www.w3.org/2000/svg', 'use'); + useEl.setAttributeNS('http://www.w3.org/1999/xlink', 'href', 'assets/icons2.svg#eye'); + eyeIcon.appendChild(useEl); previewBtn.appendChild(eyeIcon); td5.appendChild(previewBtn); td5.appendChild(document.createTextNode(' ')); @@ -193,7 +193,7 @@ var form = document.createElement('form'); form.method = 'post'; form.action = ''; - form.style.display = 'inline'; + form.style.cssText = 'display:inline; vertical-align:middle'; var hiddenInput = document.createElement('input'); hiddenInput.type = 'hidden'; hiddenInput.name = 'fn'; @@ -202,12 +202,21 @@ var importBtn = document.createElement('button'); importBtn.type = 'submit'; importBtn.className = 'btn btn-green'; - importBtn.textContent = 'Import'; - // Disable button on click to prevent double submission + // Add database icon before text + var dbIcon = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); + dbIcon.setAttribute('class', 'icon w-4 h-4 fill-current mr-1'); + var dbUse = document.createElementNS('http://www.w3.org/2000/svg', 'use'); + dbUse.setAttributeNS('http://www.w3.org/1999/xlink', 'href', 'assets/icons2.svg#download'); + dbIcon.appendChild(dbUse); + importBtn.appendChild(dbIcon); + importBtn.appendChild(document.createTextNode('Import')); + // Disable button on click and swap icon to spinner form.onsubmit = function() { importBtn.disabled = true; importBtn.classList.add('opacity-50', 'cursor-not-allowed'); - importBtn.innerHTML = 'Importing...'; + // Swap icon to spinner + dbIcon.classList.add('animate-spin'); + dbUse.setAttributeNS('http://www.w3.org/1999/xlink', 'href', 'assets/icons2.svg#spinner'); return true; // Allow form submission }; form.appendChild(importBtn); @@ -224,8 +233,14 @@ // Delete button var deleteLink = document.createElement('a'); deleteLink.href = '?delete=' + encodeURIComponent(file.name); - deleteLink.className = 'btn btn-red'; - deleteLink.textContent = 'Delete'; + deleteLink.className = 'btn btn-icon btn-red'; + deleteLink.title = 'Delete file'; + var trashIcon = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); + trashIcon.setAttribute('class', 'icon w-4 h-4 fill-current'); + var trashUse = document.createElementNS('http://www.w3.org/2000/svg', 'use'); + trashUse.setAttributeNS('http://www.w3.org/1999/xlink', 'href', 'assets/icons2.svg#trash'); + trashIcon.appendChild(trashUse); + deleteLink.appendChild(trashIcon); deleteLink.onclick = function(e) { if (deleteLink.classList.contains('disabled')) { e.preventDefault(); @@ -234,9 +249,10 @@ if (!confirm('Delete ' + file.name + '?')) { return false; } - // Disable button on click to prevent double action + // Disable button and swap icon to spinner deleteLink.classList.add('disabled', 'opacity-50', 'cursor-not-allowed', 'pointer-events-none'); - deleteLink.innerHTML = 'Deleting...'; + trashIcon.classList.add('animate-spin'); + trashUse.setAttributeNS('http://www.w3.org/1999/xlink', 'href', 'assets/icons2.svg#spinner'); return true; }; td5.appendChild(deleteLink); diff --git a/assets/src/js/history.js b/assets/src/js/history.js index 8ba40ba..10fc725 100644 --- a/assets/src/js/history.js +++ b/assets/src/js/history.js @@ -59,14 +59,11 @@ // Status icon var tdStatus = document.createElement('td'); tdStatus.className = 'px-4 py-3 border-b border-gray-200 dark:border-gray-700 text-center'; - var iconName = entry.success ? 'circle-check' : 'circle-xmark'; - var iconColor = entry.success ? 'text-green-500' : 'text-red-500'; - var svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); - svg.setAttribute('class', 'icon w-5 h-5 fill-current inline-block ' + iconColor); - var use = document.createElementNS('http://www.w3.org/2000/svg', 'use'); - use.setAttributeNS('http://www.w3.org/1999/xlink', 'href', 'assets/icons.svg#' + iconName); - svg.appendChild(use); - tdStatus.appendChild(svg); + var icon = document.createElement('i'); + icon.className = entry.success + ? 'fa-solid fa-circle-check text-green-500' + : 'fa-solid fa-circle-xmark text-red-500'; + tdStatus.appendChild(icon); // Filename var tdFile = document.createElement('td'); diff --git a/assets/src/js/preview.js b/assets/src/js/preview.js index d7660a2..3ff551b 100644 --- a/assets/src/js/preview.js +++ b/assets/src/js/preview.js @@ -93,7 +93,7 @@ document.getElementById('previewModalSubtitle').textContent = data.fileSizeFormatted + (data.isGzip ? ' (GZip compressed)' : ''); document.getElementById('previewFileSize').textContent = data.fileSizeFormatted; document.getElementById('previewFileType').textContent = data.isGzip ? 'GZip' : 'SQL'; - document.getElementById('previewTotalLines').textContent = data.totalLines.toLocaleString(); + document.getElementById('previewLinesCount').textContent = data.linesPreview; document.getElementById('previewQueriesCount').textContent = data.queriesPreview; document.getElementById('tabQueriesCount').textContent = data.queriesPreview; diff --git a/src/Services/AjaxService.php b/src/Services/AjaxService.php index e449e9b..67d1925 100644 --- a/src/Services/AjaxService.php +++ b/src/Services/AjaxService.php @@ -917,10 +917,12 @@ function createConnection() { if (source) source.close(); }); - // Handle Stop Import button - close SSE before navigation + // Handle Stop Import button - close SSE and show stopped message document.addEventListener('click', function(e) { - var stopLink = e.target.closest('a[href*="stop_import"]'); + var stopLink = e.target.closest('a[href*="import/stop"]'); if (stopLink) { + e.preventDefault(); + // Close SSE connection immediately intentionalClose = true; stopElapsedTimer(); @@ -930,11 +932,37 @@ function createConnection() { source = null; console.log('SSE: Closed by user (Stop Import)'); } - // Small delay to ensure connection is closed before navigation - e.preventDefault(); - setTimeout(function() { - window.location.href = stopLink.href; - }, 100); + + // Stop progress bar animation + var progressBar = document.querySelector('.progress-bar'); + if (progressBar) { + progressBar.style.animation = 'none'; + progressBar.style.backgroundImage = 'none'; + } + + // Get elapsed time for display + var elapsedEl = document.getElementById('elapsedTime'); + var elapsedTime = elapsedEl ? elapsedEl.textContent : '00:00:00'; + + // Show stopped message in page + var stoppedHtml = '
        ' + + '
        ⏹️
        ' + + '

        Import Stopped

        ' + + '

        The import was manually stopped after ' + elapsedTime + '.

        ' + + '

        Progress has been saved. You can resume later by selecting the same file.

        ' + + '' + + '🏠 Return to Home' + + '' + + '
        '; + + // Replace main content + var mainContent = document.querySelector('main'); + if (mainContent) { + mainContent.innerHTML = stoppedHtml; + } + + // Also call the server to clear session (fire and forget) + fetch(stopLink.href, { method: 'GET' }).catch(function() {}); } }); })(); diff --git a/templates/home.php b/templates/home.php index dca55b8..3d5e6a6 100644 --- a/templates/home.php +++ b/templates/home.php @@ -88,7 +88,7 @@

        Available Dump Files

        @@ -147,21 +147,26 @@ onclick="previewFile('escapeJs($file['name']) ?>')" class="btn btn-icon btn-purple" title="Preview SQL content"> - + -
        + - +
        not supported -
        + - +
        @@ -277,7 +282,7 @@ class="hidden"
    @@ -294,7 +299,7 @@ class="hidden" @@ -324,10 +329,10 @@ class="hidden"
    @@ -347,7 +352,7 @@ class="hidden"
    @@ -366,12 +371,12 @@ class="hidden" @@ -429,7 +434,7 @@ class="hidden" From cd4a0620b9686f29fda1f0a6bceb4b79410ab63e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C6=90=C9=94=C4=B1s3?= <8407711+w3spi5@users.noreply.github.com> Date: Wed, 28 Jan 2026 15:22:51 +0100 Subject: [PATCH 16/22] solved resume issue --- src/Services/AjaxService.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Services/AjaxService.php b/src/Services/AjaxService.php index 67d1925..92db7dd 100644 --- a/src/Services/AjaxService.php +++ b/src/Services/AjaxService.php @@ -948,8 +948,7 @@ function createConnection() { var stoppedHtml = '
    ' + '
    ⏹️
    ' + '

    Import Stopped

    ' + - '

    The import was manually stopped after ' + elapsedTime + '.

    ' + - '

    Progress has been saved. You can resume later by selecting the same file.

    ' + + '

    The import was manually stopped after ' + elapsedTime + '.

    ' + '' + '🏠 Return to Home' + '' + From 5b713f0f799ca29fbeabf3e39bdc1ecad65a28b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C6=90=C9=94=C4=B1s3?= <8407711+w3spi5@users.noreply.github.com> Date: Thu, 29 Jan 2026 12:09:12 +0100 Subject: [PATCH 17/22] Fix dark mode readability for SSE import warning details --- assets/dist/app.min.css | 2 +- assets/src/css/tailwind.css | 114 +++++++++++++++++++++++++++++++++++ src/Services/AjaxService.php | 20 +++--- 3 files changed, 125 insertions(+), 11 deletions(-) diff --git a/assets/dist/app.min.css b/assets/dist/app.min.css index 4c72cd7..3f1d2f3 100644 --- a/assets/dist/app.min.css +++ b/assets/dist/app.min.css @@ -1,2 +1,2 @@ /*! tailwindcss v4.1.18 | MIT License | https://tailwindcss.com */ -@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0;--tw-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-space-y-reverse:0;--tw-divide-y-reverse:0;--tw-border-style:solid;--tw-gradient-position:initial;--tw-gradient-from:#0000;--tw-gradient-via:#0000;--tw-gradient-to:#0000;--tw-gradient-stops:initial;--tw-gradient-via-stops:initial;--tw-gradient-from-position:0%;--tw-gradient-via-position:50%;--tw-gradient-to-position:100%;--tw-font-weight:initial;--tw-tracking:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-duration:initial;--tw-scale-x:1;--tw-scale-y:1;--tw-scale-z:1}}}@layer theme{:root,:host{--font-sans:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--font-mono:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;--color-red-50:oklch(97.1% .013 17.38);--color-red-100:oklch(93.6% .032 17.717);--color-red-200:oklch(88.5% .062 18.334);--color-red-300:oklch(80.8% .114 19.571);--color-red-400:oklch(70.4% .191 22.216);--color-red-500:oklch(63.7% .237 25.331);--color-red-600:oklch(57.7% .245 27.325);--color-red-700:oklch(50.5% .213 27.518);--color-red-800:oklch(44.4% .177 26.899);--color-red-900:oklch(39.6% .141 25.723);--color-amber-50:oklch(98.7% .022 95.277);--color-amber-100:oklch(96.2% .059 95.617);--color-amber-200:oklch(92.4% .12 95.746);--color-amber-300:oklch(87.9% .169 91.605);--color-amber-400:oklch(82.8% .189 84.429);--color-amber-500:oklch(76.9% .188 70.08);--color-amber-600:oklch(66.6% .179 58.318);--color-amber-700:oklch(55.5% .163 48.998);--color-amber-800:oklch(47.3% .137 46.201);--color-amber-900:oklch(41.4% .112 45.904);--color-yellow-50:oklch(98.7% .026 102.212);--color-yellow-100:oklch(97.3% .071 103.193);--color-yellow-200:oklch(94.5% .129 101.54);--color-yellow-300:oklch(90.5% .182 98.111);--color-yellow-500:oklch(79.5% .184 86.047);--color-yellow-600:oklch(68.1% .162 75.834);--color-yellow-800:oklch(47.6% .114 61.907);--color-yellow-900:oklch(42.1% .095 57.708);--color-green-50:oklch(98.2% .018 155.826);--color-green-100:oklch(96.2% .044 156.743);--color-green-200:oklch(92.5% .084 155.995);--color-green-300:oklch(87.1% .15 154.449);--color-green-400:oklch(79.2% .209 151.711);--color-green-500:oklch(72.3% .219 149.579);--color-green-600:oklch(62.7% .194 149.214);--color-green-700:oklch(52.7% .154 150.069);--color-green-800:oklch(44.8% .119 151.328);--color-green-900:oklch(39.3% .095 152.535);--color-cyan-100:oklch(95.6% .045 203.388);--color-cyan-200:oklch(91.7% .08 205.041);--color-cyan-500:oklch(71.5% .143 215.221);--color-cyan-600:oklch(60.9% .126 221.723);--color-cyan-900:oklch(39.8% .07 227.392);--color-blue-50:oklch(97% .014 254.604);--color-blue-100:oklch(93.2% .032 255.585);--color-blue-200:oklch(88.2% .059 254.128);--color-blue-300:oklch(80.9% .105 251.813);--color-blue-400:oklch(70.7% .165 254.624);--color-blue-500:oklch(62.3% .214 259.815);--color-blue-600:oklch(54.6% .245 262.881);--color-blue-700:oklch(48.8% .243 264.376);--color-blue-800:oklch(42.4% .199 265.638);--color-blue-900:oklch(37.9% .146 265.522);--color-indigo-100:oklch(93% .034 272.788);--color-indigo-500:oklch(58.5% .233 277.117);--color-indigo-600:oklch(51.1% .262 276.966);--color-indigo-900:oklch(35.9% .144 278.697);--color-purple-100:oklch(94.6% .033 307.174);--color-purple-200:oklch(90.2% .063 306.703);--color-purple-600:oklch(55.8% .288 302.321);--color-purple-800:oklch(43.8% .218 303.724);--color-purple-900:oklch(38.1% .176 304.987);--color-rose-900:oklch(41% .159 10.272);--color-gray-50:oklch(98.5% .002 247.839);--color-gray-100:oklch(96.7% .003 264.542);--color-gray-200:oklch(92.8% .006 264.531);--color-gray-300:oklch(87.2% .01 258.338);--color-gray-400:oklch(70.7% .022 261.325);--color-gray-500:oklch(55.1% .027 264.364);--color-gray-600:oklch(44.6% .03 256.802);--color-gray-700:oklch(37.3% .034 259.733);--color-gray-800:oklch(27.8% .033 256.848);--color-gray-900:oklch(21% .034 264.665);--color-black:#000;--color-white:#fff;--spacing:.25rem;--container-md:28rem;--container-lg:32rem;--container-2xl:42rem;--container-4xl:56rem;--container-7xl:80rem;--text-xs:.75rem;--text-xs--line-height:calc(1/.75);--text-sm:.875rem;--text-sm--line-height:calc(1.25/.875);--text-base:1rem;--text-base--line-height:calc(1.5/1);--text-lg:1.125rem;--text-lg--line-height:calc(1.75/1.125);--text-xl:1.25rem;--text-xl--line-height:calc(1.75/1.25);--text-2xl:1.5rem;--text-2xl--line-height:calc(2/1.5);--text-6xl:3.75rem;--text-6xl--line-height:1;--font-weight-medium:500;--font-weight-semibold:600;--font-weight-bold:700;--tracking-wide:.025em;--tracking-wider:.05em;--radius-md:.375rem;--radius-lg:.5rem;--radius-xl:.75rem;--animate-spin:spin 1s linear infinite;--animate-pulse:pulse 2s cubic-bezier(.4,0,.6,1)infinite;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4,0,.2,1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono);--animate-progress-stripe:progress-stripe 1s linear infinite}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab, red, red)){::placeholder{color:color-mix(in oklab,currentcolor 50%,transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}}@layer components;@layer utilities{.pointer-events-none{pointer-events:none}.visible{visibility:visible}.absolute{position:absolute}.fixed{position:fixed}.relative{position:relative}.static{position:static}.sticky{position:sticky}.inset-0{inset:calc(var(--spacing)*0)}.top-0{top:calc(var(--spacing)*0)}.z-50{z-index:50}.\!container{width:100%!important}@media (min-width:40rem){.\!container{max-width:40rem!important}}@media (min-width:48rem){.\!container{max-width:48rem!important}}@media (min-width:64rem){.\!container{max-width:64rem!important}}@media (min-width:80rem){.\!container{max-width:80rem!important}}@media (min-width:96rem){.\!container{max-width:96rem!important}}.container{width:100%}@media (min-width:40rem){.container{max-width:40rem}}@media (min-width:48rem){.container{max-width:48rem}}@media (min-width:64rem){.container{max-width:64rem}}@media (min-width:80rem){.container{max-width:80rem}}@media (min-width:96rem){.container{max-width:96rem}}.-mx-1\.5{margin-inline:calc(var(--spacing)*-1.5)}.mx-2{margin-inline:calc(var(--spacing)*2)}.mx-auto{margin-inline:auto}.-my-1\.5{margin-block:calc(var(--spacing)*-1.5)}.my-6{margin-block:calc(var(--spacing)*6)}.mt-1{margin-top:calc(var(--spacing)*1)}.mt-2{margin-top:calc(var(--spacing)*2)}.mt-3{margin-top:calc(var(--spacing)*3)}.mt-4{margin-top:calc(var(--spacing)*4)}.mr-1{margin-right:calc(var(--spacing)*1)}.mr-2{margin-right:calc(var(--spacing)*2)}.mb-1{margin-bottom:calc(var(--spacing)*1)}.mb-2{margin-bottom:calc(var(--spacing)*2)}.mb-3{margin-bottom:calc(var(--spacing)*3)}.mb-4{margin-bottom:calc(var(--spacing)*4)}.mb-6{margin-bottom:calc(var(--spacing)*6)}.ml-1{margin-left:calc(var(--spacing)*1)}.ml-2{margin-left:calc(var(--spacing)*2)}.ml-3{margin-left:calc(var(--spacing)*3)}.ml-4{margin-left:calc(var(--spacing)*4)}.ml-auto{margin-left:auto}.block{display:block}.contents{display:contents}.flex{display:flex}.grid{display:grid}.hidden{display:none}.inline{display:inline}.inline-block{display:inline-block}.inline-flex{display:inline-flex}.table{display:table}.h-2{height:calc(var(--spacing)*2)}.h-2\.5{height:calc(var(--spacing)*2.5)}.h-4{height:calc(var(--spacing)*4)}.h-5{height:calc(var(--spacing)*5)}.h-6{height:calc(var(--spacing)*6)}.h-8{height:calc(var(--spacing)*8)}.h-10{height:calc(var(--spacing)*10)}.h-12{height:calc(var(--spacing)*12)}.h-16{height:calc(var(--spacing)*16)}.h-24{height:calc(var(--spacing)*24)}.h-64{height:calc(var(--spacing)*64)}.h-full{height:100%}.max-h-\[60vh\]{max-height:60vh}.max-h-\[400px\]{max-height:400px}.min-h-\[60vh\]{min-height:60vh}.min-h-\[400px\]{min-height:400px}.min-h-full{min-height:100%}.min-h-screen{min-height:100vh}.w-2{width:calc(var(--spacing)*2)}.w-4{width:calc(var(--spacing)*4)}.w-5{width:calc(var(--spacing)*5)}.w-6{width:calc(var(--spacing)*6)}.w-8{width:calc(var(--spacing)*8)}.w-10{width:calc(var(--spacing)*10)}.w-12{width:calc(var(--spacing)*12)}.w-16{width:calc(var(--spacing)*16)}.w-24{width:calc(var(--spacing)*24)}.w-full{width:100%}.max-w-2xl{max-width:var(--container-2xl)}.max-w-4xl{max-width:var(--container-4xl)}.max-w-7xl{max-width:var(--container-7xl)}.max-w-\[90vw\]{max-width:90vw}.max-w-\[200px\]{max-width:200px}.max-w-lg{max-width:var(--container-lg)}.max-w-md{max-width:var(--container-md)}.min-w-0{min-width:calc(var(--spacing)*0)}.min-w-\[44px\]{min-width:44px}.flex-1{flex:1}.flex-shrink-0{flex-shrink:0}.-translate-y-1{--tw-translate-y:calc(var(--spacing)*-1);translate:var(--tw-translate-x)var(--tw-translate-y)}.rotate-180{rotate:180deg}.transform{transform:var(--tw-rotate-x,)var(--tw-rotate-y,)var(--tw-rotate-z,)var(--tw-skew-x,)var(--tw-skew-y,)}.animate-progress-stripe{animation:var(--animate-progress-stripe)}.animate-pulse{animation:var(--animate-pulse)}.animate-spin{animation:var(--animate-spin)}.cursor-help{cursor:help}.cursor-pointer{cursor:pointer}.resize{resize:both}.list-inside{list-style-position:inside}.list-decimal{list-style-type:decimal}.list-disc{list-style-type:disc}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-center{align-items:center}.items-start{align-items:flex-start}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.justify-end{justify-content:flex-end}.gap-1{gap:calc(var(--spacing)*1)}.gap-2{gap:calc(var(--spacing)*2)}.gap-3{gap:calc(var(--spacing)*3)}.gap-4{gap:calc(var(--spacing)*4)}.gap-6{gap:calc(var(--spacing)*6)}:where(.space-y-1>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*1)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*1)*calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-2>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*2)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*2)*calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-3>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*3)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*3)*calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-4>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*4)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*4)*calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-6>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*6)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*6)*calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-8>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*8)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*8)*calc(1 - var(--tw-space-y-reverse)))}:where(.divide-y>:not(:last-child)){--tw-divide-y-reverse:0;border-bottom-style:var(--tw-border-style);border-top-style:var(--tw-border-style);border-top-width:calc(1px*var(--tw-divide-y-reverse));border-bottom-width:calc(1px*calc(1 - var(--tw-divide-y-reverse)))}:where(.divide-gray-200>:not(:last-child)){border-color:var(--color-gray-200)}.truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.overflow-auto{overflow:auto}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.rounded{border-radius:.25rem}.rounded-full{border-radius:3.40282e38px}.rounded-lg{border-radius:var(--radius-lg)}.rounded-md{border-radius:var(--radius-md)}.rounded-xl{border-radius:var(--radius-xl)}.border{border-style:var(--tw-border-style);border-width:1px}.border-2{border-style:var(--tw-border-style);border-width:2px}.border-4{border-style:var(--tw-border-style);border-width:4px}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.border-b-2{border-bottom-style:var(--tw-border-style);border-bottom-width:2px}.border-dashed{--tw-border-style:dashed;border-style:dashed}.border-blue-200{border-color:var(--color-blue-200)}.border-blue-500{border-color:var(--color-blue-500)}.border-gray-200{border-color:var(--color-gray-200)}.border-gray-300{border-color:var(--color-gray-300)}.border-green-200{border-color:var(--color-green-200)}.border-red-200{border-color:var(--color-red-200)}.border-transparent{border-color:#0000}.border-white{border-color:var(--color-white)}.border-yellow-200{border-color:var(--color-yellow-200)}.border-t-blue-600{border-top-color:var(--color-blue-600)}.bg-amber-50{background-color:var(--color-amber-50)}.bg-amber-100{background-color:var(--color-amber-100)}.bg-amber-500{background-color:var(--color-amber-500)}.bg-black{background-color:var(--color-black)}.bg-black\/50{background-color:#00000080}@supports (color:color-mix(in lab, red, red)){.bg-black\/50{background-color:color-mix(in oklab,var(--color-black)50%,transparent)}}.bg-blue-50{background-color:var(--color-blue-50)}.bg-blue-100{background-color:var(--color-blue-100)}.bg-blue-600{background-color:var(--color-blue-600)}.bg-cyan-100{background-color:var(--color-cyan-100)}.bg-cyan-500{background-color:var(--color-cyan-500)}.bg-gray-50{background-color:var(--color-gray-50)}.bg-gray-100{background-color:var(--color-gray-100)}.bg-gray-200{background-color:var(--color-gray-200)}.bg-gray-900{background-color:var(--color-gray-900)}.bg-green-50{background-color:var(--color-green-50)}.bg-green-100{background-color:var(--color-green-100)}.bg-green-500{background-color:var(--color-green-500)}.bg-green-600{background-color:var(--color-green-600)}.bg-indigo-100{background-color:var(--color-indigo-100)}.bg-purple-100{background-color:var(--color-purple-100)}.bg-red-50{background-color:var(--color-red-50)}.bg-red-100{background-color:var(--color-red-100)}.bg-red-200{background-color:var(--color-red-200)}.bg-red-500{background-color:var(--color-red-500)}.bg-red-600{background-color:var(--color-red-600)}.bg-transparent{background-color:#0000}.bg-white{background-color:var(--color-white)}.bg-white\/90{background-color:#ffffffe6}@supports (color:color-mix(in lab, red, red)){.bg-white\/90{background-color:color-mix(in oklab,var(--color-white)90%,transparent)}}.bg-yellow-50{background-color:var(--color-yellow-50)}.bg-yellow-100{background-color:var(--color-yellow-100)}.bg-yellow-500{background-color:var(--color-yellow-500)}.bg-gradient-to-r{--tw-gradient-position:to right in oklab;background-image:linear-gradient(var(--tw-gradient-stops))}.from-blue-500{--tw-gradient-from:var(--color-blue-500);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-cyan-600{--tw-gradient-from:var(--color-cyan-600);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.to-blue-600{--tw-gradient-to:var(--color-blue-600);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.to-rose-900{--tw-gradient-to:var(--color-rose-900);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.bg-\[length\:1rem_1rem\]{background-size:1rem 1rem}.fill-current{fill:currentColor}.fill-indigo-500{fill:var(--color-indigo-500)}.p-0{padding:calc(var(--spacing)*0)}.p-0\.5{padding:calc(var(--spacing)*.5)}.p-1{padding:calc(var(--spacing)*1)}.p-1\.5{padding:calc(var(--spacing)*1.5)}.p-2{padding:calc(var(--spacing)*2)}.p-3{padding:calc(var(--spacing)*3)}.p-4{padding:calc(var(--spacing)*4)}.p-6{padding:calc(var(--spacing)*6)}.p-8{padding:calc(var(--spacing)*8)}.p-12{padding:calc(var(--spacing)*12)}.px-1{padding-inline:calc(var(--spacing)*1)}.px-1\.5{padding-inline:calc(var(--spacing)*1.5)}.px-2{padding-inline:calc(var(--spacing)*2)}.px-2\.5{padding-inline:calc(var(--spacing)*2.5)}.px-3{padding-inline:calc(var(--spacing)*3)}.px-4{padding-inline:calc(var(--spacing)*4)}.px-6{padding-inline:calc(var(--spacing)*6)}.px-8{padding-inline:calc(var(--spacing)*8)}.py-0{padding-block:calc(var(--spacing)*0)}.py-0\.5{padding-block:calc(var(--spacing)*.5)}.py-1{padding-block:calc(var(--spacing)*1)}.py-1\.5{padding-block:calc(var(--spacing)*1.5)}.py-2{padding-block:calc(var(--spacing)*2)}.py-3{padding-block:calc(var(--spacing)*3)}.py-4{padding-block:calc(var(--spacing)*4)}.py-6{padding-block:calc(var(--spacing)*6)}.py-8{padding-block:calc(var(--spacing)*8)}.py-12{padding-block:calc(var(--spacing)*12)}.pt-4{padding-top:calc(var(--spacing)*4)}.text-center{text-align:center}.text-left{text-align:left}.text-right{text-align:right}.font-mono{font-family:var(--font-mono)}.text-2xl{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height))}.text-6xl{font-size:var(--text-6xl);line-height:var(--tw-leading,var(--text-6xl--line-height))}.text-base{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}.text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xl{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.font-bold{--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold)}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.tracking-wide{--tw-tracking:var(--tracking-wide);letter-spacing:var(--tracking-wide)}.tracking-wider{--tw-tracking:var(--tracking-wider);letter-spacing:var(--tracking-wider)}.whitespace-nowrap{white-space:nowrap}.whitespace-pre-line{white-space:pre-line}.whitespace-pre-wrap{white-space:pre-wrap}.text-amber-600{color:var(--color-amber-600)}.text-amber-700{color:var(--color-amber-700)}.text-amber-800{color:var(--color-amber-800)}.text-blue-500{color:var(--color-blue-500)}.text-blue-600{color:var(--color-blue-600)}.text-blue-700{color:var(--color-blue-700)}.text-blue-800{color:var(--color-blue-800)}.text-cyan-100{color:var(--color-cyan-100)}.text-cyan-600{color:var(--color-cyan-600)}.text-gray-100{color:var(--color-gray-100)}.text-gray-400{color:var(--color-gray-400)}.text-gray-500{color:var(--color-gray-500)}.text-gray-600{color:var(--color-gray-600)}.text-gray-700{color:var(--color-gray-700)}.text-gray-800{color:var(--color-gray-800)}.text-gray-900{color:var(--color-gray-900)}.text-green-500{color:var(--color-green-500)}.text-green-600{color:var(--color-green-600)}.text-green-700{color:var(--color-green-700)}.text-green-800{color:var(--color-green-800)}.text-indigo-600{color:var(--color-indigo-600)}.text-purple-600{color:var(--color-purple-600)}.text-purple-800{color:var(--color-purple-800)}.text-red-500{color:var(--color-red-500)}.text-red-600{color:var(--color-red-600)}.text-red-700{color:var(--color-red-700)}.text-red-800{color:var(--color-red-800)}.text-white{color:var(--color-white)}.text-yellow-500{color:var(--color-yellow-500)}.text-yellow-600{color:var(--color-yellow-600)}.text-yellow-800{color:var(--color-yellow-800)}.uppercase{text-transform:uppercase}.italic{font-style:italic}.no-underline{text-decoration-line:none}.opacity-0{opacity:0}.opacity-25{opacity:.25}.opacity-50{opacity:.5}.opacity-75{opacity:.75}.opacity-80{opacity:.8}.opacity-100{opacity:1}.shadow{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a),0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-2xl{--tw-shadow:0 25px 50px -12px var(--tw-shadow-color,#00000040);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-lg{--tw-shadow:0 10px 15px -3px var(--tw-shadow-color,#0000001a),0 4px 6px -4px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-md{--tw-shadow:0 4px 6px -1px var(--tw-shadow-color,#0000001a),0 2px 4px -2px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-sm{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a),0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-xl{--tw-shadow:0 20px 25px -5px var(--tw-shadow-color,#0000001a),0 8px 10px -6px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.ring{--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(1px + var(--tw-ring-offset-width))var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.blur{--tw-blur:blur(8px);filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,)}.transition{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter,display,content-visibility,overlay,pointer-events;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-all{transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-colors{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-opacity{transition-property:opacity;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-transform{transition-property:transform,translate,scale,rotate;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.duration-150{--tw-duration:.15s;transition-duration:.15s}.duration-200{--tw-duration:.2s;transition-duration:.2s}.duration-300{--tw-duration:.3s;transition-duration:.3s}.duration-500{--tw-duration:.5s;transition-duration:.5s}.even\:bg-gray-50:nth-child(2n){background-color:var(--color-gray-50)}@media (hover:hover){.hover\:scale-105:hover{--tw-scale-x:105%;--tw-scale-y:105%;--tw-scale-z:105%;scale:var(--tw-scale-x)var(--tw-scale-y)}.hover\:border-gray-400:hover{border-color:var(--color-gray-400)}.hover\:bg-amber-600:hover{background-color:var(--color-amber-600)}.hover\:bg-black\/10:hover{background-color:#0000001a}@supports (color:color-mix(in lab, red, red)){.hover\:bg-black\/10:hover{background-color:color-mix(in oklab,var(--color-black)10%,transparent)}}.hover\:bg-blue-700:hover{background-color:var(--color-blue-700)}.hover\:bg-cyan-600:hover{background-color:var(--color-cyan-600)}.hover\:bg-gray-50:hover{background-color:var(--color-gray-50)}.hover\:bg-gray-100:hover{background-color:var(--color-gray-100)}.hover\:bg-gray-200:hover{background-color:var(--color-gray-200)}.hover\:bg-green-600:hover{background-color:var(--color-green-600)}.hover\:bg-green-700:hover{background-color:var(--color-green-700)}.hover\:bg-red-50:hover{background-color:var(--color-red-50)}.hover\:bg-red-700:hover{background-color:var(--color-red-700)}.hover\:text-gray-600:hover{color:var(--color-gray-600)}.hover\:text-gray-700:hover{color:var(--color-gray-700)}.hover\:text-red-700:hover{color:var(--color-red-700)}.hover\:underline:hover{text-decoration-line:underline}.hover\:opacity-90:hover{opacity:.9}}.focus\:ring-2:focus{--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(2px + var(--tw-ring-offset-width))var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.focus\:ring-blue-500:focus{--tw-ring-color:var(--color-blue-500)}.focus\:ring-offset-2:focus{--tw-ring-offset-width:2px;--tw-ring-offset-shadow:var(--tw-ring-inset,)0 0 0 var(--tw-ring-offset-width)var(--tw-ring-offset-color)}.focus\:outline-none:focus{--tw-outline-style:none;outline-style:none}.active\:scale-95:active{--tw-scale-x:95%;--tw-scale-y:95%;--tw-scale-z:95%;scale:var(--tw-scale-x)var(--tw-scale-y)}.disabled\:cursor-not-allowed:disabled{cursor:not-allowed}.disabled\:opacity-50:disabled{opacity:.5}@media (min-width:40rem){.sm\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.sm\:flex-row{flex-direction:row}.sm\:px-6{padding-inline:calc(var(--spacing)*6)}}@media (min-width:48rem){.md\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.md\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.md\:grid-cols-6{grid-template-columns:repeat(6,minmax(0,1fr))}}@media (min-width:64rem){.lg\:grid-cols-6{grid-template-columns:repeat(6,minmax(0,1fr))}.lg\:px-8{padding-inline:calc(var(--spacing)*8)}}:where(.dark\:divide-gray-700:where([data-theme=dark],[data-theme=dark] *)>:not(:last-child)){border-color:var(--color-gray-700)}.dark\:border-blue-800:where([data-theme=dark],[data-theme=dark] *){border-color:var(--color-blue-800)}.dark\:border-gray-600:where([data-theme=dark],[data-theme=dark] *){border-color:var(--color-gray-600)}.dark\:border-gray-700:where([data-theme=dark],[data-theme=dark] *){border-color:var(--color-gray-700)}.dark\:border-green-800:where([data-theme=dark],[data-theme=dark] *){border-color:var(--color-green-800)}.dark\:border-red-800:where([data-theme=dark],[data-theme=dark] *){border-color:var(--color-red-800)}.dark\:border-yellow-800:where([data-theme=dark],[data-theme=dark] *){border-color:var(--color-yellow-800)}.dark\:bg-amber-900\/20:where([data-theme=dark],[data-theme=dark] *){background-color:#7b330633}@supports (color:color-mix(in lab, red, red)){.dark\:bg-amber-900\/20:where([data-theme=dark],[data-theme=dark] *){background-color:color-mix(in oklab,var(--color-amber-900)20%,transparent)}}.dark\:bg-amber-900\/30:where([data-theme=dark],[data-theme=dark] *){background-color:#7b33064d}@supports (color:color-mix(in lab, red, red)){.dark\:bg-amber-900\/30:where([data-theme=dark],[data-theme=dark] *){background-color:color-mix(in oklab,var(--color-amber-900)30%,transparent)}}.dark\:bg-amber-900\/50:where([data-theme=dark],[data-theme=dark] *){background-color:#7b330680}@supports (color:color-mix(in lab, red, red)){.dark\:bg-amber-900\/50:where([data-theme=dark],[data-theme=dark] *){background-color:color-mix(in oklab,var(--color-amber-900)50%,transparent)}}.dark\:bg-blue-800:where([data-theme=dark],[data-theme=dark] *){background-color:var(--color-blue-800)}.dark\:bg-blue-800\/30:where([data-theme=dark],[data-theme=dark] *){background-color:#193cb84d}@supports (color:color-mix(in lab, red, red)){.dark\:bg-blue-800\/30:where([data-theme=dark],[data-theme=dark] *){background-color:color-mix(in oklab,var(--color-blue-800)30%,transparent)}}.dark\:bg-blue-900\/20:where([data-theme=dark],[data-theme=dark] *){background-color:#1c398e33}@supports (color:color-mix(in lab, red, red)){.dark\:bg-blue-900\/20:where([data-theme=dark],[data-theme=dark] *){background-color:color-mix(in oklab,var(--color-blue-900)20%,transparent)}}.dark\:bg-blue-900\/30:where([data-theme=dark],[data-theme=dark] *){background-color:#1c398e4d}@supports (color:color-mix(in lab, red, red)){.dark\:bg-blue-900\/30:where([data-theme=dark],[data-theme=dark] *){background-color:color-mix(in oklab,var(--color-blue-900)30%,transparent)}}.dark\:bg-blue-900\/50:where([data-theme=dark],[data-theme=dark] *){background-color:#1c398e80}@supports (color:color-mix(in lab, red, red)){.dark\:bg-blue-900\/50:where([data-theme=dark],[data-theme=dark] *){background-color:color-mix(in oklab,var(--color-blue-900)50%,transparent)}}.dark\:bg-cyan-900\/30:where([data-theme=dark],[data-theme=dark] *){background-color:#104e644d}@supports (color:color-mix(in lab, red, red)){.dark\:bg-cyan-900\/30:where([data-theme=dark],[data-theme=dark] *){background-color:color-mix(in oklab,var(--color-cyan-900)30%,transparent)}}.dark\:bg-gray-600:where([data-theme=dark],[data-theme=dark] *){background-color:var(--color-gray-600)}.dark\:bg-gray-700:where([data-theme=dark],[data-theme=dark] *){background-color:var(--color-gray-700)}.dark\:bg-gray-800:where([data-theme=dark],[data-theme=dark] *){background-color:var(--color-gray-800)}.dark\:bg-gray-900:where([data-theme=dark],[data-theme=dark] *){background-color:var(--color-gray-900)}.dark\:bg-green-900\/20:where([data-theme=dark],[data-theme=dark] *){background-color:#0d542b33}@supports (color:color-mix(in lab, red, red)){.dark\:bg-green-900\/20:where([data-theme=dark],[data-theme=dark] *){background-color:color-mix(in oklab,var(--color-green-900)20%,transparent)}}.dark\:bg-green-900\/30:where([data-theme=dark],[data-theme=dark] *){background-color:#0d542b4d}@supports (color:color-mix(in lab, red, red)){.dark\:bg-green-900\/30:where([data-theme=dark],[data-theme=dark] *){background-color:color-mix(in oklab,var(--color-green-900)30%,transparent)}}.dark\:bg-green-900\/40:where([data-theme=dark],[data-theme=dark] *){background-color:#0d542b66}@supports (color:color-mix(in lab, red, red)){.dark\:bg-green-900\/40:where([data-theme=dark],[data-theme=dark] *){background-color:color-mix(in oklab,var(--color-green-900)40%,transparent)}}.dark\:bg-green-900\/50:where([data-theme=dark],[data-theme=dark] *){background-color:#0d542b80}@supports (color:color-mix(in lab, red, red)){.dark\:bg-green-900\/50:where([data-theme=dark],[data-theme=dark] *){background-color:color-mix(in oklab,var(--color-green-900)50%,transparent)}}.dark\:bg-indigo-900\/30:where([data-theme=dark],[data-theme=dark] *){background-color:#312c854d}@supports (color:color-mix(in lab, red, red)){.dark\:bg-indigo-900\/30:where([data-theme=dark],[data-theme=dark] *){background-color:color-mix(in oklab,var(--color-indigo-900)30%,transparent)}}.dark\:bg-purple-900\/30:where([data-theme=dark],[data-theme=dark] *){background-color:#59168b4d}@supports (color:color-mix(in lab, red, red)){.dark\:bg-purple-900\/30:where([data-theme=dark],[data-theme=dark] *){background-color:color-mix(in oklab,var(--color-purple-900)30%,transparent)}}.dark\:bg-purple-900\/50:where([data-theme=dark],[data-theme=dark] *){background-color:#59168b80}@supports (color:color-mix(in lab, red, red)){.dark\:bg-purple-900\/50:where([data-theme=dark],[data-theme=dark] *){background-color:color-mix(in oklab,var(--color-purple-900)50%,transparent)}}.dark\:bg-red-800:where([data-theme=dark],[data-theme=dark] *){background-color:var(--color-red-800)}.dark\:bg-red-900\/20:where([data-theme=dark],[data-theme=dark] *){background-color:#82181a33}@supports (color:color-mix(in lab, red, red)){.dark\:bg-red-900\/20:where([data-theme=dark],[data-theme=dark] *){background-color:color-mix(in oklab,var(--color-red-900)20%,transparent)}}.dark\:bg-red-900\/30:where([data-theme=dark],[data-theme=dark] *){background-color:#82181a4d}@supports (color:color-mix(in lab, red, red)){.dark\:bg-red-900\/30:where([data-theme=dark],[data-theme=dark] *){background-color:color-mix(in oklab,var(--color-red-900)30%,transparent)}}.dark\:bg-red-900\/40:where([data-theme=dark],[data-theme=dark] *){background-color:#82181a66}@supports (color:color-mix(in lab, red, red)){.dark\:bg-red-900\/40:where([data-theme=dark],[data-theme=dark] *){background-color:color-mix(in oklab,var(--color-red-900)40%,transparent)}}.dark\:bg-red-900\/50:where([data-theme=dark],[data-theme=dark] *){background-color:#82181a80}@supports (color:color-mix(in lab, red, red)){.dark\:bg-red-900\/50:where([data-theme=dark],[data-theme=dark] *){background-color:color-mix(in oklab,var(--color-red-900)50%,transparent)}}.dark\:bg-yellow-900\/20:where([data-theme=dark],[data-theme=dark] *){background-color:#733e0a33}@supports (color:color-mix(in lab, red, red)){.dark\:bg-yellow-900\/20:where([data-theme=dark],[data-theme=dark] *){background-color:color-mix(in oklab,var(--color-yellow-900)20%,transparent)}}.dark\:bg-yellow-900\/30:where([data-theme=dark],[data-theme=dark] *){background-color:#733e0a4d}@supports (color:color-mix(in lab, red, red)){.dark\:bg-yellow-900\/30:where([data-theme=dark],[data-theme=dark] *){background-color:color-mix(in oklab,var(--color-yellow-900)30%,transparent)}}.dark\:text-amber-200:where([data-theme=dark],[data-theme=dark] *){color:var(--color-amber-200)}.dark\:text-amber-300:where([data-theme=dark],[data-theme=dark] *){color:var(--color-amber-300)}.dark\:text-amber-400:where([data-theme=dark],[data-theme=dark] *){color:var(--color-amber-400)}.dark\:text-blue-200:where([data-theme=dark],[data-theme=dark] *){color:var(--color-blue-200)}.dark\:text-blue-300:where([data-theme=dark],[data-theme=dark] *){color:var(--color-blue-300)}.dark\:text-blue-400:where([data-theme=dark],[data-theme=dark] *){color:var(--color-blue-400)}.dark\:text-cyan-200:where([data-theme=dark],[data-theme=dark] *){color:var(--color-cyan-200)}.dark\:text-gray-100:where([data-theme=dark],[data-theme=dark] *){color:var(--color-gray-100)}.dark\:text-gray-200:where([data-theme=dark],[data-theme=dark] *){color:var(--color-gray-200)}.dark\:text-gray-300:where([data-theme=dark],[data-theme=dark] *){color:var(--color-gray-300)}.dark\:text-gray-400:where([data-theme=dark],[data-theme=dark] *){color:var(--color-gray-400)}.dark\:text-gray-500:where([data-theme=dark],[data-theme=dark] *){color:var(--color-gray-500)}.dark\:text-green-200:where([data-theme=dark],[data-theme=dark] *){color:var(--color-green-200)}.dark\:text-green-300:where([data-theme=dark],[data-theme=dark] *){color:var(--color-green-300)}.dark\:text-green-400:where([data-theme=dark],[data-theme=dark] *){color:var(--color-green-400)}.dark\:text-purple-200:where([data-theme=dark],[data-theme=dark] *){color:var(--color-purple-200)}.dark\:text-red-200:where([data-theme=dark],[data-theme=dark] *){color:var(--color-red-200)}.dark\:text-red-300:where([data-theme=dark],[data-theme=dark] *){color:var(--color-red-300)}.dark\:text-red-400:where([data-theme=dark],[data-theme=dark] *){color:var(--color-red-400)}.dark\:text-white:where([data-theme=dark],[data-theme=dark] *){color:var(--color-white)}.dark\:text-yellow-300:where([data-theme=dark],[data-theme=dark] *){color:var(--color-yellow-300)}.dark\:even\:bg-gray-800\/50:where([data-theme=dark],[data-theme=dark] *):nth-child(2n){background-color:#1e293980}@supports (color:color-mix(in lab, red, red)){.dark\:even\:bg-gray-800\/50:where([data-theme=dark],[data-theme=dark] *):nth-child(2n){background-color:color-mix(in oklab,var(--color-gray-800)50%,transparent)}}@media (hover:hover){.dark\:hover\:border-gray-500:where([data-theme=dark],[data-theme=dark] *):hover{border-color:var(--color-gray-500)}.dark\:hover\:bg-gray-600:where([data-theme=dark],[data-theme=dark] *):hover{background-color:var(--color-gray-600)}.dark\:hover\:bg-gray-700:where([data-theme=dark],[data-theme=dark] *):hover{background-color:var(--color-gray-700)}.dark\:hover\:bg-gray-700\/50:where([data-theme=dark],[data-theme=dark] *):hover{background-color:#36415380}@supports (color:color-mix(in lab, red, red)){.dark\:hover\:bg-gray-700\/50:where([data-theme=dark],[data-theme=dark] *):hover{background-color:color-mix(in oklab,var(--color-gray-700)50%,transparent)}}.dark\:hover\:bg-gray-800:where([data-theme=dark],[data-theme=dark] *):hover{background-color:var(--color-gray-800)}.dark\:hover\:bg-red-900\/20:where([data-theme=dark],[data-theme=dark] *):hover{background-color:#82181a33}@supports (color:color-mix(in lab, red, red)){.dark\:hover\:bg-red-900\/20:where([data-theme=dark],[data-theme=dark] *):hover{background-color:color-mix(in oklab,var(--color-red-900)20%,transparent)}}.dark\:hover\:text-gray-200:where([data-theme=dark],[data-theme=dark] *):hover{color:var(--color-gray-200)}.dark\:hover\:text-gray-300:where([data-theme=dark],[data-theme=dark] *):hover{color:var(--color-gray-300)}}}@keyframes progress-stripe{0%{background-position:1rem 0}to{background-position:0 0}}.icon{vertical-align:middle;fill:currentColor;width:1em;height:1em;display:inline-block}.icon-lg{width:1.5em;height:1.5em}*,:before,:after{transition:background-color .3s,border-color .3s,color .3s}.btn{text-align:center;cursor:pointer;border:none;border-radius:.375rem;justify-content:center;align-items:center;padding:.5rem 1rem;font-size:.875rem;font-weight:500;line-height:1.25rem;text-decoration:none;transition:all .15s;display:inline-flex}.btn:hover{transform:scale(1.05);box-shadow:0 10px 15px -3px #0000001a,0 4px 6px -4px #0000001a}.btn:active{transform:scale(.95)}.btn:focus{outline-offset:2px;outline:2px solid #0000}.btn:focus-visible{box-shadow:0 0 0 2px #fff,0 0 0 4px #3b82f6}.btn-sm{padding:.375rem .75rem;font-size:.75rem}.btn-icon{vertical-align:middle;min-width:2.25rem;min-height:2.25rem;padding:.5rem .75rem}.btn-blue{color:#fff;background-color:#2563eb}.btn-blue:hover{background-color:#1d4ed8}.btn-green{color:#fff;background-color:#22c55e}.btn-green:hover{background-color:#16a34a}.btn-red{color:#fff;background-color:#ef4444}.btn-red:hover{background-color:#dc2626}.btn-amber{color:#fff;background-color:#f59e0b}.btn-amber:hover{background-color:#d97706}.btn-cyan{color:#fff;background-color:#06b6d4}.btn-cyan:hover{background-color:#0891b2}.btn-purple{color:#fff;background-color:#a855f7}.btn-purple:hover{background-color:#9333ea}.btn-indigo{color:#fff;background-color:#6366f1}.btn-indigo:hover{background-color:#4f46e5}.btn-gray{color:#fff;background-color:#6b7280}.btn-gray:hover{background-color:#4b5563}.btn-secondary{color:#374151;background-color:#e5e7eb}.btn-secondary:hover{background-color:#d1d5db}[data-theme=dark] .btn-secondary{color:#e5e7eb;background-color:#374151}[data-theme=dark] .btn-secondary:hover{background-color:#4b5563}.btn-danger-ghost{color:#b91c1c;background-color:#fee2e2}.btn-danger-ghost:hover{background-color:#fecaca}[data-theme=dark] .btn-danger-ghost{color:#fca5a5;background-color:#7f1d1d4d}[data-theme=dark] .btn-danger-ghost:hover{background-color:#7f1d1d80}.alert{border-style:solid;border-width:1px;border-radius:.5rem;margin-bottom:1rem;padding:.75rem 1rem;font-size:.875rem;line-height:1.25rem}.alert-success{color:#166534;background-color:#dcfce7;border-color:#86efac}[data-theme=dark] .alert-success{color:#bbf7d0;background-color:#22c55e33;border-color:#166534}.alert-error{color:#991b1b;background-color:#fee2e2;border-color:#fca5a5}[data-theme=dark] .alert-error{color:#fecaca;background-color:#ef444433;border-color:#7f1d1d}.alert-warning{color:#92400e;background-color:#fef3c7;border-color:#fcd34d}[data-theme=dark] .alert-warning{color:#fde68a;background-color:#f59e0b33;border-color:#78350f}.alert-info{color:#155e75;background-color:#ecfeff;border-color:#a5f3fc}[data-theme=dark] .alert-info{color:#a5f3fc;background-color:#06b6d433;border-color:#164e63}.alert-lg{border-radius:.75rem;margin-bottom:1.5rem;padding:1.5rem}.card{background-color:#fff;border:1px solid #e5e7eb;border-radius:.5rem;box-shadow:0 1px 2px #0000000d}[data-theme=dark] .card{background-color:#1f2937;border-color:#374151}.card-body{padding:1.25rem}.card-header{text-transform:uppercase;letter-spacing:.05em;color:#6b7280;border-bottom:1px solid #e5e7eb;padding:.75rem 1.5rem;font-size:.75rem;font-weight:600}[data-theme=dark] .card-header{color:#9ca3af;border-color:#374151}.info-box{color:#fff;background:linear-gradient(90deg,#0891b2,#0e7490);border-radius:.75rem;margin-bottom:1.5rem;padding:1rem 1.5rem}[data-theme=dark] .info-box{background:linear-gradient(90deg,#155e75,#164e63)}.stat-box{text-align:center;background-color:#fff;border:1px solid #e5e7eb;border-radius:.5rem;padding:1.25rem;transition:all .2s;box-shadow:0 1px 2px #0000000d}[data-theme=dark] .stat-box{background-color:#1f2937;border-color:#374151}.stat-box:hover{transform:translateY(-.25rem);box-shadow:0 10px 15px -3px #0000001a,0 4px 6px -4px #0000001a}.stat-value{color:#111827;font-size:1.875rem;font-weight:700}[data-theme=dark] .stat-value{color:#f3f4f6}.stat-label{text-transform:uppercase;letter-spacing:.05em;color:#6b7280;margin-top:.5rem;font-size:.75rem}[data-theme=dark] .stat-label{color:#9ca3af}.metric-box{text-align:center;background-color:#f9fafb;border:1px solid #e5e7eb;border-radius:.5rem;padding:.75rem}[data-theme=dark] .metric-box{background-color:#374151;border-color:#4b5563}.metric-label{text-transform:uppercase;letter-spacing:.05em;color:#6b7280;margin-bottom:.25rem;font-size:.75rem;display:block}[data-theme=dark] .metric-label{color:#9ca3af}.metric-value{color:#111827;font-size:.875rem;font-weight:600;display:block}[data-theme=dark] .metric-value{color:#f3f4f6}.table{border-collapse:collapse;background-color:#fff;border-radius:.5rem;width:100%;overflow:hidden;box-shadow:0 1px 2px #0000000d}[data-theme=dark] .table{background-color:#1f2937}.table thead{background-color:#f9fafb}[data-theme=dark] .table thead{background-color:#374151}.table th{text-align:left;text-transform:uppercase;letter-spacing:.05em;color:#6b7280;border-bottom:2px solid #e5e7eb;padding:.75rem 1rem;font-size:.75rem;font-weight:600}[data-theme=dark] .table th{color:#9ca3af;border-color:#4b5563}.table td{color:#111827;border-bottom:1px solid #e5e7eb;padding:.75rem 1rem}[data-theme=dark] .table td{color:#f3f4f6;border-color:#374151}.table tbody tr{transition:background-color .15s}.table tbody tr:hover{background-color:#f9fafb}[data-theme=dark] .table tbody tr:hover{background-color:#374151}.table tbody tr:nth-child(2n){background-color:#f9fafb}[data-theme=dark] .table tbody tr:nth-child(2n){background-color:#1f293780}.badge{border-radius:.25rem;padding:.25rem .5rem;font-size:.75rem;font-weight:500;display:inline-block}.badge-blue{color:#1e40af;background-color:#dbeafe}[data-theme=dark] .badge-blue{color:#93c5fd;background-color:#3b82f64d}.badge-green{color:#166534;background-color:#dcfce7}[data-theme=dark] .badge-green{color:#86efac;background-color:#22c55e4d}.badge-amber{color:#92400e;background-color:#fef3c7}[data-theme=dark] .badge-amber{color:#fcd34d;background-color:#f59e0b4d}.badge-red{color:#991b1b;background-color:#fee2e2}[data-theme=dark] .badge-red{color:#fca5a5;background-color:#ef44444d}.badge-purple{color:#7c3aed;background-color:#f3e8ff}[data-theme=dark] .badge-purple{color:#c4b5fd;background-color:#8b5cf64d}.badge-gray{color:#4b5563;background-color:#f3f4f6}[data-theme=dark] .badge-gray{color:#d1d5db;background-color:#374151}.code{background-color:#e5e7eb;border-radius:.25rem;padding:.125rem .25rem;font-family:ui-monospace,SFMono-Regular,SF Mono,Menlo,Consolas,monospace;font-size:.875rem}[data-theme=dark] .code{background-color:#374151}.modal-overlay{z-index:50;background-color:#00000080;justify-content:center;align-items:center;display:flex;position:fixed;inset:0}.modal-overlay.hidden{display:none}.modal{background-color:#fff;border-radius:.75rem;flex-direction:column;width:100%;max-width:56rem;max-height:90vh;margin:1rem;display:flex;box-shadow:0 25px 50px -12px #00000040}[data-theme=dark] .modal{background-color:#1f2937}.modal-header{border-bottom:1px solid #e5e7eb;justify-content:space-between;align-items:center;padding:1rem 1.5rem;display:flex}[data-theme=dark] .modal-header{border-color:#374151}.modal-title{color:#111827;font-size:1.125rem;font-weight:600}[data-theme=dark] .modal-title{color:#f3f4f6}.modal-subtitle{color:#6b7280;font-size:.875rem}[data-theme=dark] .modal-subtitle{color:#9ca3af}.modal-close{color:#9ca3af;cursor:pointer;background:0 0;border:none;padding:.5rem;transition:color .15s}.modal-close:hover{color:#4b5563}[data-theme=dark] .modal-close:hover{color:#e5e7eb}.modal-body{flex-direction:column;flex:1;padding:1.5rem;display:flex;overflow:hidden}.modal-footer{border-top:1px solid #e5e7eb;justify-content:flex-end;gap:.75rem;padding:1rem 1.5rem;display:flex}[data-theme=dark] .modal-footer{border-color:#374151}.dropzone{text-align:center;cursor:pointer;background-color:#f9fafb;border:2px dashed #d1d5db;border-radius:.75rem;padding:2rem;transition:all .2s}[data-theme=dark] .dropzone{background-color:#1f293780;border-color:#4b5563}.dropzone:hover{background-color:#eff6ff;border-color:#3b82f6;transform:scale(1.01);box-shadow:0 10px 15px -3px #0000001a}[data-theme=dark] .dropzone:hover{background-color:#3b82f61a}.dropzone.drag-over{background-color:#eff6ff;border-color:#3b82f6}[data-theme=dark] .dropzone.drag-over{background-color:#1e3a5f}.progress{background-color:#e5e7eb;border-radius:.375rem;height:.625rem;overflow:hidden}[data-theme=dark] .progress{background-color:#374151}.progress-bar{background:linear-gradient(45deg,#ffffff26 25%,#0000 25% 50%,#ffffff26 50% 75%,#0000 75%,#0000) 0 0/1rem 1rem;border-radius:.375rem;height:100%;transition:width .3s;animation:1s linear infinite progress-stripe}.spinner{border:4px solid #e5e7eb;border-top-color:#3b82f6;border-radius:50%;animation:1s linear infinite spin}.spinner-sm{border-width:2px;width:1.5rem;height:1.5rem}.spinner-md{width:2.5rem;height:2.5rem}.spinner-lg{width:3rem;height:3rem}@keyframes spin{to{transform:rotate(360deg)}}.tooltip-trigger{cursor:help;color:#6b7280;background-color:#e5e7eb;border-radius:9999px;justify-content:center;align-items:center;width:1rem;height:1rem;margin-left:.25rem;font-size:.65rem;display:inline-flex;position:relative}[data-theme=dark] .tooltip-trigger{color:#9ca3af;background-color:#4b5563}.tooltip-content{white-space:nowrap;opacity:0;visibility:hidden;z-index:100;color:#fff;background-color:#1f2937;border-radius:.375rem;max-width:300px;margin-bottom:.5rem;padding:.5rem .75rem;font-size:.75rem;font-weight:400;transition:opacity .2s,visibility .2s;position:absolute;bottom:100%;left:50%;transform:translate(-50%)}[data-theme=dark] .tooltip-content{color:#1f2937;background-color:#f3f4f6}.tooltip-content:after{content:"";border:6px solid #0000;border-top-color:#1f2937;position:absolute;top:100%;left:50%;transform:translate(-50%)}[data-theme=dark] .tooltip-content:after{border-top-color:#f3f4f6}.tooltip-multiline{white-space:normal;text-align:left;line-height:1.4}.tooltip-trigger:hover .tooltip-content,.tooltip-trigger:focus .tooltip-content{opacity:1;visibility:visible}.loading-overlay{z-index:9999;background:#00000080;justify-content:center;align-items:center;display:none;position:fixed;inset:0}.loading-overlay.active{display:flex}.file-upload__input{opacity:0;cursor:pointer;width:100%;height:100%;position:absolute;top:0;left:0}.file-upload__dropzone{position:relative}.file-upload__item{border-radius:.5rem;justify-content:space-between;align-items:center;margin-bottom:.5rem;padding:.75rem 1rem;display:flex}.file-upload__item-info{align-items:center;gap:.75rem;display:flex}.file-upload__item-icon{border-radius:.375rem;justify-content:center;align-items:center;width:2rem;height:2rem;display:flex}.file-upload__item-progress{border-radius:2px;height:4px;margin-top:.5rem;overflow:hidden}.file-upload__item-progress-bar{height:100%;transition:width .3s}details.error-details summary{list-style:none}details.error-details summary::-webkit-details-marker{display:none}details.error-details[open] .error-details__chevron{transform:rotate(180deg)}input:focus,select:focus,textarea:focus{outline:none;box-shadow:0 0 0 2px #fff,0 0 0 4px #3b82f6}[data-theme=dark] input:focus,[data-theme=dark] select:focus,[data-theme=dark] textarea:focus{box-shadow:0 0 0 2px #1f2937,0 0 0 4px #3b82f6}.modal-close:focus{outline:none}.modal-close:focus-visible{border-radius:.375rem;box-shadow:0 0 0 2px #fff,0 0 0 4px #3b82f6}[data-theme=dark] .modal-close:focus-visible{box-shadow:0 0 0 2px #1f2937,0 0 0 4px #3b82f6}.preview-tab:focus{outline:none}.preview-tab:focus-visible{border-radius:.25rem;box-shadow:0 0 0 2px #fff,0 0 0 4px #3b82f6}[data-theme=dark] .preview-tab:focus-visible{box-shadow:0 0 0 2px #1f2937,0 0 0 4px #3b82f6}a:focus{outline:none}a:focus-visible{border-radius:.25rem;box-shadow:0 0 0 2px #fff,0 0 0 4px #3b82f6}[data-theme=dark] a:focus-visible{box-shadow:0 0 0 2px #1f2937,0 0 0 4px #3b82f6}.dropzone:focus-within{border-color:#3b82f6;box-shadow:0 0 0 2px #fff,0 0 0 4px #3b82f6}[data-theme=dark] .dropzone:focus-within{box-shadow:0 0 0 2px #1f2937,0 0 0 4px #3b82f6}details.error-details summary:focus{outline:none}details.error-details summary:focus-visible{border-radius:.375rem;box-shadow:0 0 0 2px #fff,0 0 0 4px #3b82f6}[data-theme=dark] details.error-details summary:focus-visible{box-shadow:0 0 0 2px #1f2937,0 0 0 4px #3b82f6}.btn-blue:focus-visible{box-shadow:0 0 0 2px #fff,0 0 0 4px #fbbf24}[data-theme=dark] .btn-blue:focus-visible{box-shadow:0 0 0 2px #1f2937,0 0 0 4px #fbbf24}::-webkit-scrollbar{width:8px;height:8px}::-webkit-scrollbar-track{background:#f1f1f1;border-radius:4px}::-webkit-scrollbar-thumb{background:#c1c1c1;border-radius:4px}::-webkit-scrollbar-thumb:hover{background:#a1a1a1}[data-theme=dark] ::-webkit-scrollbar-track{background:#2d2d3f}[data-theme=dark] ::-webkit-scrollbar-thumb{background:#4a4a5a}[data-theme=dark] ::-webkit-scrollbar-thumb:hover{background:#5a5a6a}.text-muted{color:#6b7280}[data-theme=dark] .text-muted{color:#9ca3af}.flex-center{justify-content:center;align-items:center;display:flex}.container-70{width:100%;max-width:70vw;margin-left:auto;margin-right:auto}@media (max-width:768px){.container-70{max-width:95vw}}@property --tw-translate-x{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-y{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-z{syntax:"*";inherits:false;initial-value:0}@property --tw-rotate-x{syntax:"*";inherits:false}@property --tw-rotate-y{syntax:"*";inherits:false}@property --tw-rotate-z{syntax:"*";inherits:false}@property --tw-skew-x{syntax:"*";inherits:false}@property --tw-skew-y{syntax:"*";inherits:false}@property --tw-space-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-divide-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-gradient-position{syntax:"*";inherits:false}@property --tw-gradient-from{syntax:"";inherits:false;initial-value:#0000}@property --tw-gradient-via{syntax:"";inherits:false;initial-value:#0000}@property --tw-gradient-to{syntax:"";inherits:false;initial-value:#0000}@property --tw-gradient-stops{syntax:"*";inherits:false}@property --tw-gradient-via-stops{syntax:"*";inherits:false}@property --tw-gradient-from-position{syntax:"";inherits:false;initial-value:0%}@property --tw-gradient-via-position{syntax:"";inherits:false;initial-value:50%}@property --tw-gradient-to-position{syntax:"";inherits:false;initial-value:100%}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-tracking{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false}@property --tw-duration{syntax:"*";inherits:false}@property --tw-scale-x{syntax:"*";inherits:false;initial-value:1}@property --tw-scale-y{syntax:"*";inherits:false;initial-value:1}@property --tw-scale-z{syntax:"*";inherits:false;initial-value:1}@keyframes pulse{50%{opacity:.5}} \ No newline at end of file +@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0;--tw-space-y-reverse:0;--tw-border-style:solid;--tw-gradient-position:initial;--tw-gradient-from:#0000;--tw-gradient-via:#0000;--tw-gradient-to:#0000;--tw-gradient-stops:initial;--tw-gradient-via-stops:initial;--tw-gradient-from-position:0%;--tw-gradient-via-position:50%;--tw-gradient-to-position:100%;--tw-font-weight:initial;--tw-tracking:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-duration:initial;--tw-scale-x:1;--tw-scale-y:1;--tw-scale-z:1}}}@layer theme{:root,:host{--font-sans:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--font-mono:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;--color-red-50:oklch(97.1% .013 17.38);--color-red-100:oklch(93.6% .032 17.717);--color-red-200:oklch(88.5% .062 18.334);--color-red-300:oklch(80.8% .114 19.571);--color-red-400:oklch(70.4% .191 22.216);--color-red-500:oklch(63.7% .237 25.331);--color-red-600:oklch(57.7% .245 27.325);--color-red-700:oklch(50.5% .213 27.518);--color-red-800:oklch(44.4% .177 26.899);--color-red-900:oklch(39.6% .141 25.723);--color-amber-50:oklch(98.7% .022 95.277);--color-amber-100:oklch(96.2% .059 95.617);--color-amber-200:oklch(92.4% .12 95.746);--color-amber-300:oklch(87.9% .169 91.605);--color-amber-400:oklch(82.8% .189 84.429);--color-amber-500:oklch(76.9% .188 70.08);--color-amber-600:oklch(66.6% .179 58.318);--color-amber-700:oklch(55.5% .163 48.998);--color-amber-800:oklch(47.3% .137 46.201);--color-amber-900:oklch(41.4% .112 45.904);--color-green-50:oklch(98.2% .018 155.826);--color-green-100:oklch(96.2% .044 156.743);--color-green-200:oklch(92.5% .084 155.995);--color-green-300:oklch(87.1% .15 154.449);--color-green-400:oklch(79.2% .209 151.711);--color-green-500:oklch(72.3% .219 149.579);--color-green-600:oklch(62.7% .194 149.214);--color-green-700:oklch(52.7% .154 150.069);--color-green-800:oklch(44.8% .119 151.328);--color-green-900:oklch(39.3% .095 152.535);--color-cyan-100:oklch(95.6% .045 203.388);--color-cyan-200:oklch(91.7% .08 205.041);--color-cyan-500:oklch(71.5% .143 215.221);--color-cyan-600:oklch(60.9% .126 221.723);--color-blue-50:oklch(97% .014 254.604);--color-blue-100:oklch(93.2% .032 255.585);--color-blue-200:oklch(88.2% .059 254.128);--color-blue-300:oklch(80.9% .105 251.813);--color-blue-400:oklch(70.7% .165 254.624);--color-blue-500:oklch(62.3% .214 259.815);--color-blue-600:oklch(54.6% .245 262.881);--color-blue-700:oklch(48.8% .243 264.376);--color-blue-800:oklch(42.4% .199 265.638);--color-blue-900:oklch(37.9% .146 265.522);--color-indigo-500:oklch(58.5% .233 277.117);--color-purple-100:oklch(94.6% .033 307.174);--color-purple-200:oklch(90.2% .063 306.703);--color-purple-800:oklch(43.8% .218 303.724);--color-purple-900:oklch(38.1% .176 304.987);--color-rose-900:oklch(41% .159 10.272);--color-gray-50:oklch(98.5% .002 247.839);--color-gray-100:oklch(96.7% .003 264.542);--color-gray-200:oklch(92.8% .006 264.531);--color-gray-300:oklch(87.2% .01 258.338);--color-gray-400:oklch(70.7% .022 261.325);--color-gray-500:oklch(55.1% .027 264.364);--color-gray-600:oklch(44.6% .03 256.802);--color-gray-700:oklch(37.3% .034 259.733);--color-gray-800:oklch(27.8% .033 256.848);--color-gray-900:oklch(21% .034 264.665);--color-black:#000;--color-white:#fff;--spacing:.25rem;--text-xs:.75rem;--text-xs--line-height:calc(1/.75);--text-sm:.875rem;--text-sm--line-height:calc(1.25/.875);--text-lg:1.125rem;--text-lg--line-height:calc(1.75/1.125);--text-xl:1.25rem;--text-xl--line-height:calc(1.75/1.25);--text-2xl:1.5rem;--text-2xl--line-height:calc(2/1.5);--font-weight-medium:500;--font-weight-semibold:600;--font-weight-bold:700;--tracking-wide:.025em;--tracking-wider:.05em;--radius-md:.375rem;--radius-lg:.5rem;--radius-xl:.75rem;--animate-spin:spin 1s linear infinite;--animate-pulse:pulse 2s cubic-bezier(.4,0,.6,1)infinite;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4,0,.2,1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono);--animate-progress-stripe:progress-stripe 1s linear infinite}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab, red, red)){::placeholder{color:color-mix(in oklab,currentcolor 50%,transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}}@layer components;@layer utilities{.visible{visibility:visible}.absolute{position:absolute}.fixed{position:fixed}.relative{position:relative}.static{position:static}.sticky{position:sticky}.inset-0{inset:calc(var(--spacing)*0)}.top-0{top:calc(var(--spacing)*0)}.z-50{z-index:50}.container{width:100%}@media (min-width:40rem){.container{max-width:40rem}}@media (min-width:48rem){.container{max-width:48rem}}@media (min-width:64rem){.container{max-width:64rem}}@media (min-width:80rem){.container{max-width:80rem}}@media (min-width:96rem){.container{max-width:96rem}}.mx-2{margin-inline:calc(var(--spacing)*2)}.mx-auto{margin-inline:auto}.my-6{margin-block:calc(var(--spacing)*6)}.mt-1{margin-top:calc(var(--spacing)*1)}.mt-2{margin-top:calc(var(--spacing)*2)}.mt-3{margin-top:calc(var(--spacing)*3)}.mt-4{margin-top:calc(var(--spacing)*4)}.mr-1{margin-right:calc(var(--spacing)*1)}.mr-2{margin-right:calc(var(--spacing)*2)}.mb-2{margin-bottom:calc(var(--spacing)*2)}.mb-3{margin-bottom:calc(var(--spacing)*3)}.mb-4{margin-bottom:calc(var(--spacing)*4)}.mb-6{margin-bottom:calc(var(--spacing)*6)}.ml-1{margin-left:calc(var(--spacing)*1)}.ml-2{margin-left:calc(var(--spacing)*2)}.ml-4{margin-left:calc(var(--spacing)*4)}.block{display:block}.contents{display:contents}.flex{display:flex}.grid{display:grid}.hidden{display:none}.inline{display:inline}.inline-block{display:inline-block}.inline-flex{display:inline-flex}.table{display:table}.h-2\.5{height:calc(var(--spacing)*2.5)}.h-4{height:calc(var(--spacing)*4)}.h-5{height:calc(var(--spacing)*5)}.h-6{height:calc(var(--spacing)*6)}.h-8{height:calc(var(--spacing)*8)}.h-10{height:calc(var(--spacing)*10)}.h-12{height:calc(var(--spacing)*12)}.h-full{height:100%}.min-h-screen{min-height:100vh}.w-4{width:calc(var(--spacing)*4)}.w-5{width:calc(var(--spacing)*5)}.w-6{width:calc(var(--spacing)*6)}.w-8{width:calc(var(--spacing)*8)}.w-10{width:calc(var(--spacing)*10)}.w-12{width:calc(var(--spacing)*12)}.w-full{width:100%}.min-w-\[44px\]{min-width:44px}.flex-1{flex:1}.flex-shrink-0{flex-shrink:0}.-translate-y-1{--tw-translate-y:calc(var(--spacing)*-1);translate:var(--tw-translate-x)var(--tw-translate-y)}.animate-progress-stripe{animation:var(--animate-progress-stripe)}.animate-pulse{animation:var(--animate-pulse)}.animate-spin{animation:var(--animate-spin)}.cursor-help{cursor:help}.cursor-pointer{cursor:pointer}.list-inside{list-style-position:inside}.list-decimal{list-style-type:decimal}.list-disc{list-style-type:disc}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-center{align-items:center}.items-start{align-items:flex-start}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.gap-1{gap:calc(var(--spacing)*1)}.gap-2{gap:calc(var(--spacing)*2)}.gap-3{gap:calc(var(--spacing)*3)}.gap-4{gap:calc(var(--spacing)*4)}:where(.space-y-1>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*1)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*1)*calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-2>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*2)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*2)*calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-3>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*3)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*3)*calc(1 - var(--tw-space-y-reverse)))}.truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.overflow-auto{overflow:auto}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.rounded{border-radius:.25rem}.rounded-full{border-radius:3.40282e38px}.rounded-lg{border-radius:var(--radius-lg)}.rounded-md{border-radius:var(--radius-md)}.rounded-xl{border-radius:var(--radius-xl)}.border{border-style:var(--tw-border-style);border-width:1px}.border-2{border-style:var(--tw-border-style);border-width:2px}.border-4{border-style:var(--tw-border-style);border-width:4px}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.border-b-2{border-bottom-style:var(--tw-border-style);border-bottom-width:2px}.border-blue-200{border-color:var(--color-blue-200)}.border-blue-500{border-color:var(--color-blue-500)}.border-gray-200{border-color:var(--color-gray-200)}.border-red-200{border-color:var(--color-red-200)}.border-transparent{border-color:#0000}.border-white\/30{border-color:#ffffff4d}@supports (color:color-mix(in lab, red, red)){.border-white\/30{border-color:color-mix(in oklab,var(--color-white)30%,transparent)}}.border-t-blue-600{border-top-color:var(--color-blue-600)}.bg-amber-50{background-color:var(--color-amber-50)}.bg-amber-100{background-color:var(--color-amber-100)}.bg-amber-500{background-color:var(--color-amber-500)}.bg-black\/50{background-color:#00000080}@supports (color:color-mix(in lab, red, red)){.bg-black\/50{background-color:color-mix(in oklab,var(--color-black)50%,transparent)}}.bg-blue-50{background-color:var(--color-blue-50)}.bg-blue-100{background-color:var(--color-blue-100)}.bg-blue-600{background-color:var(--color-blue-600)}.bg-cyan-500{background-color:var(--color-cyan-500)}.bg-gray-50{background-color:var(--color-gray-50)}.bg-gray-100{background-color:var(--color-gray-100)}.bg-gray-200{background-color:var(--color-gray-200)}.bg-gray-900{background-color:var(--color-gray-900)}.bg-green-50{background-color:var(--color-green-50)}.bg-green-100{background-color:var(--color-green-100)}.bg-green-500{background-color:var(--color-green-500)}.bg-purple-100{background-color:var(--color-purple-100)}.bg-red-50{background-color:var(--color-red-50)}.bg-red-100{background-color:var(--color-red-100)}.bg-red-200{background-color:var(--color-red-200)}.bg-white{background-color:var(--color-white)}.bg-white\/15{background-color:#ffffff26}@supports (color:color-mix(in lab, red, red)){.bg-white\/15{background-color:color-mix(in oklab,var(--color-white)15%,transparent)}}.bg-white\/20{background-color:#fff3}@supports (color:color-mix(in lab, red, red)){.bg-white\/20{background-color:color-mix(in oklab,var(--color-white)20%,transparent)}}.bg-gradient-to-r{--tw-gradient-position:to right in oklab;background-image:linear-gradient(var(--tw-gradient-stops))}.from-blue-500{--tw-gradient-from:var(--color-blue-500);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-cyan-600{--tw-gradient-from:var(--color-cyan-600);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.to-blue-600{--tw-gradient-to:var(--color-blue-600);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.to-rose-900{--tw-gradient-to:var(--color-rose-900);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.bg-\[length\:1rem_1rem\]{background-size:1rem 1rem}.fill-current{fill:currentColor}.fill-indigo-500{fill:var(--color-indigo-500)}.p-0{padding:calc(var(--spacing)*0)}.p-4{padding:calc(var(--spacing)*4)}.p-6{padding:calc(var(--spacing)*6)}.p-8{padding:calc(var(--spacing)*8)}.px-1\.5{padding-inline:calc(var(--spacing)*1.5)}.px-2{padding-inline:calc(var(--spacing)*2)}.px-3{padding-inline:calc(var(--spacing)*3)}.px-4{padding-inline:calc(var(--spacing)*4)}.px-8{padding-inline:calc(var(--spacing)*8)}.py-0\.5{padding-block:calc(var(--spacing)*.5)}.py-1{padding-block:calc(var(--spacing)*1)}.py-2{padding-block:calc(var(--spacing)*2)}.py-3{padding-block:calc(var(--spacing)*3)}.py-4{padding-block:calc(var(--spacing)*4)}.py-6{padding-block:calc(var(--spacing)*6)}.py-8{padding-block:calc(var(--spacing)*8)}.text-center{text-align:center}.text-left{text-align:left}.font-mono{font-family:var(--font-mono)}.text-2xl{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height))}.text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xl{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.font-bold{--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold)}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.tracking-wide{--tw-tracking:var(--tracking-wide);letter-spacing:var(--tracking-wide)}.tracking-wider{--tw-tracking:var(--tracking-wider);letter-spacing:var(--tracking-wider)}.whitespace-pre-wrap{white-space:pre-wrap}.text-amber-600{color:var(--color-amber-600)}.text-amber-700{color:var(--color-amber-700)}.text-amber-800{color:var(--color-amber-800)}.text-blue-500{color:var(--color-blue-500)}.text-blue-600{color:var(--color-blue-600)}.text-blue-700{color:var(--color-blue-700)}.text-blue-800{color:var(--color-blue-800)}.text-cyan-100{color:var(--color-cyan-100)}.text-gray-100{color:var(--color-gray-100)}.text-gray-400{color:var(--color-gray-400)}.text-gray-500{color:var(--color-gray-500)}.text-gray-600{color:var(--color-gray-600)}.text-gray-700{color:var(--color-gray-700)}.text-gray-800{color:var(--color-gray-800)}.text-gray-900{color:var(--color-gray-900)}.text-green-500{color:var(--color-green-500)}.text-green-600{color:var(--color-green-600)}.text-green-700{color:var(--color-green-700)}.text-green-800{color:var(--color-green-800)}.text-purple-800{color:var(--color-purple-800)}.text-red-500{color:var(--color-red-500)}.text-red-600{color:var(--color-red-600)}.text-red-700{color:var(--color-red-700)}.text-red-800{color:var(--color-red-800)}.text-white{color:var(--color-white)}.uppercase{text-transform:uppercase}.no-underline{text-decoration-line:none}.opacity-80{opacity:.8}.shadow{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a),0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-lg{--tw-shadow:0 10px 15px -3px var(--tw-shadow-color,#0000001a),0 4px 6px -4px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-md{--tw-shadow:0 4px 6px -1px var(--tw-shadow-color,#0000001a),0 2px 4px -2px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-sm{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a),0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-xl{--tw-shadow:0 20px 25px -5px var(--tw-shadow-color,#0000001a),0 8px 10px -6px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.ring{--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(1px + var(--tw-ring-offset-width))var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.transition-all{transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-colors{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-transform{transition-property:transform,translate,scale,rotate;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.duration-150{--tw-duration:.15s;transition-duration:.15s}.duration-300{--tw-duration:.3s;transition-duration:.3s}.even\:bg-gray-50:nth-child(2n){background-color:var(--color-gray-50)}@media (hover:hover){.hover\:scale-105:hover{--tw-scale-x:105%;--tw-scale-y:105%;--tw-scale-z:105%;scale:var(--tw-scale-x)var(--tw-scale-y)}.hover\:bg-amber-600:hover{background-color:var(--color-amber-600)}.hover\:bg-blue-700:hover{background-color:var(--color-blue-700)}.hover\:bg-cyan-600:hover{background-color:var(--color-cyan-600)}.hover\:bg-gray-50:hover{background-color:var(--color-gray-50)}.hover\:bg-green-600:hover{background-color:var(--color-green-600)}.hover\:bg-white\/25:hover{background-color:#ffffff40}@supports (color:color-mix(in lab, red, red)){.hover\:bg-white\/25:hover{background-color:color-mix(in oklab,var(--color-white)25%,transparent)}}.hover\:text-white:hover{color:var(--color-white)}}.active\:scale-95:active{--tw-scale-x:95%;--tw-scale-y:95%;--tw-scale-z:95%;scale:var(--tw-scale-x)var(--tw-scale-y)}@media (min-width:48rem){.md\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.md\:grid-cols-6{grid-template-columns:repeat(6,minmax(0,1fr))}}.dark\:border-blue-800:where([data-theme=dark],[data-theme=dark] *){border-color:var(--color-blue-800)}.dark\:border-gray-600:where([data-theme=dark],[data-theme=dark] *){border-color:var(--color-gray-600)}.dark\:border-gray-700:where([data-theme=dark],[data-theme=dark] *){border-color:var(--color-gray-700)}.dark\:border-red-800:where([data-theme=dark],[data-theme=dark] *){border-color:var(--color-red-800)}.dark\:bg-amber-900\/20:where([data-theme=dark],[data-theme=dark] *){background-color:#7b330633}@supports (color:color-mix(in lab, red, red)){.dark\:bg-amber-900\/20:where([data-theme=dark],[data-theme=dark] *){background-color:color-mix(in oklab,var(--color-amber-900)20%,transparent)}}.dark\:bg-amber-900\/30:where([data-theme=dark],[data-theme=dark] *){background-color:#7b33064d}@supports (color:color-mix(in lab, red, red)){.dark\:bg-amber-900\/30:where([data-theme=dark],[data-theme=dark] *){background-color:color-mix(in oklab,var(--color-amber-900)30%,transparent)}}.dark\:bg-amber-900\/50:where([data-theme=dark],[data-theme=dark] *){background-color:#7b330680}@supports (color:color-mix(in lab, red, red)){.dark\:bg-amber-900\/50:where([data-theme=dark],[data-theme=dark] *){background-color:color-mix(in oklab,var(--color-amber-900)50%,transparent)}}.dark\:bg-blue-800\/30:where([data-theme=dark],[data-theme=dark] *){background-color:#193cb84d}@supports (color:color-mix(in lab, red, red)){.dark\:bg-blue-800\/30:where([data-theme=dark],[data-theme=dark] *){background-color:color-mix(in oklab,var(--color-blue-800)30%,transparent)}}.dark\:bg-blue-900\/20:where([data-theme=dark],[data-theme=dark] *){background-color:#1c398e33}@supports (color:color-mix(in lab, red, red)){.dark\:bg-blue-900\/20:where([data-theme=dark],[data-theme=dark] *){background-color:color-mix(in oklab,var(--color-blue-900)20%,transparent)}}.dark\:bg-blue-900\/30:where([data-theme=dark],[data-theme=dark] *){background-color:#1c398e4d}@supports (color:color-mix(in lab, red, red)){.dark\:bg-blue-900\/30:where([data-theme=dark],[data-theme=dark] *){background-color:color-mix(in oklab,var(--color-blue-900)30%,transparent)}}.dark\:bg-blue-900\/50:where([data-theme=dark],[data-theme=dark] *){background-color:#1c398e80}@supports (color:color-mix(in lab, red, red)){.dark\:bg-blue-900\/50:where([data-theme=dark],[data-theme=dark] *){background-color:color-mix(in oklab,var(--color-blue-900)50%,transparent)}}.dark\:bg-gray-600:where([data-theme=dark],[data-theme=dark] *){background-color:var(--color-gray-600)}.dark\:bg-gray-700:where([data-theme=dark],[data-theme=dark] *){background-color:var(--color-gray-700)}.dark\:bg-gray-800:where([data-theme=dark],[data-theme=dark] *){background-color:var(--color-gray-800)}.dark\:bg-gray-900:where([data-theme=dark],[data-theme=dark] *){background-color:var(--color-gray-900)}.dark\:bg-green-900\/20:where([data-theme=dark],[data-theme=dark] *){background-color:#0d542b33}@supports (color:color-mix(in lab, red, red)){.dark\:bg-green-900\/20:where([data-theme=dark],[data-theme=dark] *){background-color:color-mix(in oklab,var(--color-green-900)20%,transparent)}}.dark\:bg-green-900\/30:where([data-theme=dark],[data-theme=dark] *){background-color:#0d542b4d}@supports (color:color-mix(in lab, red, red)){.dark\:bg-green-900\/30:where([data-theme=dark],[data-theme=dark] *){background-color:color-mix(in oklab,var(--color-green-900)30%,transparent)}}.dark\:bg-green-900\/50:where([data-theme=dark],[data-theme=dark] *){background-color:#0d542b80}@supports (color:color-mix(in lab, red, red)){.dark\:bg-green-900\/50:where([data-theme=dark],[data-theme=dark] *){background-color:color-mix(in oklab,var(--color-green-900)50%,transparent)}}.dark\:bg-purple-900\/50:where([data-theme=dark],[data-theme=dark] *){background-color:#59168b80}@supports (color:color-mix(in lab, red, red)){.dark\:bg-purple-900\/50:where([data-theme=dark],[data-theme=dark] *){background-color:color-mix(in oklab,var(--color-purple-900)50%,transparent)}}.dark\:bg-red-800:where([data-theme=dark],[data-theme=dark] *){background-color:var(--color-red-800)}.dark\:bg-red-900\/20:where([data-theme=dark],[data-theme=dark] *){background-color:#82181a33}@supports (color:color-mix(in lab, red, red)){.dark\:bg-red-900\/20:where([data-theme=dark],[data-theme=dark] *){background-color:color-mix(in oklab,var(--color-red-900)20%,transparent)}}.dark\:bg-red-900\/30:where([data-theme=dark],[data-theme=dark] *){background-color:#82181a4d}@supports (color:color-mix(in lab, red, red)){.dark\:bg-red-900\/30:where([data-theme=dark],[data-theme=dark] *){background-color:color-mix(in oklab,var(--color-red-900)30%,transparent)}}.dark\:bg-red-900\/40:where([data-theme=dark],[data-theme=dark] *){background-color:#82181a66}@supports (color:color-mix(in lab, red, red)){.dark\:bg-red-900\/40:where([data-theme=dark],[data-theme=dark] *){background-color:color-mix(in oklab,var(--color-red-900)40%,transparent)}}.dark\:bg-red-900\/50:where([data-theme=dark],[data-theme=dark] *){background-color:#82181a80}@supports (color:color-mix(in lab, red, red)){.dark\:bg-red-900\/50:where([data-theme=dark],[data-theme=dark] *){background-color:color-mix(in oklab,var(--color-red-900)50%,transparent)}}.dark\:text-amber-200:where([data-theme=dark],[data-theme=dark] *){color:var(--color-amber-200)}.dark\:text-amber-300:where([data-theme=dark],[data-theme=dark] *){color:var(--color-amber-300)}.dark\:text-amber-400:where([data-theme=dark],[data-theme=dark] *){color:var(--color-amber-400)}.dark\:text-blue-200:where([data-theme=dark],[data-theme=dark] *){color:var(--color-blue-200)}.dark\:text-blue-300:where([data-theme=dark],[data-theme=dark] *){color:var(--color-blue-300)}.dark\:text-blue-400:where([data-theme=dark],[data-theme=dark] *){color:var(--color-blue-400)}.dark\:text-cyan-200:where([data-theme=dark],[data-theme=dark] *){color:var(--color-cyan-200)}.dark\:text-gray-100:where([data-theme=dark],[data-theme=dark] *){color:var(--color-gray-100)}.dark\:text-gray-200:where([data-theme=dark],[data-theme=dark] *){color:var(--color-gray-200)}.dark\:text-gray-300:where([data-theme=dark],[data-theme=dark] *){color:var(--color-gray-300)}.dark\:text-gray-400:where([data-theme=dark],[data-theme=dark] *){color:var(--color-gray-400)}.dark\:text-green-200:where([data-theme=dark],[data-theme=dark] *){color:var(--color-green-200)}.dark\:text-green-300:where([data-theme=dark],[data-theme=dark] *){color:var(--color-green-300)}.dark\:text-green-400:where([data-theme=dark],[data-theme=dark] *){color:var(--color-green-400)}.dark\:text-purple-200:where([data-theme=dark],[data-theme=dark] *){color:var(--color-purple-200)}.dark\:text-red-200:where([data-theme=dark],[data-theme=dark] *){color:var(--color-red-200)}.dark\:text-red-300:where([data-theme=dark],[data-theme=dark] *){color:var(--color-red-300)}.dark\:text-red-400:where([data-theme=dark],[data-theme=dark] *){color:var(--color-red-400)}.dark\:even\:bg-gray-800\/50:where([data-theme=dark],[data-theme=dark] *):nth-child(2n){background-color:#1e293980}@supports (color:color-mix(in lab, red, red)){.dark\:even\:bg-gray-800\/50:where([data-theme=dark],[data-theme=dark] *):nth-child(2n){background-color:color-mix(in oklab,var(--color-gray-800)50%,transparent)}}@media (hover:hover){.dark\:hover\:bg-gray-700:where([data-theme=dark],[data-theme=dark] *):hover{background-color:var(--color-gray-700)}}}@keyframes progress-stripe{0%{background-position:1rem 0}to{background-position:0 0}}.icon{vertical-align:middle;fill:currentColor;width:1em;height:1em;display:inline-block}.icon-lg{width:1.5em;height:1.5em}*,:before,:after{transition:background-color .3s,border-color .3s,color .3s}.btn{text-align:center;cursor:pointer;border:none;border-radius:.375rem;justify-content:center;align-items:center;padding:.5rem 1rem;font-size:.875rem;font-weight:500;line-height:1.25rem;text-decoration:none;transition:all .15s;display:inline-flex}.btn:hover{transform:scale(1.05);box-shadow:0 10px 15px -3px #0000001a,0 4px 6px -4px #0000001a}.btn:active{transform:scale(.95)}.btn:focus{outline-offset:2px;outline:2px solid #0000}.btn:focus-visible{box-shadow:0 0 0 2px #fff,0 0 0 4px #3b82f6}.btn-sm{padding:.375rem .75rem;font-size:.75rem}.btn-icon{min-width:2.25rem;min-height:2.25rem;vertical-align:middle;padding:.5rem .75rem}.btn-blue{color:#fff;background-color:#2563eb}.btn-blue:hover{background-color:#1d4ed8}.btn-green{color:#fff;background-color:#22c55e}.btn-green:hover{background-color:#16a34a}.btn-red{color:#fff;background-color:#ef4444}.btn-red:hover{background-color:#dc2626}.btn-amber{color:#fff;background-color:#f59e0b}.btn-amber:hover{background-color:#d97706}.btn-cyan{color:#fff;background-color:#06b6d4}.btn-cyan:hover{background-color:#0891b2}.btn-purple{color:#fff;background-color:#a855f7}.btn-purple:hover{background-color:#9333ea}.btn-indigo{color:#fff;background-color:#6366f1}.btn-indigo:hover{background-color:#4f46e5}.btn-gray{color:#fff;background-color:#6b7280}.btn-gray:hover{background-color:#4b5563}.btn-secondary{color:#374151;background-color:#e5e7eb}.btn-secondary:hover{background-color:#d1d5db}[data-theme=dark] .btn-secondary{color:#e5e7eb;background-color:#374151}[data-theme=dark] .btn-secondary:hover{background-color:#4b5563}.btn-danger-ghost{color:#b91c1c;background-color:#fee2e2}.btn-danger-ghost:hover{background-color:#fecaca}[data-theme=dark] .btn-danger-ghost{color:#fca5a5;background-color:#7f1d1d4d}[data-theme=dark] .btn-danger-ghost:hover{background-color:#7f1d1d80}.alert{border-style:solid;border-width:1px;border-radius:.5rem;margin-bottom:1rem;padding:.75rem 1rem;font-size:.875rem;line-height:1.25rem}.alert-success{color:#166534;background-color:#dcfce7;border-color:#86efac}[data-theme=dark] .alert-success{color:#bbf7d0;background-color:#22c55e33;border-color:#166534}.alert-error{color:#991b1b;background-color:#fee2e2;border-color:#fca5a5}[data-theme=dark] .alert-error{color:#fecaca;background-color:#ef444433;border-color:#7f1d1d}.alert-warning{color:#92400e;background-color:#fef3c7;border-color:#fcd34d}[data-theme=dark] .alert-warning{color:#fde68a;background-color:#f59e0b33;border-color:#78350f}.alert-info{color:#155e75;background-color:#ecfeff;border-color:#a5f3fc}[data-theme=dark] .alert-info{color:#a5f3fc;background-color:#06b6d433;border-color:#164e63}.alert-lg{border-radius:.75rem;margin-bottom:1.5rem;padding:1.5rem}.card{background-color:#fff;border:1px solid #e5e7eb;border-radius:.5rem;box-shadow:0 1px 2px #0000000d}[data-theme=dark] .card{background-color:#1f2937;border-color:#374151}.card-body{padding:1.25rem}.card-header{text-transform:uppercase;letter-spacing:.05em;color:#6b7280;border-bottom:1px solid #e5e7eb;padding:.75rem 1.5rem;font-size:.75rem;font-weight:600}[data-theme=dark] .card-header{color:#9ca3af;border-color:#374151}.info-box{color:#fff;background:linear-gradient(90deg,#0891b2,#0e7490);border-radius:.75rem;margin-bottom:1.5rem;padding:1rem 1.5rem}[data-theme=dark] .info-box{background:linear-gradient(90deg,#155e75,#164e63)}.stat-box{text-align:center;background-color:#fff;border:1px solid #e5e7eb;border-radius:.5rem;padding:1.25rem;transition:all .2s;box-shadow:0 1px 2px #0000000d}[data-theme=dark] .stat-box{background-color:#1f2937;border-color:#374151}.stat-box:hover{transform:translateY(-.25rem);box-shadow:0 10px 15px -3px #0000001a,0 4px 6px -4px #0000001a}.stat-value{color:#111827;font-size:1.875rem;font-weight:700}[data-theme=dark] .stat-value{color:#f3f4f6}.stat-label{text-transform:uppercase;letter-spacing:.05em;color:#6b7280;margin-top:.5rem;font-size:.75rem}[data-theme=dark] .stat-label{color:#9ca3af}.metric-box{text-align:center;background-color:#f9fafb;border:1px solid #e5e7eb;border-radius:.5rem;padding:.75rem}[data-theme=dark] .metric-box{background-color:#374151;border-color:#4b5563}.metric-label{text-transform:uppercase;letter-spacing:.05em;color:#6b7280;margin-bottom:.25rem;font-size:.75rem;display:block}[data-theme=dark] .metric-label{color:#9ca3af}.metric-value{color:#111827;font-size:.875rem;font-weight:600;display:block}[data-theme=dark] .metric-value{color:#f3f4f6}.table{border-collapse:collapse;background-color:#fff;border-radius:.5rem;width:100%;overflow:hidden;box-shadow:0 1px 2px #0000000d}[data-theme=dark] .table{background-color:#1f2937}.table thead{background-color:#f9fafb}[data-theme=dark] .table thead{background-color:#374151}.table th{text-align:left;text-transform:uppercase;letter-spacing:.05em;color:#6b7280;border-bottom:2px solid #e5e7eb;padding:.75rem 1rem;font-size:.75rem;font-weight:600}[data-theme=dark] .table th{color:#9ca3af;border-color:#4b5563}.table td{color:#111827;border-bottom:1px solid #e5e7eb;padding:.75rem 1rem}[data-theme=dark] .table td{color:#f3f4f6;border-color:#374151}.table tbody tr{transition:background-color .15s}.table tbody tr:hover{background-color:#f9fafb}[data-theme=dark] .table tbody tr:hover{background-color:#374151}.table tbody tr:nth-child(2n){background-color:#f9fafb}[data-theme=dark] .table tbody tr:nth-child(2n){background-color:#1f293780}.badge{border-radius:.25rem;padding:.25rem .5rem;font-size:.75rem;font-weight:500;display:inline-block}.badge-blue{color:#1e40af;background-color:#dbeafe}[data-theme=dark] .badge-blue{color:#93c5fd;background-color:#3b82f64d}.badge-green{color:#166534;background-color:#dcfce7}[data-theme=dark] .badge-green{color:#86efac;background-color:#22c55e4d}.badge-amber{color:#92400e;background-color:#fef3c7}[data-theme=dark] .badge-amber{color:#fcd34d;background-color:#f59e0b4d}.badge-red{color:#991b1b;background-color:#fee2e2}[data-theme=dark] .badge-red{color:#fca5a5;background-color:#ef44444d}.badge-purple{color:#7c3aed;background-color:#f3e8ff}[data-theme=dark] .badge-purple{color:#c4b5fd;background-color:#8b5cf64d}.badge-gray{color:#4b5563;background-color:#f3f4f6}[data-theme=dark] .badge-gray{color:#d1d5db;background-color:#374151}.code{background-color:#e5e7eb;border-radius:.25rem;padding:.125rem .25rem;font-family:ui-monospace,SFMono-Regular,SF Mono,Menlo,Consolas,monospace;font-size:.875rem}[data-theme=dark] .code{background-color:#374151}.modal-overlay{z-index:50;background-color:#00000080;justify-content:center;align-items:center;display:flex;position:fixed;inset:0}.modal-overlay.hidden{display:none}.modal{background-color:#fff;border-radius:.75rem;flex-direction:column;width:100%;max-width:56rem;max-height:90vh;margin:1rem;display:flex;box-shadow:0 25px 50px -12px #00000040}[data-theme=dark] .modal{background-color:#1f2937}.modal-header{border-bottom:1px solid #e5e7eb;justify-content:space-between;align-items:center;padding:1rem 1.5rem;display:flex}[data-theme=dark] .modal-header{border-color:#374151}.modal-title{color:#111827;font-size:1.125rem;font-weight:600}[data-theme=dark] .modal-title{color:#f3f4f6}.modal-subtitle{color:#6b7280;font-size:.875rem}[data-theme=dark] .modal-subtitle{color:#9ca3af}.modal-close{color:#9ca3af;cursor:pointer;background:0 0;border:none;padding:.5rem;transition:color .15s}.modal-close:hover{color:#4b5563}[data-theme=dark] .modal-close:hover{color:#e5e7eb}.modal-body{flex-direction:column;flex:1;padding:1.5rem;display:flex;overflow:hidden}.modal-footer{border-top:1px solid #e5e7eb;justify-content:flex-end;gap:.75rem;padding:1rem 1.5rem;display:flex}[data-theme=dark] .modal-footer{border-color:#374151}.dropzone{text-align:center;cursor:pointer;background-color:#f9fafb;border:2px dashed #d1d5db;border-radius:.75rem;padding:2rem;transition:all .2s}[data-theme=dark] .dropzone{background-color:#1f293780;border-color:#4b5563}.dropzone:hover{background-color:#eff6ff;border-color:#3b82f6;transform:scale(1.01);box-shadow:0 10px 15px -3px #0000001a}[data-theme=dark] .dropzone:hover{background-color:#3b82f61a}.dropzone.drag-over{background-color:#eff6ff;border-color:#3b82f6}[data-theme=dark] .dropzone.drag-over{background-color:#1e3a5f}.progress{background-color:#e5e7eb;border-radius:.375rem;height:.625rem;overflow:hidden}[data-theme=dark] .progress{background-color:#374151}.progress-bar{background:linear-gradient(45deg,#ffffff26 25%,#0000 25% 50%,#ffffff26 50% 75%,#0000 75%,#0000) 0 0/1rem 1rem;border-radius:.375rem;height:100%;transition:width .3s;animation:1s linear infinite progress-stripe}.spinner{border:4px solid #e5e7eb;border-top-color:#3b82f6;border-radius:50%;animation:1s linear infinite spin}.spinner-sm{border-width:2px;width:1.5rem;height:1.5rem}.spinner-md{width:2.5rem;height:2.5rem}.spinner-lg{width:3rem;height:3rem}@keyframes spin{to{transform:rotate(360deg)}}.tooltip-trigger{cursor:help;color:#6b7280;background-color:#e5e7eb;border-radius:9999px;justify-content:center;align-items:center;width:1rem;height:1rem;margin-left:.25rem;font-size:.65rem;display:inline-flex;position:relative}[data-theme=dark] .tooltip-trigger{color:#9ca3af;background-color:#4b5563}.tooltip-content{white-space:nowrap;opacity:0;visibility:hidden;z-index:100;color:#fff;background-color:#1f2937;border-radius:.375rem;max-width:300px;margin-bottom:.5rem;padding:.5rem .75rem;font-size:.75rem;font-weight:400;transition:opacity .2s,visibility .2s;position:absolute;bottom:100%;left:50%;transform:translate(-50%)}[data-theme=dark] .tooltip-content{color:#1f2937;background-color:#f3f4f6}.tooltip-content:after{content:"";border:6px solid #0000;border-top-color:#1f2937;position:absolute;top:100%;left:50%;transform:translate(-50%)}[data-theme=dark] .tooltip-content:after{border-top-color:#f3f4f6}.tooltip-multiline{white-space:normal;text-align:left;line-height:1.4}.tooltip-trigger:hover .tooltip-content,.tooltip-trigger:focus .tooltip-content{opacity:1;visibility:visible}.loading-overlay{z-index:9999;background:#00000080;justify-content:center;align-items:center;display:none;position:fixed;inset:0}.loading-overlay.active{display:flex}.file-upload__input{opacity:0;cursor:pointer;width:100%;height:100%;position:absolute;top:0;left:0}.file-upload__dropzone{position:relative}.file-upload__item{border-radius:.5rem;justify-content:space-between;align-items:center;margin-bottom:.5rem;padding:.75rem 1rem;display:flex}.file-upload__item-info{align-items:center;gap:.75rem;display:flex}.file-upload__item-icon{border-radius:.375rem;justify-content:center;align-items:center;width:2rem;height:2rem;display:flex}.file-upload__item-progress{border-radius:2px;height:4px;margin-top:.5rem;overflow:hidden}.file-upload__item-progress-bar{height:100%;transition:width .3s}details.error-details summary{list-style:none}details.error-details summary::-webkit-details-marker{display:none}details.error-details[open] .error-details__chevron{transform:rotate(180deg)}input:focus,select:focus,textarea:focus{outline:none;box-shadow:0 0 0 2px #fff,0 0 0 4px #3b82f6}[data-theme=dark] input:focus,[data-theme=dark] select:focus,[data-theme=dark] textarea:focus{box-shadow:0 0 0 2px #1f2937,0 0 0 4px #3b82f6}.modal-close:focus{outline:none}.modal-close:focus-visible{border-radius:.375rem;box-shadow:0 0 0 2px #fff,0 0 0 4px #3b82f6}[data-theme=dark] .modal-close:focus-visible{box-shadow:0 0 0 2px #1f2937,0 0 0 4px #3b82f6}.preview-tab:focus{outline:none}.preview-tab:focus-visible{border-radius:.25rem;box-shadow:0 0 0 2px #fff,0 0 0 4px #3b82f6}[data-theme=dark] .preview-tab:focus-visible{box-shadow:0 0 0 2px #1f2937,0 0 0 4px #3b82f6}a:focus{outline:none}a:focus-visible{border-radius:.25rem;box-shadow:0 0 0 2px #fff,0 0 0 4px #3b82f6}[data-theme=dark] a:focus-visible{box-shadow:0 0 0 2px #1f2937,0 0 0 4px #3b82f6}.dropzone:focus-within{border-color:#3b82f6;box-shadow:0 0 0 2px #fff,0 0 0 4px #3b82f6}[data-theme=dark] .dropzone:focus-within{box-shadow:0 0 0 2px #1f2937,0 0 0 4px #3b82f6}details.error-details summary:focus{outline:none}details.error-details summary:focus-visible{border-radius:.375rem;box-shadow:0 0 0 2px #fff,0 0 0 4px #3b82f6}[data-theme=dark] details.error-details summary:focus-visible{box-shadow:0 0 0 2px #1f2937,0 0 0 4px #3b82f6}.btn-blue:focus-visible{box-shadow:0 0 0 2px #fff,0 0 0 4px #fbbf24}[data-theme=dark] .btn-blue:focus-visible{box-shadow:0 0 0 2px #1f2937,0 0 0 4px #fbbf24}::-webkit-scrollbar{width:8px;height:8px}::-webkit-scrollbar-track{background:#f1f1f1;border-radius:4px}::-webkit-scrollbar-thumb{background:#c1c1c1;border-radius:4px}::-webkit-scrollbar-thumb:hover{background:#a1a1a1}[data-theme=dark] ::-webkit-scrollbar-track{background:#2d2d3f}[data-theme=dark] ::-webkit-scrollbar-thumb{background:#4a4a5a}[data-theme=dark] ::-webkit-scrollbar-thumb:hover{background:#5a5a6a}.text-muted{color:#6b7280}[data-theme=dark] .text-muted{color:#9ca3af}.flex-center{justify-content:center;align-items:center;display:flex}.container-70{width:100%;max-width:70vw;margin-left:auto;margin-right:auto}@media (max-width:768px){.container-70{max-width:95vw}}@property --tw-translate-x{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-y{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-z{syntax:"*";inherits:false;initial-value:0}@property --tw-space-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-gradient-position{syntax:"*";inherits:false}@property --tw-gradient-from{syntax:"";inherits:false;initial-value:#0000}@property --tw-gradient-via{syntax:"";inherits:false;initial-value:#0000}@property --tw-gradient-to{syntax:"";inherits:false;initial-value:#0000}@property --tw-gradient-stops{syntax:"*";inherits:false}@property --tw-gradient-via-stops{syntax:"*";inherits:false}@property --tw-gradient-from-position{syntax:"";inherits:false;initial-value:0%}@property --tw-gradient-via-position{syntax:"";inherits:false;initial-value:50%}@property --tw-gradient-to-position{syntax:"";inherits:false;initial-value:100%}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-tracking{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-duration{syntax:"*";inherits:false}@property --tw-scale-x{syntax:"*";inherits:false;initial-value:1}@property --tw-scale-y{syntax:"*";inherits:false;initial-value:1}@property --tw-scale-z{syntax:"*";inherits:false;initial-value:1}@keyframes pulse{50%{opacity:.5}} \ No newline at end of file diff --git a/assets/src/css/tailwind.css b/assets/src/css/tailwind.css index 44ac9de..1bd781a 100644 --- a/assets/src/css/tailwind.css +++ b/assets/src/css/tailwind.css @@ -1119,6 +1119,120 @@ details.error-details summary:focus-visible { background: #5a5a6a; } +/* ======================================== + WARNING DETAILS (SSE Import Warnings) + ======================================== */ + +.warning-box { + background-color: #fffbeb; + border: 1px solid #fcd34d; + border-radius: 0.75rem; + padding: 1.5rem; + margin-bottom: 1.5rem; +} +[data-theme="dark"] .warning-box { + background-color: rgba(245, 158, 11, 0.15); + border-color: #92400e; +} + +.warning-icon { + color: #f59e0b; +} +[data-theme="dark"] .warning-icon { + color: #fbbf24; +} + +.warning-details-list { + background-color: #fef3c7; + border-radius: 0.5rem; + padding: 0.75rem; + max-height: 16rem; + overflow-y: auto; +} +[data-theme="dark"] .warning-details-list { + background-color: rgba(245, 158, 11, 0.2); +} + +.warning-details-item { + padding: 0.5rem 0; + border-bottom: 1px solid #fcd34d; +} +.warning-details-item:last-child { + border-bottom: none; +} +[data-theme="dark"] .warning-details-item { + border-color: #92400e; +} + +.warning-line-badge { + font-family: ui-monospace, monospace; + font-size: 0.75rem; + background-color: #fde68a; + color: #92400e; + padding: 0.125rem 0.375rem; + border-radius: 0.25rem; + margin-right: 0.5rem; +} +[data-theme="dark"] .warning-line-badge { + background-color: #92400e; + color: #fef3c7; +} + +.warning-message { + font-size: 0.875rem; + color: #92400e; +} +[data-theme="dark"] .warning-message { + color: #fef3c7; +} + +.warning-title { + font-size: 1.125rem; + font-weight: 600; + color: #92400e; +} +[data-theme="dark"] .warning-title { + color: #fde68a; +} + +.warning-subtitle { + font-size: 0.875rem; + color: #b45309; + margin-top: 0.25rem; +} +[data-theme="dark"] .warning-subtitle { + color: #fcd34d; +} + +.warning-summary { + cursor: pointer; + display: flex; + align-items: center; + gap: 0.5rem; + font-size: 0.875rem; + font-weight: 500; + color: #b45309; +} +.warning-summary:hover { + color: #92400e; +} +[data-theme="dark"] .warning-summary { + color: #fcd34d; +} +[data-theme="dark"] .warning-summary:hover { + color: #fde68a; +} + +.warning-limit-notice { + margin-top: 0.5rem; + font-size: 0.75rem; + font-style: italic; + color: #d97706; +} +[data-theme="dark"] .warning-limit-notice { + color: #fbbf24; +} + /* ======================================== UTILITY CLASSES ======================================== */ diff --git a/src/Services/AjaxService.php b/src/Services/AjaxService.php index 92db7dd..08f03b2 100644 --- a/src/Services/AjaxService.php +++ b/src/Services/AjaxService.php @@ -196,39 +196,39 @@ function displaySuccessInPage(stats, warnings, warningsCount, warningsLimitReach var warningsHtml = ''; if (hasWarnings) { var warningItems = warnings.map(function(w) { - return '
  • ' + - 'Line ' + w.line + '' + - '' + escapeHtml(w.message.split('\\n')[0]) + '' + + return '
  • ' + + 'Line ' + w.line + '' + + '' + escapeHtml(w.message.split('\\n')[0]) + '' + '
  • '; }).join(''); var limitNotice = ''; if (warningsLimitReached) { - limitNotice = '
    ' + + limitNotice = '
    ' + '⚠️ Warning limit reached (' + maxWarnings + '). Additional errors may have occurred but were not recorded.' + '
    '; } - warningsHtml = '
    ' + + warningsHtml = '
    ' + '
    ' + '
    ' + - '' + + '' + '' + '' + '
    ' + '
    ' + - '

    ' + displayWarningsCount + ' Warning' + (displayWarningsCount !== 1 ? 's' : '') + '

    ' + - '
    Some SQL statements failed but import continued (continue_on_error enabled).
    ' + + '

    ' + displayWarningsCount + ' Warning' + (displayWarningsCount !== 1 ? 's' : '') + '

    ' + + '
    Some SQL statements failed but import continued (continue_on_error enabled).
    ' + '
    ' + '
    ' + '
    ' + - '' + + '' + '' + '' + '' + 'Show Warning Details' + '' + - '
      ' + warningItems + '
    ' + + '
      ' + warningItems + '
    ' + limitNotice + '
    ' + '
    '; From b9e203ad85cf8e15f8f58c1648e49bb44950e70f Mon Sep 17 00:00:00 2001 From: bd cannes Date: Sat, 28 Feb 2026 18:38:59 +0100 Subject: [PATCH 18/22] Add 'sse_files' to validActions whitelist in Request::determineAction() Sync the action whitelist with Application::setupRoutes() so that ?action=sse_files requests are no longer silently dropped to 'home'. Co-Authored-By: Claude Opus 4.6 --- src/Core/Request.php | 690 +++++++++++++++++++++---------------------- 1 file changed, 345 insertions(+), 345 deletions(-) diff --git a/src/Core/Request.php b/src/Core/Request.php index a501d02..fce3481 100644 --- a/src/Core/Request.php +++ b/src/Core/Request.php @@ -1,345 +1,345 @@ - - */ - private array $get; - - /** - * Sanitized POST data. - * @var array - */ - private array $post; - - /** - * FILES data. - * @var array - */ - private array $files; - - /** - * SERVER data. - * @var array - */ - private array $server; - - /** - * Requested action. - * @var string - */ - private string $action; - - /** - * Constructor - Initializes the request with superglobals. - */ - public function __construct() - { - $this->get = $this->sanitizeInput($_GET); - $this->post = $this->sanitizeInput($_POST); - $this->files = $_FILES; - $this->server = $_SERVER; - $this->action = $this->determineAction(); - } - - /** - * Sanitizes input data securely. - * - * Unlike the original which removed too many characters, - * this version preserves valid UTF-8 characters while - * removing dangerous control characters. - * - * @param array $data Data to sanitize. - * @return array Sanitized data. - */ - private function sanitizeInput(array $data): array - { - $sanitized = []; - - foreach ($data as $key => $value) { - // Sanitize key - $cleanKey = $this->sanitizeKey($key); - - if (is_array($value)) { - $sanitized[$cleanKey] = $this->sanitizeInput($value); - } else { - $sanitized[$cleanKey] = $this->sanitizeValue((string) $value); - } - } - - return $sanitized; - } - - /** - * Sanitizes a parameter key. - * - * @param string $key Key to sanitize. - * @return string Sanitized key. - */ - private function sanitizeKey(string $key): string - { - // Keys should only contain alphanumeric characters and underscore - return preg_replace('/[^a-zA-Z0-9_]/', '', $key) ?? ''; - } - - /** - * Sanitizes a parameter value. - * - * Preserves valid UTF-8 characters, only removes - * control characters (except tab, newline, carriage return). - * - * @param string $value Value to sanitize. - * @return string Sanitized value. - */ - private function sanitizeValue(string $value): string - { - // Remove control characters except \t, \n, \r - $value = preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/', '', $value) ?? ''; - - // Remove null byte sequences (injection) - $value = str_replace("\0", '', $value); - - return $value; - } - - /** - * Determines the action to execute based on request parameters. - * - * Priority: explicit action > form actions > parameter-based actions - * - * @return string Action name. - */ - private function determineAction(): string - { - // 1. Explicit action parameter takes highest priority (for SSE, AJAX, etc.) - if ($this->has('action')) { - $action = $this->input('action', ''); - // Validate against known actions to prevent injection - $validActions = ['home', 'upload', 'delete', 'import', 'start_import', 'stop_import', 'sse_import', 'drop_restart', 'restart_import', 'preview', 'history', 'files_list']; - if (in_array($action, $validActions, true)) { - return $action; - } - } - - // 2. Form-based actions (button submissions) - if ($this->has('uploadbutton')) { - return 'upload'; - } - - if ($this->has('delete')) { - return 'delete'; - } - - // 3. POST with fn parameter = start import - if ($this->has('fn') && ($_SERVER['REQUEST_METHOD'] ?? '') === 'POST') { - return 'start_import'; - } - - return 'home'; - } - - /** - * Gets a GET value. - * - * @param string $key Parameter key. - * @param mixed $default Default value. - * @return mixed Parameter value. - */ - public function get(string $key, mixed $default = null): mixed - { - return $this->get[$key] ?? $default; - } - - /** - * Gets a POST value. - * - * @param string $key Parameter key. - * @param mixed $default Default value. - * @return mixed Parameter value. - */ - public function post(string $key, mixed $default = null): mixed - { - return $this->post[$key] ?? $default; - } - - /** - * Gets a GET or POST value (GET takes priority). - * - * @param string $key Parameter key. - * @param mixed $default Default value. - * @return mixed Parameter value. - */ - public function input(string $key, mixed $default = null): mixed - { - return $this->get[$key] ?? $this->post[$key] ?? $default; - } - - /** - * Checks if a parameter exists (GET or POST). - * - * @param string $key Parameter key. - * @return bool True if parameter exists. - */ - public function has(string $key): bool - { - return isset($this->get[$key]) || isset($this->post[$key]); - } - - /** - * Gets an integer from parameters. - * - * Safely converts input to integer, rejecting scientific notation - * and values that would overflow PHP's integer range. - * - * @param string $key Parameter key. - * @param int $default Default value. - * @return int Integer value. - */ - public function getInt(string $key, int $default = 0): int - { - $value = $this->input($key); - - if ($value === null) { - return $default; - } - - // Only accept simple integer or decimal strings, reject scientific notation - // This prevents 1e10 being accepted as 10000000000 - if (!is_numeric($value) || preg_match('/[eE]/', (string) $value)) { - return $default; - } - - $floatValue = (float) $value; - - // Check for integer overflow - if ($floatValue > PHP_INT_MAX || $floatValue < PHP_INT_MIN) { - return $default; - } - - return (int) floor($floatValue); - } - - /** - * Gets information about an uploaded file. - * - * @param string $key File key. - * @return array|null File information or null. - */ - public function file(string $key): ?array - { - return $this->files[$key] ?? null; - } - - /** - * Checks if a file has been uploaded correctly. - * - * @param string $key File key. - * @return bool True if file is valid. - */ - public function hasValidFile(string $key): bool - { - $file = $this->file($key); - - if ($file === null) { - return false; - } - - return isset($file['tmp_name']) - && is_uploaded_file($file['tmp_name']) - && ($file['error'] ?? UPLOAD_ERR_NO_FILE) === UPLOAD_ERR_OK; - } - - /** - * Gets the requested action. - * - * @return string Action name. - */ - public function getAction(): string - { - return $this->action; - } - - /** - * Gets a SERVER value. - * - * @param string $key Parameter key. - * @param mixed $default Default value. - * @return mixed Parameter value. - */ - public function server(string $key, mixed $default = null): mixed - { - return $this->server[$key] ?? $default; - } - - /** - * Gets the PHP script name. - * - * @return string Script name. - */ - public function getScriptName(): string - { - return basename($this->server('SCRIPT_FILENAME', 'index.php')); - } - - /** - * Gets the script URI (base path without index.php). - * - * @return string Script URI (e.g., '/bigdump' instead of '/bigdump/index.php'). - */ - public function getScriptUri(): string - { - $phpSelf = $this->server('PHP_SELF', '/index.php'); - // Remove /index.php suffix to get clean base URL - $uri = preg_replace('#/index\.php$#', '', $phpSelf); - - // If empty (app at root), return empty string - callers will handle query params - // e.g., getScriptUri() . '?action=import' = '?action=import' (correct) - return $uri === '' ? '' : $uri; - } - - /** - * Checks if the request is an AJAX request. - * - * @return bool True if AJAX request. - */ - public function isAjax(): bool - { - return $this->has('ajaxrequest') - || strtolower($this->server('HTTP_X_REQUESTED_WITH', '')) === 'xmlhttprequest'; - } - - /** - * Gets all GET data. - * - * @return array GET data. - */ - public function getAllGet(): array - { - return $this->get; - } - - /** - * Gets all POST data. - * - * @return array POST data. - */ - public function getAllPost(): array - { - return $this->post; - } -} + + */ + private array $get; + + /** + * Sanitized POST data. + * @var array + */ + private array $post; + + /** + * FILES data. + * @var array + */ + private array $files; + + /** + * SERVER data. + * @var array + */ + private array $server; + + /** + * Requested action. + * @var string + */ + private string $action; + + /** + * Constructor - Initializes the request with superglobals. + */ + public function __construct() + { + $this->get = $this->sanitizeInput($_GET); + $this->post = $this->sanitizeInput($_POST); + $this->files = $_FILES; + $this->server = $_SERVER; + $this->action = $this->determineAction(); + } + + /** + * Sanitizes input data securely. + * + * Unlike the original which removed too many characters, + * this version preserves valid UTF-8 characters while + * removing dangerous control characters. + * + * @param array $data Data to sanitize. + * @return array Sanitized data. + */ + private function sanitizeInput(array $data): array + { + $sanitized = []; + + foreach ($data as $key => $value) { + // Sanitize key + $cleanKey = $this->sanitizeKey($key); + + if (is_array($value)) { + $sanitized[$cleanKey] = $this->sanitizeInput($value); + } else { + $sanitized[$cleanKey] = $this->sanitizeValue((string) $value); + } + } + + return $sanitized; + } + + /** + * Sanitizes a parameter key. + * + * @param string $key Key to sanitize. + * @return string Sanitized key. + */ + private function sanitizeKey(string $key): string + { + // Keys should only contain alphanumeric characters and underscore + return preg_replace('/[^a-zA-Z0-9_]/', '', $key) ?? ''; + } + + /** + * Sanitizes a parameter value. + * + * Preserves valid UTF-8 characters, only removes + * control characters (except tab, newline, carriage return). + * + * @param string $value Value to sanitize. + * @return string Sanitized value. + */ + private function sanitizeValue(string $value): string + { + // Remove control characters except \t, \n, \r + $value = preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/', '', $value) ?? ''; + + // Remove null byte sequences (injection) + $value = str_replace("\0", '', $value); + + return $value; + } + + /** + * Determines the action to execute based on request parameters. + * + * Priority: explicit action > form actions > parameter-based actions + * + * @return string Action name. + */ + private function determineAction(): string + { + // 1. Explicit action parameter takes highest priority (for SSE, AJAX, etc.) + if ($this->has('action')) { + $action = $this->input('action', ''); + // Validate against known actions to prevent injection + $validActions = ['home', 'upload', 'delete', 'import', 'start_import', 'stop_import', 'sse_import', 'drop_restart', 'restart_import', 'preview', 'history', 'files_list', 'sse_files']; + if (in_array($action, $validActions, true)) { + return $action; + } + } + + // 2. Form-based actions (button submissions) + if ($this->has('uploadbutton')) { + return 'upload'; + } + + if ($this->has('delete')) { + return 'delete'; + } + + // 3. POST with fn parameter = start import + if ($this->has('fn') && ($_SERVER['REQUEST_METHOD'] ?? '') === 'POST') { + return 'start_import'; + } + + return 'home'; + } + + /** + * Gets a GET value. + * + * @param string $key Parameter key. + * @param mixed $default Default value. + * @return mixed Parameter value. + */ + public function get(string $key, mixed $default = null): mixed + { + return $this->get[$key] ?? $default; + } + + /** + * Gets a POST value. + * + * @param string $key Parameter key. + * @param mixed $default Default value. + * @return mixed Parameter value. + */ + public function post(string $key, mixed $default = null): mixed + { + return $this->post[$key] ?? $default; + } + + /** + * Gets a GET or POST value (GET takes priority). + * + * @param string $key Parameter key. + * @param mixed $default Default value. + * @return mixed Parameter value. + */ + public function input(string $key, mixed $default = null): mixed + { + return $this->get[$key] ?? $this->post[$key] ?? $default; + } + + /** + * Checks if a parameter exists (GET or POST). + * + * @param string $key Parameter key. + * @return bool True if parameter exists. + */ + public function has(string $key): bool + { + return isset($this->get[$key]) || isset($this->post[$key]); + } + + /** + * Gets an integer from parameters. + * + * Safely converts input to integer, rejecting scientific notation + * and values that would overflow PHP's integer range. + * + * @param string $key Parameter key. + * @param int $default Default value. + * @return int Integer value. + */ + public function getInt(string $key, int $default = 0): int + { + $value = $this->input($key); + + if ($value === null) { + return $default; + } + + // Only accept simple integer or decimal strings, reject scientific notation + // This prevents 1e10 being accepted as 10000000000 + if (!is_numeric($value) || preg_match('/[eE]/', (string) $value)) { + return $default; + } + + $floatValue = (float) $value; + + // Check for integer overflow + if ($floatValue > PHP_INT_MAX || $floatValue < PHP_INT_MIN) { + return $default; + } + + return (int) floor($floatValue); + } + + /** + * Gets information about an uploaded file. + * + * @param string $key File key. + * @return array|null File information or null. + */ + public function file(string $key): ?array + { + return $this->files[$key] ?? null; + } + + /** + * Checks if a file has been uploaded correctly. + * + * @param string $key File key. + * @return bool True if file is valid. + */ + public function hasValidFile(string $key): bool + { + $file = $this->file($key); + + if ($file === null) { + return false; + } + + return isset($file['tmp_name']) + && is_uploaded_file($file['tmp_name']) + && ($file['error'] ?? UPLOAD_ERR_NO_FILE) === UPLOAD_ERR_OK; + } + + /** + * Gets the requested action. + * + * @return string Action name. + */ + public function getAction(): string + { + return $this->action; + } + + /** + * Gets a SERVER value. + * + * @param string $key Parameter key. + * @param mixed $default Default value. + * @return mixed Parameter value. + */ + public function server(string $key, mixed $default = null): mixed + { + return $this->server[$key] ?? $default; + } + + /** + * Gets the PHP script name. + * + * @return string Script name. + */ + public function getScriptName(): string + { + return basename($this->server('SCRIPT_FILENAME', 'index.php')); + } + + /** + * Gets the script URI (base path without index.php). + * + * @return string Script URI (e.g., '/bigdump' instead of '/bigdump/index.php'). + */ + public function getScriptUri(): string + { + $phpSelf = $this->server('PHP_SELF', '/index.php'); + // Remove /index.php suffix to get clean base URL + $uri = preg_replace('#/index\.php$#', '', $phpSelf); + + // If empty (app at root), return empty string - callers will handle query params + // e.g., getScriptUri() . '?action=import' = '?action=import' (correct) + return $uri === '' ? '' : $uri; + } + + /** + * Checks if the request is an AJAX request. + * + * @return bool True if AJAX request. + */ + public function isAjax(): bool + { + return $this->has('ajaxrequest') + || strtolower($this->server('HTTP_X_REQUESTED_WITH', '')) === 'xmlhttprequest'; + } + + /** + * Gets all GET data. + * + * @return array GET data. + */ + public function getAllGet(): array + { + return $this->get; + } + + /** + * Gets all POST data. + * + * @return array POST data. + */ + public function getAllPost(): array + { + return $this->post; + } +} From 02d646fe9c6cb24ac51e8813acea6c72de2e2f86 Mon Sep 17 00:00:00 2001 From: bd cannes Date: Sat, 28 Feb 2026 18:43:13 +0100 Subject: [PATCH 19/22] Fix delete button: replace broken POST form with GET link matching controller The form sent 'filename' but BigDumpController::delete() reads 'delete'. Replace with to match both the controller and the pattern already used in filepolling.js. Co-Authored-By: Claude Opus 4.6 --- templates/home.php | 885 ++++++++++++++++++++++----------------------- 1 file changed, 441 insertions(+), 444 deletions(-) diff --git a/templates/home.php b/templates/home.php index 3d5e6a6..825565b 100644 --- a/templates/home.php +++ b/templates/home.php @@ -1,444 +1,441 @@ - - - - - - -
    - Test Mode Enabled - Queries will be parsed but not executed. -
    - - - - -
    e($uploadResult['message']) ?>
    - -
    e($uploadResult['message']) ?>
    - - - - - -
    e($deleteResult['message']) ?>
    - -
    e($deleteResult['message']) ?>
    - - - - - -
    - Database not configured!
    - Please edit config/config.php and set your database credentials. -
    - -
    - Database connection failed!
    - e($connectionInfo['message']) ?> -
    - -
    - Connected to e($dbName) ?> at e($dbServer) ?>
    - Connection charset: e($connectionInfo['charset']) ?> - (Your dump file must use the same charset) -
    - - - -
    - Predefined file: e($predefinedFile) ?>
    -
    - - -
    -
    - - -
    -

    Available Dump Files

    - -
    - - -
    - No dump files found in e($uploadDir) ?>
    - Upload a .sql, .gz or .csv file using the form below, or via FTP. -
    - - -
    - - - - - - - - - - - - - - - - - - - - - -
    FilenameSizeDate AddedTypeActions
    e($file['name']) ?>formatBytes($file['size']) ?>e($file['date']) ?> - 'badge badge-blue', - 'GZip' => 'badge badge-purple', - 'BZ2' => 'badge badge-purple', - 'CSV' => 'badge badge-green', - default => 'badge badge-blue' - }; - ?> - e($file['type']) ?> - - - - - -
    - - -
    - - not supported - - -
    - - - -
    -
    -
    - -
    - -

    Upload Dump Files

    - - -
    - Upload disabled. Directory e($uploadDir) ?> is not writable.
    - Set permissions to 755 or 777, or upload files via FTP. -
    - - -

    - Maximum file size: formatBytes($uploadMaxSize) ?> - (php.ini: upload_max_filesize, post_max_size) - • Allowed types:
    - For larger files, use FTP to upload directly to e($uploadDir) ?> -

    - -
    - -
    -
    - - - -
    -
    - Click to upload or drag and drop -
    -
    - SQL, GZip or CSV files up to formatBytes($uploadMaxSize) ?> -
    - -
    - - -
    - - - - - - -
    - - - - - - -
    -
    -
    -
    Preparing import...
    -
    Loading file
    -
    -
    - - - - - - + + + + + + +
    + Test Mode Enabled - Queries will be parsed but not executed. +
    + + + + +
    e($uploadResult['message']) ?>
    + +
    e($uploadResult['message']) ?>
    + + + + + +
    e($deleteResult['message']) ?>
    + +
    e($deleteResult['message']) ?>
    + + + + + +
    + Database not configured!
    + Please edit config/config.php and set your database credentials. +
    + +
    + Database connection failed!
    + e($connectionInfo['message']) ?> +
    + +
    + Connected to e($dbName) ?> at e($dbServer) ?>
    + Connection charset: e($connectionInfo['charset']) ?> + (Your dump file must use the same charset) +
    + + + +
    + Predefined file: e($predefinedFile) ?>
    +
    + + +
    +
    + + +
    +

    Available Dump Files

    + +
    + + +
    + No dump files found in e($uploadDir) ?>
    + Upload a .sql, .gz or .csv file using the form below, or via FTP. +
    + + +
    + + + + + + + + + + + + + + + + + + + + + +
    FilenameSizeDate AddedTypeActions
    e($file['name']) ?>formatBytes($file['size']) ?>e($file['date']) ?> + 'badge badge-blue', + 'GZip' => 'badge badge-purple', + 'BZ2' => 'badge badge-purple', + 'CSV' => 'badge badge-green', + default => 'badge badge-blue' + }; + ?> + e($file['type']) ?> + + + + + +
    + + +
    + + not supported + + + + + +
    +
    + +
    + +

    Upload Dump Files

    + + +
    + Upload disabled. Directory e($uploadDir) ?> is not writable.
    + Set permissions to 755 or 777, or upload files via FTP. +
    + + +

    + Maximum file size: formatBytes($uploadMaxSize) ?> + (php.ini: upload_max_filesize, post_max_size) + • Allowed types:
    + For larger files, use FTP to upload directly to e($uploadDir) ?> +

    + +
    + +
    +
    + + + +
    +
    + Click to upload or drag and drop +
    +
    + SQL, GZip or CSV files up to formatBytes($uploadMaxSize) ?> +
    + +
    + + +
    + + + + + + +
    + + + + + + +
    +
    +
    +
    Preparing import...
    +
    Loading file
    +
    +
    + + + + + + From 95bfa92f10b753f7e953904df25b0930f66ecf90 Mon Sep 17 00:00:00 2001 From: bd cannes Date: Sat, 28 Feb 2026 18:48:18 +0100 Subject: [PATCH 20/22] Fix getScriptUri() to strip .phar entry points in addition to index.php The regex now matches both /index.php and /anything.phar so that PHAR-mode requests get a clean base URI for link generation. Adds RequestTest.php with 8 tests covering index.php, .phar, nested paths, and non-matching paths. Co-Authored-By: Claude Opus 4.6 --- src/Core/Request.php | 6 +-- tests/RequestTest.php | 100 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 103 insertions(+), 3 deletions(-) create mode 100644 tests/RequestTest.php diff --git a/src/Core/Request.php b/src/Core/Request.php index fce3481..2e29e44 100644 --- a/src/Core/Request.php +++ b/src/Core/Request.php @@ -297,15 +297,15 @@ public function getScriptName(): string } /** - * Gets the script URI (base path without index.php). + * Gets the script URI (base path without index.php or .phar entry point). * * @return string Script URI (e.g., '/bigdump' instead of '/bigdump/index.php'). */ public function getScriptUri(): string { $phpSelf = $this->server('PHP_SELF', '/index.php'); - // Remove /index.php suffix to get clean base URL - $uri = preg_replace('#/index\.php$#', '', $phpSelf); + // Remove /index.php or /anything.phar suffix to get clean base URL + $uri = preg_replace('#/(index\.php|[^/]+\.phar)$#', '', $phpSelf); // If empty (app at root), return empty string - callers will handle query params // e.g., getScriptUri() . '?action=import' = '?action=import' (correct) diff --git a/tests/RequestTest.php b/tests/RequestTest.php new file mode 100644 index 0000000..9dd2916 --- /dev/null +++ b/tests/RequestTest.php @@ -0,0 +1,100 @@ +test('getScriptUri() strips /index.php from subdirectory path', function () use ($runner) { + $request = makeRequestWithPhpSelf('/bigdump/index.php'); + $runner->assertEquals('/bigdump', $request->getScriptUri()); +}); + +// Test 2: index.php at root +$runner->test('getScriptUri() returns empty string for /index.php at root', function () use ($runner) { + $request = makeRequestWithPhpSelf('/index.php'); + $runner->assertEquals('', $request->getScriptUri()); +}); + +// Test 3: PHAR file at subdirectory +$runner->test('getScriptUri() strips /bigdump.phar from subdirectory path', function () use ($runner) { + $request = makeRequestWithPhpSelf('/tools/bigdump.phar'); + $runner->assertEquals('/tools', $request->getScriptUri()); +}); + +// Test 4: PHAR file at root +$runner->test('getScriptUri() returns empty string for /bigdump.phar at root', function () use ($runner) { + $request = makeRequestWithPhpSelf('/bigdump.phar'); + $runner->assertEquals('', $request->getScriptUri()); +}); + +// Test 5: PHAR with different name +$runner->test('getScriptUri() strips any .phar filename', function () use ($runner) { + $request = makeRequestWithPhpSelf('/subdir/my-app-v2.28.phar'); + $runner->assertEquals('/subdir', $request->getScriptUri()); +}); + +// Test 6: Deep nested path with index.php +$runner->test('getScriptUri() handles deep nested paths with index.php', function () use ($runner) { + $request = makeRequestWithPhpSelf('/a/b/c/index.php'); + $runner->assertEquals('/a/b/c', $request->getScriptUri()); +}); + +// Test 7: Deep nested path with .phar +$runner->test('getScriptUri() handles deep nested paths with .phar', function () use ($runner) { + $request = makeRequestWithPhpSelf('/a/b/c/dump.phar'); + $runner->assertEquals('/a/b/c', $request->getScriptUri()); +}); + +// Test 8: Path without index.php or .phar is left untouched +$runner->test('getScriptUri() leaves non-matching path untouched', function () use ($runner) { + $request = makeRequestWithPhpSelf('/bigdump/other.php'); + $runner->assertEquals('/bigdump/other.php', $request->getScriptUri()); +}); + +echo "\n"; +exit($runner->summary()); From d9ad15704183cc4e86db557b50c88469c7981883 Mon Sep 17 00:00:00 2001 From: bd cannes Date: Sat, 28 Feb 2026 18:49:21 +0100 Subject: [PATCH 21/22] Fix PHAR home link: use $scriptName instead of $scriptUri $scriptUri resolves to the parent directory (/tools) which breaks navigation in PHAR mode. $scriptName points to the PHAR file itself (bigdump.phar), giving a correct home link. Co-Authored-By: Claude Opus 4.6 --- templates/layout_phar.php | 244 +++++++++++++++++++------------------- 1 file changed, 122 insertions(+), 122 deletions(-) diff --git a/templates/layout_phar.php b/templates/layout_phar.php index 0f7ed0c..dbc96f4 100644 --- a/templates/layout_phar.php +++ b/templates/layout_phar.php @@ -1,122 +1,122 @@ - - - - - - - - - - BigDump v<?= $view->e($version) ?> - MySQL Dump Importer (PHAR Mode) - - - - - - - -
    - getInlinedIcons() ?> -
    - -
    - -
    - -
    - - -
    - content() ?> -
    - - -
    - BigDump ve($version) ?> - PHAR Edition
    - Original by Alexey Ozerov (2003) | Refactored with improved stability and security | Made by w3spi5 (wespify.com) -
    -
    - - - - - + + + + + + + + + + BigDump v<?= $view->e($version) ?> - MySQL Dump Importer (PHAR Mode) + + + + + + + +
    + getInlinedIcons() ?> +
    + +
    + +
    + +
    + + +
    + content() ?> +
    + + +
    + BigDump ve($version) ?> - PHAR Edition
    + Original by Alexey Ozerov (2003) | Refactored with improved stability and security | Made by w3spi5 (wespify.com) +
    +
    + + + + + From e944cda62b12934f97f4f9851696d451b2f0fc22 Mon Sep 17 00:00:00 2001 From: bd cannes Date: Sun, 1 Mar 2026 01:40:07 +0100 Subject: [PATCH 22/22] Add View::iconRef() helper for PHAR-compatible SVG icon references In PHAR mode icons are inlined in the DOM, so elements must reference fragments (#id) instead of external files (assets/icons2.svg#id). - Add iconRef(string $id) to View.php - Replace all 15 hardcoded icon paths in home.php with the helper Co-Authored-By: Claude Opus 4.6 --- src/Core/View.php | 14 ++++++++++++++ templates/home.php | 28 ++++++++++++++-------------- 2 files changed, 28 insertions(+), 14 deletions(-) diff --git a/src/Core/View.php b/src/Core/View.php index 9c23751..01e7c15 100644 --- a/src/Core/View.php +++ b/src/Core/View.php @@ -453,4 +453,18 @@ public function has(string $key): bool { return isset($this->data[$key]) && $this->data[$key] !== ''; } + + /** + * Returns the correct SVG icon href for a element. + * + * In PHAR mode, icons are inlined in the DOM so we reference by fragment only (#id). + * In normal mode, we reference the external sprite file (assets/icons2.svg#id). + * + * @param string $id Icon identifier (e.g., 'eye', 'trash', 'spinner'). + * @return string The href value for a element. + */ + public function iconRef(string $id): string + { + return $this->isPharMode ? '#' . $id : 'assets/icons2.svg#' . $id; + } } diff --git a/templates/home.php b/templates/home.php index 825565b..d8c2716 100644 --- a/templates/home.php +++ b/templates/home.php @@ -88,7 +88,7 @@

    Available Dump Files

    @@ -147,13 +147,13 @@ onclick="previewFile('escapeJs($file['name']) ?>')" class="btn btn-icon btn-purple" title="Preview SQL content"> - +
    + onsubmit="this.querySelector('button').disabled=true; this.querySelector('button').classList.add('opacity-50','cursor-not-allowed'); this.querySelector('.btn-icon-swap').innerHTML='iconRef('spinner') ?>">'; this.querySelector('.btn-icon-swap').classList.add('animate-spin');">
    @@ -161,9 +161,9 @@ class="btn btn-icon btn-purple" - + @@ -279,7 +279,7 @@ class="hidden"
    @@ -296,7 +296,7 @@ class="hidden" @@ -326,10 +326,10 @@ class="hidden"
    @@ -349,7 +349,7 @@ class="hidden"
    @@ -368,12 +368,12 @@ class="hidden" @@ -431,7 +431,7 @@ class="hidden"