commit 45623973a892f00a24cc3db0b833795508c5fe21 Author: dyzulk <66510723+dyzulk@users.noreply.github.com> Date: Fri Jan 16 11:21:32 2026 +0700 Initial Release v1.0.0: Full feature set with Docker automation, Nginx/Alpine stack diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..417cdf0 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,10 @@ +.git +.gitignore +.env +node_modules +deploy_package.tar.gz +temp_debug +*.md +docker-compose.yml +docs/ +app/Database/*.sqlite diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..64186b7 --- /dev/null +++ b/.env.example @@ -0,0 +1,7 @@ +APP_NAME=MIVO +APP_ENV=production +APP_KEY=mikhmonv3remake_secret_key_32bytes +APP_DEBUG=true + +# Database +DB_PATH=/app/Database/database.sqlite diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml new file mode 100644 index 0000000..f4375cd --- /dev/null +++ b/.github/workflows/docker-publish.yml @@ -0,0 +1,77 @@ +name: Docker Build & Publish + +on: + push: + branches: [ "main", "master" ] + # Publish semver tags as releases. + tags: [ 'v*.*.*' ] + pull_request: + branches: [ "main", "master" ] + +env: + # Use docker.io for Docker Hub if empty + REGISTRY: docker.io + # github.repository as / + IMAGE_NAME: dyzulk/mivo + +jobs: + build: + + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + # Login against a Docker registry except on PR + # https://github.com/docker/login-action + - name: Log into registry ${{ env.REGISTRY }} + if: github.event_name != 'pull_request' + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + + # Extract metadata (tags, labels) for Docker + # https://github.com/docker/metadata-action + - name: Extract Docker metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + # Branch main -> dyzulk/mivo:edge + type=raw,value=edge,enable={{is_default_branch}} + # Tag v1.0.0 -> dyzulk/mivo:1.0.0 + type=ref,event=tag + # Tag v1.0.0 -> dyzulk/mivo:latest + type=raw,value=latest,enable=${{ startsWith(github.ref, 'refs/tags/') }} + + # Build and push Docker image with Buildx (don't push on PR) + # https://github.com/docker/build-push-action + - name: Build and push Docker image + id: build-and-push + uses: docker/build-push-action@v5 + with: + context: . + push: ${{ github.event_name != 'pull_request' }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max + + # Update Docker Hub Description (README) + # https://github.com/peter-evans/dockerhub-description + - name: Update Docker Hub Description + if: github.event_name != 'pull_request' + uses: peter-evans/dockerhub-description@v4 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + repository: ${{ env.IMAGE_NAME }} + short-description: ${{ github.event.repository.description }} + readme-filepath: ./README.md diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ca999cc --- /dev/null +++ b/.gitignore @@ -0,0 +1,22 @@ +# Dependencies +/node_modules +/vendor + +# System Files +.DS_Store +Thumbs.db +.vscode/ +.idea/ + +# Application Data +*.log +/app/Database/*.sqlite +/temp_debug/ +*.bak + +# Build Artifacts & Deployments +/deploy_package.tar.gz +/mivo_backup_*.mivo + +# Secrets and Environment +.env \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..230b8ec --- /dev/null +++ b/Dockerfile @@ -0,0 +1,36 @@ +FROM php:8.2-fpm-alpine + +# Install system dependencies +RUN apk add --no-cache \ + nginx \ + supervisor \ + sqlite-dev \ + libzip-dev \ + zip \ + unzip + +# Install PHP extensions +RUN docker-php-ext-install pdo_sqlite zip + +# Configure Nginx +COPY docker/nginx.conf /etc/nginx/http.d/default.conf + +# Configure Supervisor +COPY docker/supervisord.conf /etc/supervisord.conf + +# Set working directory +WORKDIR /var/www/html + +# Copy application files +COPY . /var/www/html + +# Create Database directory explicitly & Set Permissions +RUN mkdir -p /var/www/html/app/Database && \ + chown -R www-data:www-data /var/www/html && \ + chmod -R 755 /var/www/html + +# Expose port +EXPOSE 80 + +# Start Supervisor (which starts Nginx & PHP-FPM) +CMD ["/usr/bin/supervisord", "-c", "/etc/supervisord.conf"] diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..1a14372 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 DyzulkDev + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..72cbbfc --- /dev/null +++ b/README.md @@ -0,0 +1,78 @@ +

+ MIVO Logo +

+ +# MIVO (Mikrotik Voucher) + +> **Modern. Lightweight. Efficient.** + +MIVO is a complete rewrite of the legendary **Mikhmon v3**, re-engineered with a modern MVC architecture to run efficiently on low-end devices like STB (Set Top Boxes) and Android, while providing a premium user experience on desktop. + +![Status](https://img.shields.io/badge/Status-Beta-orange) ![PHP](https://img.shields.io/badge/PHP-8.0+-777BB4) ![License](https://img.shields.io/badge/License-MIT-green) + +## 🚀 Key Features + +* **⚡ Lightweight Core**: Built on a custom minimal MVC framework (~50KB core) optimized for speed. +* **🎨 Modern UI/UX**: Fresh Glassmorphism design system using TailwindCSS and Alpine.js. +* **📱 Responsive**: Fully optimized mobile experience with touch-friendly navigation. +* **🔒 Secure**: Environment-based configuration (`.env`), encrypted credentials, and secure session management. +* **🔌 API Ready**: Built-in REST API support with CORS management for external integrations. +* **🛠️ CLI Tool**: Includes `mivo` CLI helper for easy management and installation. + +## 🛠️ Installation + +### Requirements +* PHP 8.0 or higher +* SQLite3 Extension +* OpenSSL Extension + +### Quick Start + +1. **Clone the Repository** + ```bash + git clone https://github.com/dyzulk/mivo.git + cd mivo + ``` + +2. **Setup Environment** + ```bash + cp .env.example .env + ``` + +3. **Install & Generate Key** + ```bash + php mivo install + ``` + *This will create the database, run migrations, generate your secure `APP_KEY`, and set up the admin account.* + +4. **Run Development Server** + ```bash + php mivo serve + ``` + Access the app at `http://localhost:8000`. + +## 📂 Structure + +* `app/` - Core application logic (Controllers, Models, Views). +* `public/` - Web root and assets. +* `routes/` - Route definitions (`web.php`, `api.php`). +* `mivo` - CLI executable entry point. + + +## 🤝 Contributing + +Contributions are welcome! Please feel free to submit a Pull Request. + +## ☕ Support the Project + +If you find MIVO useful, please consider supporting its development. Your contribution helps keep the project alive! + +[![SociaBuzz Tribe](https://img.shields.io/badge/SociaBuzz-Tribe-green?style=for-the-badge&logo=sociabuzz&logoColor=white)](https://sociabuzz.com/dyzulkdev/tribe) + + +## 📄 License + +This project is open-sourced software licensed under the [MIT license](https://opensource.org/licenses/MIT). + +--- +*Created with ❤️ by DyzulkDev* diff --git a/app/Config/SiteConfig.php b/app/Config/SiteConfig.php new file mode 100644 index 0000000..c5d50f1 --- /dev/null +++ b/app/Config/SiteConfig.php @@ -0,0 +1,36 @@ + Developed by ' . self::CREDIT_NAME . ''; + } +} diff --git a/app/Controllers/ApiController.php b/app/Controllers/ApiController.php new file mode 100644 index 0000000..b9592b5 --- /dev/null +++ b/app/Controllers/ApiController.php @@ -0,0 +1,74 @@ + 'Method Not Allowed']); + return; + } + + // Get JSON Input + $input = json_decode(file_get_contents('php://input'), true); + + $ip = $input['ip'] ?? ''; + $user = $input['user'] ?? ''; + $pass = $input['password'] ?? ''; + $id = $input['id'] ?? null; + $port = $input['port'] ?? 8728; // Default port + + // Fallback to stored password if empty and ID provided (Edit Mode) + if (empty($pass) && !empty($id)) { + $configModel = new Config(); + $session = $configModel->getSessionById($id); + if ($session && !empty($session['password'])) { + $pass = EncryptionHelper::decrypt($session['password']); + } + } + + if (empty($ip) || empty($user)) { + http_response_code(400); + echo json_encode(['error' => 'IP Address and Username are required']); + return; + } + + $api = new RouterOSAPI(); + // $api->debug = true; // Enable for debugging + $api->port = (int)$port; + + if ($api->connect($ip, $user, $pass)) { + $api->write('/interface/print'); + $read = $api->read(false); + $interfaces = $api->parseResponse($read); + $api->disconnect(); + + $list = []; + foreach ($interfaces as $iface) { + if (isset($iface['name'])) { + $list[] = $iface['name']; + } + } + + // Return success + echo json_encode([ + 'success' => true, + 'interfaces' => $list + ]); + } else { + http_response_code(500); + echo json_encode([ + 'error' => 'Connection failed. Check IP, User, Password, or connectivity.' + ]); + } + } +} diff --git a/app/Controllers/AuthController.php b/app/Controllers/AuthController.php new file mode 100644 index 0000000..1bf07bb --- /dev/null +++ b/app/Controllers/AuthController.php @@ -0,0 +1,43 @@ +view('login'); + } + + public function login() { + $username = $_POST['username'] ?? ''; + $password = $_POST['password'] ?? ''; + + $userModel = new User(); + $user = $userModel->attempt($username, $password); + + if ($user) { + $_SESSION['user_id'] = $user['id']; + $_SESSION['username'] = $user['username']; + \App\Helpers\FlashHelper::set('success', 'Welcome Back', 'Login successful.'); + header('Location: /'); + exit; + } else { + \App\Helpers\FlashHelper::set('error', 'Login Failed', 'Invalid credentials'); + header('Location: /login'); + exit; + } + } + + public function logout() { + session_destroy(); + header('Location: /login'); + exit; + } +} diff --git a/app/Controllers/DashboardController.php b/app/Controllers/DashboardController.php new file mode 100644 index 0000000..e58bfaa --- /dev/null +++ b/app/Controllers/DashboardController.php @@ -0,0 +1,114 @@ +getSession($session); + + if (!$creds) { + echo "Session not found."; + return; + } + + // Mock Data for Demo (SQLite or Legacy) + if ($session === 'demo') { + $data = [ + 'session' => $session, + 'clock' => ['time' => '12:00:00', 'date' => 'jan/01/2024'], + 'resource' => [ + 'board-name' => 'CHR (Demo SQLite)', + 'version' => '7.12', + 'uptime' => '1w 2d 3h', + 'cpu-load' => '15', + 'free-memory' => 1048576 * 512, // 512 MB + 'free-hdd-space' => 1048576 * 1024, // 1 GB + ], + // ... rest of mock data + 'routerboard' => ['model' => 'x86_64'], + 'hotspot_active' => 25, + 'hotspot_users' => 150, + 'lang' => [ + 'system_date_time' => 'System Date & Time', + 'uptime' => 'Uptime', + 'board_name' => 'Board Name', + 'model' => 'Model', + 'cpu_load' => 'CPU Load', + 'free_memory' => 'Free Memory', + 'free_hdd' => 'Free HDD', + 'hotspot_active' => 'Hotspot Active', + 'hotspot_users' => 'Hotspot Users', + ] + ]; + return $this->view('dashboard', $data); + } + + $API = new RouterOSAPI(); + + // Determine password: if legacy, decrypt it. If SQLite (new), assume plain for now + // (since we just seeded 'admin' plain in setup_database.php) or decrypt if you decide to encrypt in DB. + // For this Demo, setup_database.php inserted plain 'admin'. + // Existing v3 passwords are encrypted. + + $password = $creds['password']; + if (isset($creds['source']) && $creds['source'] === 'legacy') { + $password = RouterOSAPI::decrypt($password); + } + + if ($API->connect($creds['ip'], $creds['user'], $password)) { + // ... API calls + $getclock = $API->comm("/system/clock/print"); + $clock = $getclock[0] ?? []; + + $getresource = $API->comm("/system/resource/print"); + $resource = $getresource[0] ?? []; + + $getrouterboard = $API->comm("/system/routerboard/print"); + $routerboard = $getrouterboard[0] ?? []; + + $counthotspotactive = $API->comm("/ip/hotspot/active/print", array("count-only" => "")); + $countallusers = $API->comm("/ip/hotspot/user/print", array("count-only" => "")); + + $API->disconnect(); + + $data = [ + 'session' => $session, + 'clock' => $clock, + 'resource' => $resource, + 'routerboard' => $routerboard, + 'hotspot_active' => $counthotspotactive, + 'hotspot_users' => $countallusers, + 'lang' => [ + 'system_date_time' => 'System Date & Time', + 'uptime' => 'Uptime', + 'board_name' => 'Board Name', + 'model' => 'Model', + 'cpu_load' => 'CPU Load', + 'free_memory' => 'Free Memory', + 'free_hdd' => 'Free HDD', + 'hotspot_active' => 'Hotspot Active', + 'hotspot_users' => 'Hotspot Users', + 'hotspot_users' => 'Hotspot Users', + ], + 'interface' => $creds['interface'] ?? 'ether1' + ]; + // Pass Users Link (Optional: could be part of layout or card link) + // Ideally, the "Hotspot Users" card on dashboard should be clickable. + return $this->view('dashboard', $data); + + } else { + echo "Connection Failed to " . $creds['ip']; + } + } +} diff --git a/app/Controllers/DhcpController.php b/app/Controllers/DhcpController.php new file mode 100644 index 0000000..ae22a5f --- /dev/null +++ b/app/Controllers/DhcpController.php @@ -0,0 +1,38 @@ +getSession($session); + if (!$config) { + header('Location: /'); + exit; + } + + $leases = []; + $API = new RouterOSAPI(); + $API->attempts = 1; + $API->timeout = 3; + + if ($API->connect($config['ip_address'], $config['username'], $config['password'])) { + // Fetch DHCP Leases + $leases = $API->comm("/ip/dhcp-server/lease/print"); + $API->disconnect(); + } + + // Add index for viewing + return $this->view('network/dhcp', [ + 'session' => $session, + 'leases' => $leases ?? [] + ]); + } +} diff --git a/app/Controllers/GeneratorController.php b/app/Controllers/GeneratorController.php new file mode 100644 index 0000000..1d31b90 --- /dev/null +++ b/app/Controllers/GeneratorController.php @@ -0,0 +1,166 @@ +getSession($session); + + if (!$creds) { + $this->redirect('/'); + return; + } + + $API = new RouterOSAPI(); + if ($API->connect($creds['ip'], $creds['user'], $creds['password'])) { + // Fetch Profiles for Dropdown + $profiles = $API->comm('/ip/hotspot/user/profile/print'); + // Fetch Hotspot Servers + $servers = $API->comm('/ip/hotspot/print'); + $API->disconnect(); + + $data = [ + 'session' => $session, + 'title' => 'Generate Vouchers - ' . $session, + 'profiles' => $profiles, + 'servers' => $servers + ]; + + $this->view('hotspot/generate', $data); + } else { + // Handle connection error (flash message ideally, but for now redirect or show error) + echo "Connection failed to " . $creds['ip']; + } + } + + public function process() { + $session = $_POST['session'] ?? ''; + $qty = intval($_POST['qty'] ?? 1); + $server = $_POST['server'] ?? 'all'; + $userMode = $_POST['userModel'] ?? 'up'; + $userLength = intval($_POST['userLength'] ?? 4); + $prefix = $_POST['prefix'] ?? ''; + $char = $_POST['char'] ?? 'mix'; + $profile = $_POST['profile'] ?? ''; + $comment = $_POST['comment'] ?? ''; + + // Time Limit Logic (d, h, m) + $timelimit_d = $_POST['timelimit_d'] ?? ''; + $timelimit_h = $_POST['timelimit_h'] ?? ''; + $timelimit_m = $_POST['timelimit_m'] ?? ''; + + $timeLimit = ''; + if ($timelimit_d != '') $timeLimit .= $timelimit_d . 'd'; + if ($timelimit_h != '') $timeLimit .= $timelimit_h . 'h'; + if ($timelimit_m != '') $timeLimit .= $timelimit_m . 'm'; + + // Data Limit Logic (Value, Unit) + $datalimit_val = $_POST['datalimit_val'] ?? ''; + $datalimit_unit = $_POST['datalimit_unit'] ?? 'MB'; + + $dataLimit = ''; + if (!empty($datalimit_val) && is_numeric($datalimit_val)) { + $bytes = (float)$datalimit_val; + if ($datalimit_unit === 'GB') { + $bytes = $bytes * 1073741824; + } else { + // MB + $bytes = $bytes * 1048576; + } + $dataLimit = (string)round($bytes); + } + + if (!$session || $qty < 1 || !$profile) { + $this->back($session); + return; + } + + $configModel = new Config(); + $creds = $configModel->getSession($session); + if (!$creds) { + $this->redirect('/'); + return; + } + + $API = new RouterOSAPI(); + if ($API->connect($creds['ip'], $creds['user'], $creds['password'])) { + + // Format Comment: prefix-rand-date- comment + // Example: up-123-12.01.26- premium + $commentPrefix = ($userMode === 'vc') ? 'vc-' : 'up-'; + $batchId = rand(100, 999); + $date = date('m.d.y'); + $commentBody = $comment ?: $profile; + $finalComment = "{$commentPrefix}{$batchId}-{$date}- {$commentBody}"; + + for ($i = 0; $i < $qty; $i++) { + $username = $prefix . $this->generateRandomString($userLength, $char); + $password = $username; + + if ($userMode === 'up') { + $password = $this->generateRandomString($userLength, $char); + } + + $user = [ + 'server' => $server, + 'profile' => $profile, + 'name' => $username, + 'password' => $password, + 'comment' => $finalComment + ]; + + if (!empty($timeLimit)) $user['limit-uptime'] = $timeLimit; + if (!empty($dataLimit)) $user['limit-bytes-total'] = $dataLimit; + + $API->comm("/ip/hotspot/user/add", $user); + } + + $API->disconnect(); + } + + \App\Helpers\FlashHelper::set('success', 'toasts.vouchers_generated', 'toasts.vouchers_generated_desc', ['qty' => $qty], true); + $this->redirect('/' . $session . '/hotspot/users'); + } + + private function generateRandomString($length, $charType) { + $characters = ''; + switch ($charType) { + case 'lower': + $characters = 'abcdefghijklmnopqrstuvwxyz'; + break; + case 'upper': + $characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; + break; + case 'number': + $characters = '0123456789'; + break; + case 'uppernumber': + $characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; + break; + case 'lowernumber': + $characters = 'abcdefghijklmnopqrstuvwxyz0123456789'; + break; + case 'mix': + default: + $characters = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; + break; + } + + $randomString = ''; + for ($i = 0; $i < $length; $i++) { + $randomString .= $characters[rand(0, strlen($characters) - 1)]; + } + + return $randomString; + } + + private function back($session) { + $this->redirect('/' . $session . '/hotspot/generate'); + } +} diff --git a/app/Controllers/HomeController.php b/app/Controllers/HomeController.php new file mode 100644 index 0000000..621bde5 --- /dev/null +++ b/app/Controllers/HomeController.php @@ -0,0 +1,34 @@ +getAllSessions(); + + $data = [ + 'routers' => $routers + ]; + + $this->view('home', $data); + } + + public function designSystem() { + $data = ['title' => 'MIVO - Design System']; + $this->view('design_system', $data); + } + + public function testAlert() { + \App\Helpers\FlashHelper::set('success', 'toasts.test_alert', 'toasts.test_alert_desc', [], true); + header("Location: /"); + exit; + } +} diff --git a/app/Controllers/HotspotController.php b/app/Controllers/HotspotController.php new file mode 100644 index 0000000..3fc9054 --- /dev/null +++ b/app/Controllers/HotspotController.php @@ -0,0 +1,864 @@ +getSession($session); + + if (!$creds) { + echo "Session not found."; + return; + } + + $userId = $session; // For view context + $users = []; + $error = null; + + $API = new RouterOSAPI(); + //$API->debug = true; // Enable for debugging + + // Decrypt password if from SQLite + $password = $creds['password']; + if (isset($creds['source']) && $creds['source'] === 'legacy') { + $password = RouterOSAPI::decrypt($password); + } + + if ($API->connect($creds['ip'], $creds['user'], $password)) { + // Get all hotspot users + $users = $API->comm("/ip/hotspot/user/print"); + + // Get active users to mark status (optional, can be done later for optimization) + // $active = $API->comm("/ip/hotspot/active/print"); + + $API->disconnect(); + } else { + $error = "Connection Failed to " . $creds['ip']; + } + + $data = [ + 'session' => $session, + 'users' => $users, + 'error' => $error + ]; + + return $this->view('hotspot/users/users', $data); + } + + public function add($session) { + $configModel = new Config(); + $creds = $configModel->getSession($session); + if (!$creds) return; // Should handle error better + + $API = new RouterOSAPI(); + + $password = $creds['password']; + if (isset($creds['source']) && $creds['source'] === 'legacy') { + $password = RouterOSAPI::decrypt($password); + } + + $profiles = []; + if ($API->connect($creds['ip'], $creds['user'], $password)) { + $profiles = $API->comm("/ip/hotspot/user/profile/print"); + $API->disconnect(); + } + + $data = [ + 'session' => $session, + 'profiles' => $profiles + ]; + + return $this->view('hotspot/users/add', $data); + } + + public function store() { + if ($_SERVER['REQUEST_METHOD'] !== 'POST') { + header('Location: /'); + exit; + } + + $session = $_POST['session'] ?? ''; + $name = $_POST['name'] ?? ''; + $password_user = $_POST['password'] ?? ''; + $profile = $_POST['profile'] ?? 'default'; + $comment = $_POST['comment'] ?? ''; + + // Time Limit Logic (d, h, m) + $timelimit_d = $_POST['timelimit_d'] ?? ''; + $timelimit_h = $_POST['timelimit_h'] ?? ''; + $timelimit_m = $_POST['timelimit_m'] ?? ''; + + $timelimit = ''; + if ($timelimit_d != '') $timelimit .= $timelimit_d . 'd'; + if ($timelimit_h != '') $timelimit .= $timelimit_h . 'h'; + if ($timelimit_m != '') $timelimit .= $timelimit_m . 'm'; + + // Data Limit Logic (Value, Unit) + $datalimit_val = $_POST['datalimit_val'] ?? ''; + $datalimit_unit = $_POST['datalimit_unit'] ?? 'MB'; + + $datalimit = ''; + if (!empty($datalimit_val) && is_numeric($datalimit_val)) { + $bytes = (int)$datalimit_val; + if ($datalimit_unit === 'GB') { + $bytes = $bytes * 1073741824; + } else { + // MB + $bytes = $bytes * 1048576; + } + $datalimit = (string)$bytes; + } + + $configModel = new Config(); + $creds = $configModel->getSession($session); + if (!$creds) return; + + $API = new RouterOSAPI(); + $password_router = $creds['password']; + if (isset($creds['source']) && $creds['source'] === 'legacy') { + $password_router = RouterOSAPI::decrypt($password_router); + } + + if ($API->connect($creds['ip'], $creds['user'], $password_router)) { + + $userData = [ + 'name' => $name, + 'password' => $password_user, + 'profile' => $profile, + 'comment' => $comment + ]; + + if(!empty($timelimit)) $userData['limit-uptime'] = $timelimit; + if(!empty($datalimit)) $userData['limit-bytes-total'] = $datalimit; + + $API->comm("/ip/hotspot/user/add", $userData); + $API->disconnect(); + } + + \App\Helpers\FlashHelper::set('success', 'toasts.user_added', 'toasts.user_added_desc', ['name' => $name], true); + header("Location: /" . $session . "/hotspot/users"); + exit; + } + + public function delete() { + if ($_SERVER['REQUEST_METHOD'] !== 'POST') { + header('Location: /'); + exit; + } + + $session = $_POST['session'] ?? ''; + $rawId = $_POST['id'] ?? ''; + + $configModel = new Config(); + $creds = $configModel->getSession($session); + + $API = new RouterOSAPI(); + $password_router = $creds['password']; + if (isset($creds['source']) && $creds['source'] === 'legacy') { + $password_router = RouterOSAPI::decrypt($password_router); + } + + if ($API->connect($creds['ip'], $creds['user'], $password_router)) { + // Handle Multiple IDs (comma separated) + $ids = explode(',', $rawId); + foreach ($ids as $id) { + $id = trim($id); + if (!empty($id)) { + // 1. Get Username first (to delete scheduler) + $user = $API->comm("/ip/hotspot/user/print", [ + "?.id" => $id + ]); + + if (!empty($user) && isset($user[0]['name'])) { + $username = $user[0]['name']; + + // 2. Remove User + $API->comm("/ip/hotspot/user/remove", [".id" => $id]); + + // 3. Remove Scheduler (Ghost Cleanup) + // Check if scheduler exists with same name as user + $schedules = $API->comm("/system/scheduler/print", [ + "?name" => $username + ]); + + if(!empty($schedules)) { + // Loop just in case multiple matches (unlikely if unique name) + foreach($schedules as $sch) { + $API->comm("/system/scheduler/remove", [ + ".id" => $sch['.id'] + ]); + } + } + } + } + } + $API->disconnect(); + } + + \App\Helpers\FlashHelper::set('success', 'toasts.user_deleted', 'toasts.user_deleted_desc', [], true); + header("Location: /" . $session . "/hotspot/users"); + exit; + } + + public function edit($session, $id) { + $configModel = new Config(); + $creds = $configModel->getSession($session); + if (!$creds) return; + + $API = new RouterOSAPI(); + $password_router = $creds['password']; + if (isset($creds['source']) && $creds['source'] === 'legacy') { + $password_router = RouterOSAPI::decrypt($password_router); + } + + $user = null; + $profiles = []; + + if ($API->connect($creds['ip'], $creds['user'], $password_router)) { + // Fetch specific user + $userRequest = $API->comm("/ip/hotspot/user/print", [ + "?.id" => $id + ]); + if (!empty($userRequest)) { + $user = $userRequest[0]; + + // Parse Time Limit (limit-uptime) e.g. 1d04:00:00 or 30d + // Mikrotik uptime format varies. Safe parse: + // Regex for Xd, Xh, Xm? NO, Mikrotik returns "4w2d" or "10:00:00" (h:m:s) + // Or simple seconds if raw? Print usually returns formatted. + // Let's defer to a helper or simple parsing. + // Actually standard format: 1d 04:00:00 or 1h30m. + // Let's try simple regex extraction. + $t_d = ''; $t_h = ''; $t_m = ''; + $uptime = $user['limit-uptime'] ?? ''; + if ($uptime) { + if (preg_match('/(\d+)d/', $uptime, $m)) $t_d = $m[1]; + if (preg_match('/(\d+)h/', $uptime, $m)) $t_h = $m[1]; + if (preg_match('/(\d+)m/', $uptime, $m)) $t_m = $m[1]; + // Handle H:M:S format (e.g. 04:00:00) if no 'h'/'m' chars? + // Mikrotik CLI `print` implies "1d04:00:00". API might return "1d04:00:00". + // If so, 04 is hours. + // Simple parse if regex failed? + // Let's assume standard XdXhXm usage for now based on Add form. + } + $user['time_d'] = $t_d; + $user['time_h'] = $t_h; + $user['time_m'] = $t_m; + + // Parse Data Limit (limit-bytes-total) + $bytes = $user['limit-bytes-total'] ?? 0; + $d_val = ''; $d_unit = 'MB'; + if ($bytes > 0) { + if ($bytes >= 1073741824) { + $d_val = round($bytes / 1073741824, 2); + $d_unit = 'GB'; + } else { + $d_val = round($bytes / 1048576, 2); + $d_unit = 'MB'; + } + } + $user['data_val'] = $d_val; + $user['data_unit'] = $d_unit; + } + + // Fetch Profiles + $profiles = $API->comm("/ip/hotspot/user/profile/print"); + + $API->disconnect(); + } + + if (!$user) { + header("Location: /" . $session . "/hotspot/users"); + exit; + } + + $data = [ + 'session' => $session, + 'user' => $user, + 'profiles' => $profiles + ]; + + return $this->view('hotspot/users/edit', $data); + } + + public function update() { + if ($_SERVER['REQUEST_METHOD'] !== 'POST') { + header('Location: /'); + exit; + } + + $session = $_POST['session'] ?? ''; + $id = $_POST['id'] ?? ''; + $name = $_POST['name'] ?? ''; + $password_user = $_POST['password'] ?? ''; + $profile = $_POST['profile'] ?? ''; + $comment = $_POST['comment'] ?? ''; + $server = $_POST['server'] ?? 'all'; + + // Time Limit Logic (d, h, m) + $timelimit_d = $_POST['timelimit_d'] ?? ''; + $timelimit_h = $_POST['timelimit_h'] ?? ''; + $timelimit_m = $_POST['timelimit_m'] ?? ''; + + $timelimit = ''; + if ($timelimit_d != '') $timelimit .= $timelimit_d . 'd'; + if ($timelimit_h != '') $timelimit .= $timelimit_h . 'h'; + if ($timelimit_m != '') $timelimit .= $timelimit_m . 'm'; + + // Data Limit Logic (Value, Unit) + $datalimit_val = $_POST['datalimit_val'] ?? ''; + $datalimit_unit = $_POST['datalimit_unit'] ?? 'MB'; + + $datalimit = '0'; + if (!empty($datalimit_val) && is_numeric($datalimit_val)) { + $bytes = (float)$datalimit_val; // float to handle decimals before calc + if ($datalimit_unit === 'GB') { + $bytes = $bytes * 1073741824; + } else { + // MB + $bytes = $bytes * 1048576; + } + $datalimit = (string)round($bytes); + } + + $configModel = new Config(); + $creds = $configModel->getSession($session); + if (!$creds) return; + + $API = new RouterOSAPI(); + $password_router = $creds['password']; + if (isset($creds['source']) && $creds['source'] === 'legacy') { + $password_router = RouterOSAPI::decrypt($password_router); + } + + if ($API->connect($creds['ip'], $creds['user'], $password_router)) { + + $userData = [ + '.id' => $id, + 'name' => $name, + 'password' => $password_user, + 'profile' => $profile, + 'comment' => $comment, + 'server' => $server + ]; + + if(!empty($timelimit)) $userData['limit-uptime'] = $timelimit; + else $userData['limit-uptime'] = '0s'; // Reset if empty + + // Always set if calculated, 0 resets it. + $userData['limit-bytes-total'] = $datalimit; + + $API->comm("/ip/hotspot/user/set", $userData); + $API->disconnect(); + } + + \App\Helpers\FlashHelper::set('success', 'toasts.user_updated', 'toasts.user_updated_desc', ['name' => $name], true); + header("Location: /" . $session . "/hotspot/users"); + exit; + } + public function active($session) { + $configModel = new Config(); + $creds = $configModel->getSession($session); + if (!$creds) { + header("Location: /"); + exit; + } + + $items = []; + $error = null; + + $API = new RouterOSAPI(); + $password_router = $creds['password']; + if (isset($creds['source']) && $creds['source'] === 'legacy') { + $password_router = RouterOSAPI::decrypt($password_router); + } + + if ($API->connect($creds['ip'], $creds['user'], $password_router)) { + $items = $API->comm("/ip/hotspot/active/print"); + $API->disconnect(); + } else { + $error = "Connection Failed to " . $creds['ip']; + } + + $data = [ + 'session' => $session, + 'items' => $items, + 'error' => $error + ]; + + return $this->view('status/active', $data); + } + + public function removeActive() { + if ($_SERVER['REQUEST_METHOD'] !== 'POST') { + header('Location: /'); + exit; + } + + $session = $_POST['session'] ?? ''; + $id = $_POST['id'] ?? ''; + + $configModel = new Config(); + $creds = $configModel->getSession($session); + if (!$creds) return; + + $API = new RouterOSAPI(); + $password_router = $creds['password']; + if (isset($creds['source']) && $creds['source'] === 'legacy') { + $password_router = RouterOSAPI::decrypt($password_router); + } + + if ($API->connect($creds['ip'], $creds['user'], $password_router)) { + $API->comm("/ip/hotspot/active/remove", [".id" => $id]); + $API->disconnect(); + } + + \App\Helpers\FlashHelper::set('success', 'toasts.session_removed', 'toasts.session_removed_desc', [], true); + header("Location: /" . $session . "/hotspot/active"); + exit; + } + + public function hosts($session) { + $configModel = new Config(); + $creds = $configModel->getSession($session); + if (!$creds) { + header("Location: /"); + exit; + } + + $items = []; + $error = null; + + $API = new RouterOSAPI(); + $password_router = $creds['password']; + if (isset($creds['source']) && $creds['source'] === 'legacy') { + $password_router = RouterOSAPI::decrypt($password_router); + } + + if ($API->connect($creds['ip'], $creds['user'], $password_router)) { + $items = $API->comm("/ip/hotspot/host/print"); + $API->disconnect(); + } else { + $error = "Connection Failed to " . $creds['ip']; + } + + $data = [ + 'session' => $session, + 'items' => $items, + 'error' => $error + ]; + + return $this->view('status/hosts', $data); + } + + public function bindings($session) { + $configModel = new Config(); + $creds = $configModel->getSession($session); + if (!$creds) { + header("Location: /"); + exit; + } + + $items = []; + $error = null; + + $API = new RouterOSAPI(); + $password_router = $creds['password']; + if (isset($creds['source']) && $creds['source'] === 'legacy') { + $password_router = RouterOSAPI::decrypt($password_router); + } + + if ($API->connect($creds['ip'], $creds['user'], $password_router)) { + $items = $API->comm("/ip/hotspot/ip-binding/print"); + $API->disconnect(); + } else { + $error = "Connection Failed to " . $creds['ip']; + } + + $data = [ + 'session' => $session, + 'items' => $items, + 'error' => $error + ]; + + return $this->view('security/bindings', $data); + } + + public function storeBinding() { + if ($_SERVER['REQUEST_METHOD'] !== 'POST') { + header('Location: /'); + exit; + } + + $session = $_POST['session'] ?? ''; + $mac = $_POST['mac'] ?? ''; + $address = $_POST['address'] ?? ''; + $toAddress = $_POST['to_address'] ?? ''; + $server = $_POST['server'] ?? 'all'; + $type = $_POST['type'] ?? 'regular'; + $comment = $_POST['comment'] ?? ''; + + $configModel = new Config(); + $creds = $configModel->getSession($session); + if (!$creds) return; + + $API = new RouterOSAPI(); + $password_router = $creds['password']; + if (isset($creds['source']) && $creds['source'] === 'legacy') { + $password_router = RouterOSAPI::decrypt($password_router); + } + + if ($API->connect($creds['ip'], $creds['user'], $password_router)) { + $data = [ + 'mac-address' => $mac, + 'type' => $type, + 'comment' => $comment, + 'server' => $server + ]; + if(!empty($address)) $data['address'] = $address; + if(!empty($toAddress)) $data['to-address'] = $toAddress; + + $API->comm("/ip/hotspot/ip-binding/add", $data); + $API->disconnect(); + } + + \App\Helpers\FlashHelper::set('success', 'toasts.binding_added', 'toasts.binding_added_desc', [], true); + header("Location: /" . $session . "/hotspot/bindings"); + exit; + } + + public function removeBinding() { + if ($_SERVER['REQUEST_METHOD'] !== 'POST') { + header('Location: /'); + exit; + } + + $session = $_POST['session'] ?? ''; + $id = $_POST['id'] ?? ''; + + $configModel = new Config(); + $creds = $configModel->getSession($session); + if (!$creds) return; + + $API = new RouterOSAPI(); + $password_router = $creds['password']; + if (isset($creds['source']) && $creds['source'] === 'legacy') { + $password_router = RouterOSAPI::decrypt($password_router); + } + + if ($API->connect($creds['ip'], $creds['user'], $password_router)) { + $API->comm("/ip/hotspot/ip-binding/remove", [".id" => $id]); + $API->disconnect(); + } + + \App\Helpers\FlashHelper::set('success', 'toasts.binding_removed', 'toasts.binding_removed_desc', [], true); + header("Location: /" . $session . "/hotspot/bindings"); + exit; + } + + public function walledGarden($session) { + $configModel = new Config(); + $creds = $configModel->getSession($session); + if (!$creds) { + header("Location: /"); + exit; + } + + $items = []; + $error = null; + + $API = new RouterOSAPI(); + $password_router = $creds['password']; + if (isset($creds['source']) && $creds['source'] === 'legacy') { + $password_router = RouterOSAPI::decrypt($password_router); + } + + if ($API->connect($creds['ip'], $creds['user'], $password_router)) { + $items = $API->comm("/ip/hotspot/walled-garden/ip/print"); + // Standard walled garden print usually involves /ip/hotspot/walled-garden/ip/print for IP based or just /ip/hotspot/walled-garden/print + // Let's use /ip/hotspot/walled-garden/ip/print as general walled garden often implies IP based rules in modern RouterOS or just walled-garden + // Actually, usually there is /ip/hotspot/walled-garden (Dst Host, etc) and /ip/hotspot/walled-garden/ip (Dst Address, etc) + // Mikhmon v3 usually merges them or uses one. + // Let's check typical Mikhmon usage. Usually "Walled Garden" uses `/ip/hotspot/walled-garden/print` (which captures domains) and IP List uses `/ip/hotspot/walled-garden/ip/print`. + // My view lists Dst Host / IP. + // Let's fetch BOTH and merge, or just one. + // For now, let's target `/ip/hotspot/walled-garden/ip/print` as it allows protocol, port, dst-address, dst-host (in newer ROS?). + // Wait, `/ip/hotspot/walled-garden/print` allows `dst-host`. + // `/ip/hotspot/walled-garden/ip/print` allows `dst-address`. + // I'll stick to `/ip/hotspot/walled-garden/ip/print` for now as it seems more robust for IP rules, but domains need `walled-garden/print`. + // Actually, let's look at `walled_garden.php`. It handles `dst-host` or `dst-address`. + // I will use `/ip/hotspot/walled-garden/ip/print` which is "Walled Garden IP List". This is usually what people mean by "Walled Garden" for banking apps etc (IP ranges or strict definitions). + // BUT domain bypasses are in `/ip/hotspot/walled-garden/print`. + // Let's try to fetch `/ip/hotspot/walled-garden/ip/print` first. + + $items = $API->comm("/ip/hotspot/walled-garden/ip/print"); + $API->disconnect(); + } else { + $error = "Connection Failed to " . $creds['ip']; + } + + $data = [ + 'session' => $session, + 'items' => $items, + 'error' => $error + ]; + + return $this->view('security/walled_garden', $data); + } + + public function storeWalledGarden() { + if ($_SERVER['REQUEST_METHOD'] !== 'POST') { + header('Location: /'); + exit; + } + + $session = $_POST['session'] ?? ''; + $dstHost = $_POST['dst_host'] ?? ''; + $dstAddress = $_POST['dst_address'] ?? ''; + $protocol = $_POST['protocol'] ?? ''; + $dstPort = $_POST['dst_port'] ?? ''; + $action = $_POST['action'] ?? 'allow'; + $server = $_POST['server'] ?? 'all'; + $comment = $_POST['comment'] ?? ''; + + $configModel = new Config(); + $creds = $configModel->getSession($session); + if (!$creds) return; + + $API = new RouterOSAPI(); + $password_router = $creds['password']; + if (isset($creds['source']) && $creds['source'] === 'legacy') { + $password_router = RouterOSAPI::decrypt($password_router); + } + + if ($API->connect($creds['ip'], $creds['user'], $password_router)) { + $data = [ + 'action' => $action, + 'server' => $server, + 'comment' => $comment + ]; + + // If dst-host is present, we might need to use /ip/hotspot/walled-garden/add instead of /ip/.../ip/add? + // RouterOS distinguishes them. active.php shows I used `walled-garden/ip/print`. + // If user enters dst-host, it usually goes to `walled-garden`. If dst-address, `walled-garden/ip`. + // This is complex. Let's assume we are adding to `walled-garden/ip` for now which supports protocol/port/dst-address but NOT dst-host typically (older ROS). + // Actually, newer ROS might merge. + // Let's assume standard behavior: + // If dst-host is provided, add to `/ip/hotspot/walled-garden/add`. + // If dst-address is provided, add to `/ip/hotspot/walled-garden/ip/add`. + // My View asks for BOTH? + // Let's simplification: Check if dst_host is set. + + $path = "/ip/hotspot/walled-garden/ip/add"; + if (!empty($dstHost)) { + $path = "/ip/hotspot/walled-garden/add"; + $data['dst-host'] = $dstHost; + } else { + if(!empty($dstAddress)) $data['dst-address'] = $dstAddress; + } + + // Protocol and Port logic + // Note: `walled-garden` (host) takes protocol/port too? Yes. + if(!empty($protocol)) { + // extract protocol name if format is "(6) tcp" + if(preg_match('/\)\s*(\w+)/', $protocol, $m)) $protocol = $m[1]; + $data['protocol'] = $protocol; + } + if(!empty($dstPort)) $data['dst-port'] = $dstPort; + + $API->comm($path, $data); + $API->disconnect(); + } + + \App\Helpers\FlashHelper::set('success', 'toasts.rule_added', 'toasts.rule_added_desc', [], true); + header("Location: /" . $session . "/hotspot/walled-garden"); + exit; + } + + public function removeWalledGarden() { + if ($_SERVER['REQUEST_METHOD'] !== 'POST') { + header('Location: /'); + exit; + } + + $session = $_POST['session'] ?? ''; + $id = $_POST['id'] ?? ''; + + $configModel = new Config(); + $creds = $configModel->getSession($session); + if (!$creds) return; + + $API = new RouterOSAPI(); + $password_router = $creds['password']; + if (isset($creds['source']) && $creds['source'] === 'legacy') { + $password_router = RouterOSAPI::decrypt($password_router); + } + + if ($API->connect($creds['ip'], $creds['user'], $password_router)) { + $API->comm("/ip/hotspot/walled-garden/ip/remove", [".id" => $id]); + $API->disconnect(); + } + + \App\Helpers\FlashHelper::set('success', 'toasts.rule_removed', 'toasts.rule_removed_desc', [], true); + header("Location: /" . $session . "/hotspot/walled-garden"); + exit; + } + + // Print Single User + public function printUser($session, $id) { + return $this->printBatch($session, $id); + } + + // Print Batch Users (Comma separated IDs) + public function printBatchActions($session) { + $ids = $_GET['ids'] ?? ''; + return $this->printBatch($session, $ids); + } + + // Cookies + public function cookies($session) { + $configModel = new Config(); + $creds = $configModel->getSession($session); + if (!$creds) return header('Location: /'); + + $cookies = []; + $API = new RouterOSAPI(); + $API->attempts = 1; + $API->timeout = 3; + + if ($API->connect($creds['ip_address'], $creds['username'], $creds['password'])) { + $cookies = $API->comm("/ip/hotspot/cookie/print"); + $API->disconnect(); + } + + return $this->view('hotspot/cookies', [ + 'session' => $session, + 'cookies' => $cookies ?? [] + ]); + } + + public function removeCookie() { + $session = $_POST['session'] ?? ''; + $id = $_POST['id'] ?? ''; + + $configModel = new Config(); + $creds = $configModel->getSession($session); + if (!$creds) return; + + $API = new RouterOSAPI(); + if ($API->connect($creds['ip_address'], $creds['username'], $creds['password'])) { + $API->comm("/ip/hotspot/cookie/remove", [".id" => $id]); + $API->disconnect(); + } + + \App\Helpers\FlashHelper::set('success', 'toasts.cookie_removed', 'toasts.cookie_removed_desc', [], true); + header("Location: /$session/hotspot/cookies"); + } + + private function printBatch($session, $rawIds) { + $configModel = new Config(); + $creds = $configModel->getSession($session); + if (!$creds) die("Session error"); + + $API = new RouterOSAPI(); + $password_router = $creds['password']; + if (isset($creds['source']) && $creds['source'] === 'legacy') { + $password_router = RouterOSAPI::decrypt($password_router); + } + + // Handle ID List + // IDs can be "id1,id2,id3" + // Also Mikrotik IDs start with *, we need to ensure they are handled. + // If passed via URL, `*` might be encoded. + $idList = explode(',', urldecode($rawIds)); + $validUsers = []; + + if ($API->connect($creds['ip'], $creds['user'], $password_router)) { + // Optimized: Fetch ALL users and filter PHP side? + // Or fetch loop? Mikrotik API `/print` with `?.id` only supports single match usually or we need filtered print. + // Usually `print` returning all is faster for < 1000 users than 100 calls. + // But if we have 5000 users, we shouldn't fetch all. + // Mikrotik API doesn't support `WHERE id IN (...)`. + // So for batch, we might have to loop calls OR fetch all and filter if list is huge. + // Let's loop for now as batch print is usually < 50 items. + + foreach ($idList as $id) { + // Ensure ID has * if missing (unlikely if coming from app logic) + $req = $API->comm("/ip/hotspot/user/print", [ + "?.id" => $id + ]); + if (!empty($req)) { + $u = $req[0]; + $validUsers[] = [ + 'username' => $u['name'], + 'password' => $u['password'] ?? '', + 'price' => $u['price'] ?? '', + 'validity' => $u['limit-uptime'] ?? '', + 'timelimit' => \App\Helpers\HotspotHelper::formatValidity($u['limit-uptime'] ?? ''), + 'datalimit' => \App\Helpers\HotspotHelper::formatBytes($u['limit-bytes-total'] ?? 0), + 'profile' => $u['profile'] ?? 'default', + 'comment' => $u['comment'] ?? '', + 'hotspotname' => $creds['hotspot_name'], + 'dns_name' => $creds['dns_name'], + 'login_url' => (preg_match("~^(?:f|ht)tps?://~i", $creds['dns_name']) ? $creds['dns_name'] : "http://" . $creds['dns_name']) . "/login" + ]; + } + } + $API->disconnect(); + } + + if (empty($validUsers)) die("No users found"); + + // --- Template Handling --- + $tplModel = new VoucherTemplateModel(); + $templates = $tplModel->getAll(); // Need session? Model usually handles simple select, maybe filter by session later if needed? Schema says global? + // Verification: Schema in implementation plan says id, name, content... doesn't mention session. Assuming global. + + $currentTemplate = $_GET['template'] ?? 'default'; + $templateContent = ''; + + $viewName = 'print/default'; + + if ($currentTemplate !== 'default') { + $tpl = $tplModel->getById($currentTemplate); + if ($tpl) { + $templateContent = $tpl['content']; + $viewName = 'print/custom'; + } else { + // Fallback if ID invalid + $currentTemplate = 'default'; + } + } + + // --- Logo Handling --- + $logoModel = new \App\Models\Logo(); + $logos = $logoModel->getAll(); + $logoMap = []; + foreach ($logos as $l) { + $logoMap[$l['id']] = $l['path']; + } + + $data = [ + 'users' => $validUsers, + 'templates' => $templates, + 'currentTemplate' => $currentTemplate, + 'templateContent' => $templateContent, + 'session' => $session, + 'logoMap' => $logoMap + ]; + + return $this->view($viewName, $data); + } +} diff --git a/app/Controllers/InstallController.php b/app/Controllers/InstallController.php new file mode 100644 index 0000000..6a331f2 --- /dev/null +++ b/app/Controllers/InstallController.php @@ -0,0 +1,119 @@ +isInstalled()) { + header('Location: /login'); + exit; + } + + return $this->view('install'); + } + + public function process() { + if ($this->isInstalled()) { + header('Location: /login'); + exit; + } + + $username = $_POST['username'] ?? 'admin'; + $password = $_POST['password'] ?? 'admin'; + + try { + // 1. Run Migrations + Migrations::up(); + + // 2. Generate Key if default + if (SiteConfig::getSecretKey() === 'mikhmonv3remake_secret_key_32bytes') { + $this->generateKey(); + } + + // 3. Create Admin + $db = Database::getInstance(); + $hash = password_hash($password, PASSWORD_DEFAULT); + + // Check if user exists (edge case where key was default but user existed) + $check = $db->query("SELECT id FROM users WHERE username = ?", [$username])->fetch(); + if (!$check) { + $db->query("INSERT INTO users (username, password, created_at) VALUES (?, ?, ?)", [ + $username, $hash, date('Y-m-d H:i:s') + ]); + } else { + $db->query("UPDATE users SET password = ? WHERE username = ?", [$hash, $username]); + } + + // Success + \App\Helpers\FlashHelper::set('success', 'Installation Complete', 'System has been successfully installed. Please login.'); + header('Location: /login'); + exit; + + } catch (\Exception $e) { + \App\Helpers\FlashHelper::set('error', 'Installation Failed', $e->getMessage()); + header('Location: /install'); + exit; + } + } + + private function isInstalled() { + // Check if .env exists and APP_KEY is set to something other than the default/example + $envPath = ROOT . '/.env'; + if (!file_exists($envPath)) { + // Check if SiteConfig has a manual override (legacy) + return SiteConfig::getSecretKey() !== 'mikhmonv3remake_secret_key_32bytes'; + } + + $key = getenv('APP_KEY'); + $keyChanged = ($key && $key !== 'mikhmonv3remake_secret_key_32bytes'); + + try { + $db = Database::getInstance(); + $count = $db->query("SELECT count(*) as c FROM users")->fetchColumn(); + $hasUser = ($count > 0); + } catch (\Exception $e) { + $hasUser = false; + } + + return $keyChanged && $hasUser; + } + + private function generateKey() { + $key = bin2hex(random_bytes(16)); + $envPath = ROOT . '/.env'; + $examplePath = ROOT . '/.env.example'; + + if (!file_exists($envPath)) { + if (file_exists($examplePath)) { + copy($examplePath, $envPath); + } else { + return; // Cannot generate without source + } + } + + $content = file_get_contents($envPath); + + if (strpos($content, 'APP_KEY=') !== false) { + $newContent = preg_replace( + "/APP_KEY=.*/", + "APP_KEY=$key", + $content + ); + } else { + $newContent = $content . "\nAPP_KEY=$key"; + } + + file_put_contents($envPath, $newContent); + + // Refresh env in current session so next steps use it + putenv("APP_KEY=$key"); + $_ENV['APP_KEY'] = $key; + } +} diff --git a/app/Controllers/LogController.php b/app/Controllers/LogController.php new file mode 100644 index 0000000..e9b55fa --- /dev/null +++ b/app/Controllers/LogController.php @@ -0,0 +1,55 @@ +getSession($session); + if (!$config) return header('Location: /'); + + $logs = []; + $API = new RouterOSAPI(); + $API->attempts = 1; + $API->timeout = 3; + + if ($API->connect($config['ip_address'], $config['username'], $config['password'])) { + // Fetch Hotspot Logs + // /log/print where topics~hotspot + // In API we can't always filter effectively by topic in all versions, + // but we can try ?topics=hotspot,info or similar. + // Safe bet: fetch last 100 logs and filter PHP side or use API filter if possible. + // Using a limit to avoid timeout. + + // Getting generic logs for now, filtered by topic 'hotspot' if possible. + // RouterOS API query for array search: ?topics=hotspot + + $logs = $API->comm("/log/print", [ + "?topics" => "hotspot,info,debug", // Try detailed match + ]); + + // Fallback if strict match fails, just get recent logs + if (empty($logs) || isset($logs['!trap'])) { + $logs = $API->comm("/log/print", []); // Get all (capped usually by buffer) + } + + // Reverse to show newest first + if (is_array($logs)) { + $logs = array_reverse($logs); + } + + $API->disconnect(); + } + + return $this->view('reports/user_log', [ + 'session' => $session, + 'logs' => $logs + ]); + } +} diff --git a/app/Controllers/ProfileController.php b/app/Controllers/ProfileController.php new file mode 100644 index 0000000..03a16d5 --- /dev/null +++ b/app/Controllers/ProfileController.php @@ -0,0 +1,348 @@ +getSession($session); + + if (!$creds) { + $this->redirect('/'); + return; + } + + $API = new RouterOSAPI(); + // Use default port 8728 if not specified + if ($API->connect($creds['ip'], $creds['user'], $creds['password'])) { + $profiles = $API->comm('/ip/hotspot/user/profile/print'); + $API->disconnect(); + + // Process profiles to add metadata from on-login script + foreach ($profiles as &$profile) { + $meta = \App\Helpers\HotspotHelper::parseProfileMetadata($profile['on-login'] ?? ''); + $profile['meta'] = $meta; + $profile['meta']['expired_mode_formatted'] = \App\Helpers\HotspotHelper::formatExpiredMode($meta['expired_mode'] ?? ''); + } + + $this->view('hotspot/profiles/index', [ + 'session' => $session, + 'profiles' => $profiles, + 'title' => 'User Profiles' + ]); + } else { + $this->view('hotspot/profiles/index', [ + 'session' => $session, + 'profiles' => [], + 'error' => 'Connection Failed to ' . $creds['ip'], + 'title' => 'User Profiles' + ]); + } + } + + public function add($session) + { + $configModel = new Config(); + $creds = $configModel->getSession($session); + if (!$creds) { + $this->redirect('/'); + return; + } + + $API = new RouterOSAPI(); + $pools = []; + $queues = []; + + if ($API->connect($creds['ip'], $creds['user'], $creds['password'])) { + $pools = $API->comm('/ip/pool/print'); + + // Fetch Queues (Simple & Tree) + $simple = $API->comm('/queue/simple/print'); + $tree = $API->comm('/queue/tree/print'); + + // Extract just names for dropdown + foreach ($simple as $q) { + if(isset($q['name'])) $queues[] = $q['name']; + } + foreach ($tree as $q) { + if(isset($q['name'])) $queues[] = $q['name']; + } + sort($queues); + + $API->disconnect(); + } + + $this->view('hotspot/profiles/add', [ + 'session' => $session, + 'pools' => $pools, + 'queues' => $queues + ]); + } + + public function store() + { + if ($_SERVER['REQUEST_METHOD'] !== 'POST') { + $this->redirect('/'); + return; + } + + $session = $_POST['session'] ?? ''; + $name = $_POST['name'] ?? ''; + $sharedUsers = $_POST['shared-users'] ?? '1'; + $rateLimit = $_POST['rate-limit'] ?? ''; + $addressPool = $_POST['address-pool'] ?? 'none'; + $parentQueue = $_POST['parent-queue'] ?? 'none'; + + // Metadata fields + $expiredMode = $_POST['expired_mode'] ?? 'none'; + + // Validity Logic + $val_d = $_POST['validity_d'] ?? ''; + $val_h = $_POST['validity_h'] ?? ''; + $val_m = $_POST['validity_m'] ?? ''; + $validity = ''; + if($val_d) $validity .= $val_d . 'd'; + if($val_h) $validity .= $val_h . 'h'; + if($val_m) $validity .= $val_m . 'm'; + + $price = $_POST['price'] ?? ''; + $sellingPrice = $_POST['selling_price'] ?? ''; + $lockUser = $_POST['lock_user'] ?? 'Disable'; + + // Construct on-login script + // Construct on-login script + $metaScript = sprintf( + ':put (",%s,%s,%s,%s,,%s,")', + $expiredMode, + $price, + $validity, + $sellingPrice, + $lockUser + ); + + // Logic Script (The "Enforcer") - Enforces Calendar Validity + // Automates adding a scheduler to Disable user after "Validity" time passes from first login. + // Update: Added Self-Cleaning logic (:do {} on-error={}) to ensure scheduler deletes itself + // even if user was manually deleted from Winbox. + $logicScript = ""; + if (!empty($validity)) { + $logicScript = ' :local v "'.$validity.'"; :local u $user; :local c [/ip hotspot user get [find name=$u] comment]; :if ([:find $c "exp"] = -1) do={ /sys sch add name=$u interval=$v on-event=":do { /ip hotspot user set [find name=$u] disabled=yes } on-error={}; /sys sch remove [find name=$u]"; /ip hotspot user set [find name=$u] comment=("exp: " . $v . " " . $c); }'; + } + + $onLogin = $metaScript . $logicScript; + + $configModel = new Config(); + $creds = $configModel->getSession($session); + if (!$creds) return; + + $API = new RouterOSAPI(); + if ($API->connect($creds['ip'], $creds['user'], $creds['password'])) { + $profileData = [ + 'name' => $name, + 'shared-users' => $sharedUsers, + 'on-login' => $onLogin, + 'address-pool' => $addressPool, + 'parent-queue' => $parentQueue + ]; + + if ($parentQueue === 'none') { + unset($profileData['parent-queue']); // Or handle appropriately if Mikrotik accepts 'none' or unset + } + + if (!empty($rateLimit)) { + $profileData['rate-limit'] = $rateLimit; + } + + $API->comm("/ip/hotspot/user/profile/add", $profileData); + $API->disconnect(); + } + + \App\Helpers\FlashHelper::set('success', 'toasts.profile_created', 'toasts.profile_created_desc', ['name' => $name], true); + $this->redirect('/' . $session . '/hotspot/profiles'); + } + + + public function delete() + { + if ($_SERVER['REQUEST_METHOD'] !== 'POST') { + $this->redirect('/'); + return; + } + + $session = $_POST['session'] ?? ''; + $id = $_POST['id'] ?? ''; + + if (empty($session) || empty($id)) { + $this->redirect('/'); + return; + } + + $configModel = new Config(); + $creds = $configModel->getSession($session); + if (!$creds) return; + + $API = new RouterOSAPI(); + if ($API->connect($creds['ip'], $creds['user'], $creds['password'])) { + $API->comm("/ip/hotspot/user/profile/remove", [ + ".id" => $id, + ]); + $API->disconnect(); + } + + \App\Helpers\FlashHelper::set('success', 'toasts.profile_deleted', 'toasts.profile_deleted_desc', [], true); + $this->redirect('/' . $session . '/hotspot/profiles'); + } + + public function edit($session, $id) + { + $configModel = new Config(); + $creds = $configModel->getSession($session); + if (!$creds) { + $this->redirect('/'); + return; + } + + $API = new RouterOSAPI(); + $profile = null; + $pools = []; + $queues = []; + + if ($API->connect($creds['ip'], $creds['user'], $creds['password'])) { + $pools = $API->comm('/ip/pool/print'); + + // Fetch Queues (Simple & Tree) + $simple = $API->comm('/queue/simple/print'); + $tree = $API->comm('/queue/tree/print'); + + foreach ($simple as $q) { + if(isset($q['name'])) $queues[] = $q['name']; + } + foreach ($tree as $q) { + if(isset($q['name'])) $queues[] = $q['name']; + } + sort($queues); + + $profiles = $API->comm('/ip/hotspot/user/profile/print', [ + "?.id" => $id + ]); + + if (!empty($profiles)) { + $profile = $profiles[0]; + // Parse metadata + $meta = \App\Helpers\HotspotHelper::parseProfileMetadata($profile['on-login'] ?? ''); + $profile['meta'] = $meta; + + // Parse Validity + $val_d = ''; + $val_h = ''; + $val_m = ''; + + if (!empty($meta['validity'])) { + if (preg_match('/(\d+)d/', $meta['validity'], $m)) $val_d = $m[1]; + if (preg_match('/(\d+)h/', $meta['validity'], $m)) $val_h = $m[1]; + if (preg_match('/(\d+)m/', $meta['validity'], $m)) $val_m = $m[1]; + } + + $profile['val_d'] = $val_d; + $profile['val_h'] = $val_h; + $profile['val_m'] = $val_m; + } + + $API->disconnect(); + } + + if (!$profile) { + $this->redirect('/' . $session . '/hotspot/profiles'); + return; + } + + $this->view('hotspot/profiles/edit', [ + 'session' => $session, + 'profile' => $profile, + 'pools' => $pools, + 'queues' => $queues + ]); + } + + public function update() + { + if ($_SERVER['REQUEST_METHOD'] !== 'POST') { + $this->redirect('/'); + return; + } + + $session = $_POST['session'] ?? ''; + $id = $_POST['id'] ?? ''; + $name = $_POST['name'] ?? ''; + $sharedUsers = $_POST['shared-users'] ?? '1'; + $rateLimit = $_POST['rate-limit'] ?? ''; + $addressPool = $_POST['address-pool'] ?? 'none'; + $parentQueue = $_POST['parent-queue'] ?? 'none'; + + // Metadata fields + $expiredMode = $_POST['expired_mode'] ?? 'none'; + + // Validity Logic + $val_d = $_POST['validity_d'] ?? ''; + $val_h = $_POST['validity_h'] ?? ''; + $val_m = $_POST['validity_m'] ?? ''; + $validity = ''; + if($val_d) $validity .= $val_d . 'd'; + if($val_h) $validity .= $val_h . 'h'; + if($val_m) $validity .= $val_m . 'm'; + + $price = $_POST['price'] ?? ''; + $sellingPrice = $_POST['selling_price'] ?? ''; + $lockUser = $_POST['lock_user'] ?? 'Disable'; + + $metaScript = sprintf( + ':put (",%s,%s,%s,%s,,%s,")', + $expiredMode, + $price, + $validity, + $sellingPrice, + $lockUser + ); + + // Logic Script (The "Enforcer") + $logicScript = ""; + if (!empty($validity)) { + $logicScript = ' :local v "'.$validity.'"; :local u $user; :local c [/ip hotspot user get [find name=$u] comment]; :if ([:find $c "exp"] = -1) do={ /sys sch add name=$u interval=$v on-event=":do { /ip hotspot user set [find name=$u] disabled=yes } on-error={}; /sys sch remove [find name=$u]"; /ip hotspot user set [find name=$u] comment=("exp: " . $v . " " . $c); }'; + } + + $onLogin = $metaScript . $logicScript; + + $configModel = new Config(); + $creds = $configModel->getSession($session); + if (!$creds) return; + + $API = new RouterOSAPI(); + if ($API->connect($creds['ip'], $creds['user'], $creds['password'])) { + $profileData = [ + '.id' => $id, + 'name' => $name, + 'shared-users' => $sharedUsers, + 'on-login' => $onLogin, + 'address-pool' => $addressPool, + 'parent-queue' => $parentQueue + ]; + + if ($parentQueue === 'none') { + unset($profileData['parent-queue']); + } + + $profileData['rate-limit'] = $rateLimit; + + $API->comm("/ip/hotspot/user/profile/set", $profileData); + $API->disconnect(); + } + + \App\Helpers\FlashHelper::set('success', 'toasts.profile_updated', 'toasts.profile_updated_desc', ['name' => $name], true); + $this->redirect('/' . $session . '/hotspot/profiles'); + } +} diff --git a/app/Controllers/PublicStatusController.php b/app/Controllers/PublicStatusController.php new file mode 100644 index 0000000..2209323 --- /dev/null +++ b/app/Controllers/PublicStatusController.php @@ -0,0 +1,240 @@ +getSession($session); + + if (!$creds) { + // If session invalid, maybe show 404 or generic error + echo "Session not found."; + return; + } + + $data = [ + 'session' => $session, + 'hotspot_name' => $creds['hotspot_name'] ?? 'Hotspot', + 'footer_text' => SiteConfig::getFooter() + ]; + + return $this->view('public/status', $data); + } + + // API: Check Status + public function check($codeUrl = null) { + header('Content-Type: application/json'); + + // Allow POST and GET + if ($_SERVER['REQUEST_METHOD'] !== 'POST' && $_SERVER['REQUEST_METHOD'] !== 'GET') { + http_response_code(405); + echo json_encode(['error' => 'Method Not Allowed']); + return; + } + + $input = json_decode(file_get_contents('php://input'), true) ?? []; + + // Session: Try Body -> Try Header + $session = $input['session'] ?? ''; + if (empty($session)) { + $headers = getallheaders(); + // Handle case-insensitivity of headers + $session = $headers['X-Mivo-Session'] ?? ($headers['x-mivo-session'] ?? ''); + } + + // Code: Can be in URL or Body + $code = $codeUrl ?? ($input['code'] ?? ''); + + if (empty($session) || empty($code)) { + http_response_code(400); + echo json_encode(['error' => 'Session and Voucher Code are required']); + return; + } + + $configModel = new Config(); + $creds = $configModel->getSession($session); + + if (!$creds) { + http_response_code(404); + echo json_encode(['error' => 'Session not found']); + return; + } + + $password = $creds['password']; + if (isset($creds['source']) && $creds['source'] === 'legacy') { + $password = RouterOSAPI::decrypt($password); + } + + $api = new RouterOSAPI(); + if (!$api->connect($creds['ip'], $creds['user'], $password)) { + http_response_code(500); + echo json_encode(['error' => 'Router Connection Failed']); + return; + } + + // Logic Refactor: Pivot to User Table as primary source for Voucher Details + // 1. Check User in Database + $user = $api->comm("/ip/hotspot/user/print", [ + "?name" => $code + ]); + + if (!empty($user)) { + $u = $user[0]; + + // DEBUG: Log the user data to see raw values + error_log("Status Debug: " . json_encode($u)); + + // --- SECURITY CHECK: Hide Unused Vouchers --- + $uptimeRaw = $u['uptime'] ?? '0s'; + $bytesIn = intval($u['bytes-in'] ?? 0); + $bytesOut = intval($u['bytes-out'] ?? 0); + + if (($uptimeRaw === '0s' || empty($uptimeRaw)) && ($bytesIn + $bytesOut) === 0) { + $api->disconnect(); + echo json_encode(['success' => false, 'message' => 'Voucher Not Found']); + return; + } + + // --- SECURITY CHECK: Hide Unlimited Members --- + $limitBytes = isset($u['limit-bytes-total']) ? intval($u['limit-bytes-total']) : 0; + $limitUptime = $u['limit-uptime'] ?? '0s'; + + if ($limitBytes === 0 && ($limitUptime === '0s' || empty($limitUptime))) { + // Option: Allow checking them but show minimalistic info, or hide. + // Sticking to original logic: Hide them. + $api->disconnect(); + echo json_encode(['success' => false, 'message' => 'Voucher Not Found']); + return; + } + + // --- CALCULATIONS --- + $dataUsed = $bytesIn + $bytesOut; + $dataLeft = 'Unlimited'; + + if ($limitBytes > 0) { + $remaining = max(0, $limitBytes - $dataUsed); + $dataLeft = ($remaining === 0) ? '0 B' : FormatHelper::formatBytes($remaining); + } + + // Validity Logic + $validityRaw = $u['limit-uptime'] ?? '0s'; + $validityDisplay = ($validityRaw === '0s') ? 'Unlimited' : FormatHelper::elapsedTime($validityRaw); + $expiration = '-'; + + $comment = strtolower($u['comment'] ?? ''); + if (preg_match('/exp\W+([a-z]{3}\/\d{2}\/\d{4}\s\d{2}:\d{2}:\d{2})/', $comment, $matches)) { + $expiration = $matches[1]; + } elseif ($validityRaw !== '0s') { + $totalSeconds = FormatHelper::parseDuration($validityRaw); + $usedSeconds = FormatHelper::parseDuration($uptimeRaw); + $remainingSeconds = max(0, $totalSeconds - $usedSeconds); + + if ($remainingSeconds > 0) { + $expiration = date('d M Y H:i', time() + $remainingSeconds); + } else { + $expiration = 'Expired'; + } + } + + // BASE STATUS + $status = 'offline'; + $statusLabel = 'Valid / Offline'; + $isDisabled = ($u['disabled'] ?? 'false') === 'true'; + + // Calculate Time Left + $timeLeft = 'Unlimited'; + if ($expiration !== '-' && $expiration !== 'Expired') { + $expTime = strtotime($expiration); + if ($expTime) { + $rem = max(0, $expTime - time()); + $timeLeft = ($rem === 0) ? 'Expired' : FormatHelper::formatSeconds($rem); + } + } elseif ($validityRaw !== '0s') { + $totalSeconds = FormatHelper::parseDuration($validityRaw); + $usedSeconds = FormatHelper::parseDuration($uptimeRaw); + $rem = max(0, $totalSeconds - $usedSeconds); + $timeLeft = ($rem === 0) ? 'Expired' : FormatHelper::formatSeconds($rem); + } + + if (strpos($comment, 'exp') !== false || ($expiration === 'Expired')) { + $status = 'expired'; + $statusLabel = 'Expired'; + } elseif ($limitBytes > 0 && $dataUsed >= $limitBytes) { + $status = 'limited'; + $statusLabel = 'Quota Exceeded'; + } elseif ($isDisabled) { + $status = 'locked'; + $statusLabel = 'Locked / Disabled'; + } + + // 2. CHECK ACTIVE OVERRIDE + // If user is conceptually valid (or even if limited?), check if they are currently active + // Because they might be active BUT expiring soon, or active BUT over quota (if server hasn't kicked them yet) + $active = $api->comm("/ip/hotspot/active/print", [ + "?user" => $code + ]); + + if (!empty($active)) { + $status = 'active'; + $statusLabel = 'Active (Online)'; + } + + $data = [ + 'status' => $status, + 'status_label' => $statusLabel, + 'username' => $u['name'] ?? 'Unknown', + 'profile' => $u['profile'] ?? 'default', + 'uptime_used' => FormatHelper::elapsedTime($uptimeRaw), + 'validity' => $validityDisplay, + 'data_used' => FormatHelper::formatBytes($dataUsed), + 'data_left' => $dataLeft, + 'expiration' => $expiration, + 'time_left' => $timeLeft, + 'comment' => $u['comment'] ?? '', + ]; + + echo json_encode(['success' => true, 'data' => $data]); + $api->disconnect(); + return; + } + + // 3. Fallback: Check Active Only (Trial Users or IP Bindings not in User Table) + $active = $api->comm("/ip/hotspot/active/print", [ + "?user" => $code + ]); + + if (!empty($active)) { + $u = $active[0]; + $data = [ + 'status' => 'active', + 'status_label' => 'Active (Online)', + 'username' => $u['user'] ?? 'Unknown', + 'profile' => '-', // Active usually doesn't have profile name directly unless queried + 'uptime_used' => FormatHelper::elapsedTime($u['uptime'] ?? '0s'), + 'validity' => '-', + 'data_used' => FormatHelper::formatBytes(intval($u['bytes-in'] ?? 0) + intval($u['bytes-out'] ?? 0)), + 'data_left' => 'Unknown', + 'time_left' => isset($u['session-time-left']) ? FormatHelper::elapsedTime($u['session-time-left']) : '-', + 'expiration' => '-', + 'comment' => '' + ]; + echo json_encode(['success' => true, 'data' => $data]); + $api->disconnect(); + return; + } + + $api->disconnect(); + echo json_encode(['success' => false, 'message' => 'Voucher Not Found']); + } +} diff --git a/app/Controllers/QuickPrintController.php b/app/Controllers/QuickPrintController.php new file mode 100644 index 0000000..b56e307 --- /dev/null +++ b/app/Controllers/QuickPrintController.php @@ -0,0 +1,220 @@ +getAllBySession($session); + + $data = [ + 'session' => $session, + 'packages' => $packages + ]; + // Note: View will be 'quick_print/index' + return $this->view('quick_print/index', $data); + } + + // List/Manage Packages (CRUD) + public function manage($session) { + $qpModel = new QuickPrintModel(); + $packages = $qpModel->getAllBySession($session); + + // Need profiles for the Add/Edit Modal + $configModel = new Config(); + $creds = $configModel->getSession($session); + $profiles = []; + if ($creds) { + $API = new RouterOSAPI(); + $password = $creds['password']; + if (isset($creds['source']) && $creds['source'] === 'legacy') { + $password = RouterOSAPI::decrypt($password); + } + if ($API->connect($creds['ip'], $creds['user'], $password)) { + $profiles = $API->comm("/ip/hotspot/user/profile/print"); + $API->disconnect(); + } + } + + $data = [ + 'session' => $session, + 'packages' => $packages, + 'profiles' => $profiles + ]; + return $this->view('quick_print/list', $data); + } + + // CRUD: Store + public function store() { + if ($_SERVER['REQUEST_METHOD'] !== 'POST') return; + + $session = $_POST['session'] ?? ''; + $data = [ + 'session_name' => $session, + 'name' => $_POST['name'] ?? 'Package', + 'server' => $_POST['server'] ?? 'all', + 'profile' => $_POST['profile'] ?? 'default', + 'prefix' => $_POST['prefix'] ?? '', + 'char_length' => $_POST['char_length'] ?? 4, + 'price' => $_POST['price'] ?? 0, + 'time_limit' => $_POST['time_limit'] ?? '', + 'data_limit' => $_POST['data_limit'] ?? '', + 'comment' => $_POST['comment'] ?? '', + 'color' => $_POST['color'] ?? 'bg-blue-500' + ]; + + $qpModel = new QuickPrintModel(); + $qpModel->add($data); + + \App\Helpers\FlashHelper::set('success', 'toasts.package_saved', 'toasts.package_saved_desc', [], true); + header("Location: /" . $session . "/quick-print/manage"); + exit; + } + + // CRUD: Delete + public function delete() { + if ($_SERVER['REQUEST_METHOD'] !== 'POST') return; + $session = $_POST['session'] ?? ''; + $id = $_POST['id'] ?? ''; + + $qpModel = new QuickPrintModel(); + $qpModel->delete($id); + + \App\Helpers\FlashHelper::set('success', 'toasts.package_deleted', 'toasts.package_deleted_desc', [], true); + header("Location: /" . $session . "/quick-print/manage"); + exit; + } + + // ACTION: Generate User & Print + public function printPacket($session, $id) { + // 1. Get Package Details + $qpModel = new QuickPrintModel(); + $package = $qpModel->getById($id); + + if (!$package) { + die("Package not found"); + } + + // 2. Generate Credentials + $prefix = $package['prefix']; + $length = $package['char_length']; + $charSet = '1234567890abcdefghijklmnopqrstuvwxyz'; // Simple lowercase + num + $rand = substr(str_shuffle($charSet), 0, $length); + $username = $prefix . $rand; + $password = $username; // Default: user=pass (User Mode) - Can be improved later + + // 3. Connect to Mikrotik & Add User + $configModel = new Config(); + $creds = $configModel->getSession($session); + if (!$creds) die("Session error"); + + $API = new RouterOSAPI(); + $password_router = $creds['password']; + if (isset($creds['source']) && $creds['source'] === 'legacy') { + $password_router = RouterOSAPI::decrypt($password_router); + } + + if ($API->connect($creds['ip'], $creds['user'], $password_router)) { + $userData = [ + 'name' => $username, + 'password' => $password, + 'profile' => $package['profile'], + 'comment' => $package['comment'] . " [QP]" // Mark as QuickPrint + ]; + + // Limits + if(!empty($package['time_limit'])) $userData['limit-uptime'] = $package['time_limit']; + if(!empty($package['data_limit'])) { + // Check if M or G + // Simple logic for now, assuming raw if number, or passing string if Mikrotik accepts it (usually requires bytes) + // Let's assume user inputs "100M" or "1G" which usually needs parsing. + // For now, let's assume input is NUMBER in MB as per standard Mikhmon practice, OR generic string. + // We'll pass as is for strings, or multiply if strictly numeric? + // Let's rely on standard Mikrotik parsing if string passed, or convert. + // Mikhmon v3 usually uses dropdown "MB/GB". + // Implementing simple conversion: + $val = intval($package['data_limit']); + if (strpos(strtolower($package['data_limit']), 'g') !== false) { + $userData['limit-bytes-total'] = $val * 1024 * 1024 * 1024; + } else { + $userData['limit-bytes-total'] = $val * 1024 * 1024; // Default MB + } + } + + $API->comm("/ip/hotspot/user/add", $userData); + $API->disconnect(); + } else { + die("Connection failed"); + } + + + // 4. Render Template + $tplModel = new VoucherTemplateModel(); + $templates = $tplModel->getAll(); + + $currentTemplate = $_GET['template'] ?? 'default'; + $templateContent = ''; + $viewName = 'print/default'; + + if ($currentTemplate !== 'default') { + $tpl = $tplModel->getById($currentTemplate); + if ($tpl) { + $templateContent = $tpl['content']; + $viewName = 'print/custom'; + } else { + $currentTemplate = 'default'; + } + } + + // Calculate bytes for display + $dlVal = intval($package['data_limit']); + $bytes = (strpos(strtolower($package['data_limit']), 'g') !== false) ? $dlVal * 1024*1024*1024 : $dlVal * 1024*1024; + + $userDataValues = [ + 'username' => $username, + 'password' => $password, + 'price' => $package['price'], + 'validity' => $package['time_limit'], + 'timelimit' => \App\Helpers\HotspotHelper::formatValidity($package['time_limit']), + 'datalimit' => \App\Helpers\HotspotHelper::formatBytes($bytes), + 'profile' => $package['profile'], + 'comment' => 'Quick Print', + 'hotspotname' => $creds['hotspot_name'], + 'dns_name' => $creds['dns_name'], + 'login_url' => (preg_match("~^(?:f|ht)tps?://~i", $creds['dns_name']) ? $creds['dns_name'] : "http://" . $creds['dns_name']) . "/login" + ]; + + // --- Logo Handling --- + $logoModel = new \App\Models\Logo(); + $logos = $logoModel->getAll(); + $logoMap = []; + foreach ($logos as $l) { + $logoMap[$l['id']] = $l['path']; + } + + $data = [ + 'users' => [$userDataValues], + 'templates' => $templates, + 'currentTemplate' => $currentTemplate, + 'templateContent' => $templateContent, + 'session' => $session, + 'logoMap' => $logoMap + ]; + + return $this->view($viewName, $data); + } +} diff --git a/app/Controllers/ReportController.php b/app/Controllers/ReportController.php new file mode 100644 index 0000000..c989f25 --- /dev/null +++ b/app/Controllers/ReportController.php @@ -0,0 +1,165 @@ +getSession($session); + + if (!$config) { + header('Location: /'); + exit; + } + + $API = new RouterOSAPI(); + $users = []; + + if ($API->connect($config['ip_address'], $config['username'], $config['password'])) { + // Fetch All Users + // Optimized print: get .id, name, price, comment + $users = $API->comm("/ip/hotspot/user/print"); + $API->disconnect(); + } + + // Aggregate Data + $report = []; + $totalIncome = 0; + $totalVouchers = 0; + + foreach ($users as $user) { + // Skip if no price + if (empty($user['price']) || $user['price'] == '0') continue; + + // Determine Date from Comment + // Mikhmon format usually: "mikhmon-MM/DD/YYYY" or just "MM/DD/YYYY" or plain comment + // We will try to parse a date from the comment, or use "Unknown Date" + $date = 'Unknown Date'; + $comment = $user['comment'] ?? ''; + + // Regex for date patterns (d-m-Y or m/d/Y or Y-m-d) + // Simplify: Group by Comment content itself if it looks like a date/batch + // Or try to extract M-Y. + + // For feature parity, Mikhmon often groups by the exact comment string as the "Batch/Date" + if (!empty($comment)) { + $date = $comment; + } + + if (!isset($report[$date])) { + $report[$date] = [ + 'date' => $date, + 'count' => 0, + 'total' => 0 + ]; + } + + $price = intval($user['price']); + $report[$date]['count']++; + $report[$date]['total'] += $price; + + $totalIncome += $price; + $totalVouchers++; + } + + // Sort by key (Date/Comment) desc + krsort($report); + + return $this->view('reports/selling', [ + 'session' => $session, + 'report' => $report, + 'totalIncome' => $totalIncome, + 'totalVouchers' => $totalVouchers, + 'currency' => $config['currency'] ?? 'Rp' + ]); + } + public function resume($session) + { + $configModel = new Config(); + $config = $configModel->getSession($session); + + if (!$config) { + header('Location: /'); + exit; + } + + $API = new RouterOSAPI(); + $users = []; + + if ($API->connect($config['ip_address'], $config['username'], $config['password'])) { + $users = $API->comm("/ip/hotspot/user/print"); + $API->disconnect(); + } + + // Initialize Aggregates + $daily = []; + $monthly = []; + $yearly = []; + $totalIncome = 0; + + foreach ($users as $user) { + if (empty($user['price']) || $user['price'] == '0') continue; + + // Try to parse Date from Comment (Mikhmon format: mikhmon-10/25/2023 or just 10/25/2023) + $comment = $user['comment'] ?? ''; + $dateObj = null; + + // Simple parser: try to find MM/DD/YYYY + if (preg_match('/(\d{1,2})[\/.-](\d{1,2})[\/.-](\d{2,4})/', $comment, $matches)) { + // Assuming MM/DD/YYYY based on typical Mikhmon, but could be DD-MM-YYYY + // Let's standardise on checking valid date. + // Standard Mikhmon V3 is MM/DD/YYYY. + $m = $matches[1]; + $d = $matches[2]; + $y = $matches[3]; + if (strlen($y) == 2) $y = '20' . $y; + $dateObj = \DateTime::createFromFormat('m/d/Y', "$m/$d/$y"); + } + + // Fallback: If no date found in comment, maybe created at? + // Usually Mikhmon relies strictly on comment. + if (!$dateObj) continue; + + $price = intval($user['price']); + $totalIncome += $price; + + // Formats + $dayKey = $dateObj->format('Y-m-d'); + $monthKey = $dateObj->format('Y-m'); + $yearKey = $dateObj->format('Y'); + + // Daily + if (!isset($daily[$dayKey])) $daily[$dayKey] = 0; + $daily[$dayKey] += $price; + + // Monthly + if (!isset($monthly[$monthKey])) $monthly[$monthKey] = 0; + $monthly[$monthKey] += $price; + + // Yearly + if (!isset($yearly[$yearKey])) $yearly[$yearKey] = 0; + $yearly[$yearKey] += $price; + } + + // Sort Keys + ksort($daily); + ksort($monthly); + ksort($yearly); + + return $this->view('reports/resume', [ + 'session' => $session, + 'daily' => $daily, + 'monthly' => $monthly, + 'yearly' => $yearly, + 'totalIncome' => $totalIncome, + 'currency' => $config['currency'] ?? 'Rp' + ]); + } +} diff --git a/app/Controllers/SchedulerController.php b/app/Controllers/SchedulerController.php new file mode 100644 index 0000000..90f5444 --- /dev/null +++ b/app/Controllers/SchedulerController.php @@ -0,0 +1,97 @@ +getSession($session); + + if (!$config) { + header('Location: /'); + exit; + } + + $API = new RouterOSAPI(); + $schedulers = []; + + if ($API->connect($config['ip_address'], $config['username'], $config['password'])) { + $schedulers = $API->comm("/system/scheduler/print"); + $API->disconnect(); + } + + return $this->view('system/scheduler', [ + 'session' => $session, + 'schedulers' => $schedulers + ]); + } + + public function store($session) + { + $configModel = new Config(); + $config = $configModel->getSession($session); + if (!$config) exit; + + $API = new RouterOSAPI(); + if ($API->connect($config['ip_address'], $config['username'], $config['password'])) { + $API->comm("/system/scheduler/add", [ + "name" => $_POST['name'], + "on-event" => $_POST['on_event'], + "start-date" => $_POST['start_date'], + "start-time" => $_POST['start_time'], + "interval" => $_POST['interval'], + "comment" => $_POST['comment'] ?? '', + "disabled" => "no" + ]); + $API->disconnect(); + } + \App\Helpers\FlashHelper::set('success', 'toasts.schedule_added', 'toasts.schedule_added_desc', [], true); + header("Location: /$session/system/scheduler"); + } + + public function update($session) + { + $configModel = new Config(); + $config = $configModel->getSession($session); + if (!$config) exit; + + $API = new RouterOSAPI(); + if ($API->connect($config['ip_address'], $config['username'], $config['password'])) { + $API->comm("/system/scheduler/set", [ + ".id" => $_POST['id'], + "name" => $_POST['name'], + "on-event" => $_POST['on_event'], + "start-date" => $_POST['start_date'], + "start-time" => $_POST['start_time'], + "interval" => $_POST['interval'], + "comment" => $_POST['comment'] ?? '' + ]); + $API->disconnect(); + } + \App\Helpers\FlashHelper::set('success', 'toasts.schedule_updated', 'toasts.schedule_updated_desc', [], true); + header("Location: /$session/system/scheduler"); + } + + public function delete($session) + { + $configModel = new Config(); + $config = $configModel->getSession($session); + if (!$config) exit; + + $API = new RouterOSAPI(); + if ($API->connect($config['ip_address'], $config['username'], $config['password'])) { + $API->comm("/system/scheduler/remove", [ + ".id" => $_POST['id'] + ]); + $API->disconnect(); + } + \App\Helpers\FlashHelper::set('success', 'toasts.schedule_deleted', 'toasts.schedule_deleted_desc', [], true); + header("Location: /$session/system/scheduler"); + } +} diff --git a/app/Controllers/SettingsController.php b/app/Controllers/SettingsController.php new file mode 100644 index 0000000..fbafe24 --- /dev/null +++ b/app/Controllers/SettingsController.php @@ -0,0 +1,462 @@ +getAll(); + + $username = $_SESSION['username'] ?? 'admin'; + + return $this->view('settings/systems', [ + 'settings' => $settings, + 'username' => $username + ]); + } + + public function routers() { + // Routers List Tab + $configModel = new Config(); + $routers = $configModel->getAllSessions(); + return $this->view('settings/index', ['routers' => $routers]); + } + + public function add() { + return $this->view('settings/form'); + } + + // ... (Existing Store methods) ... + public function store() { + // Sanitize Session Name (Duplicate Frontend Logic) + $rawSess = $_POST['sessname'] ?? ''; + $sessName = preg_replace('/[^a-z0-9-]/', '', strtolower(str_replace(' ', '-', $rawSess))); + + $data = [ + 'session_name' => $sessName, + 'ip_address' => $_POST['ipmik'], + 'username' => $_POST['usermik'], + 'password' => $_POST['passmik'], + 'hotspot_name' => $_POST['hotspotname'], + 'dns_name' => $_POST['dnsname'], + 'currency' => $_POST['currency'], + 'reload_interval' => $_POST['areload'], + 'interface' => $_POST['iface'], + 'description' => 'Added via Remake', + 'quick_access' => isset($_POST['quick_access']) ? 1 : 0 + ]; + + $configModel = new Config(); + try { + $configModel->addSession($data); + + $redirect = '/settings/routers'; + if (isset($_POST['action']) && $_POST['action'] === 'connect') { + $redirect = '/' . urlencode($data['session_name']) . '/dashboard'; + } + + \App\Helpers\FlashHelper::set('success', 'toasts.router_added', 'toasts.router_added_desc', ['name' => $data['session_name']], true); + header("Location: $redirect"); + } catch (\Exception $e) { + echo "Error adding session: " . $e->getMessage(); + } + } + + // Update Admin Password + public function updateAdmin() { + $newPassword = $_POST['admin_password'] ?? ''; + + if (!empty($newPassword)) { + $db = \App\Core\Database::getInstance(); + $hash = password_hash($newPassword, PASSWORD_DEFAULT); + // Assuming we are updating the default 'admin' user or the currently logged in user + // Original Mikhmon usually has one main user. Let's update 'admin' for now. + $db->query("UPDATE users SET password = ? WHERE username = 'admin'", [$hash]); + \App\Helpers\FlashHelper::set('success', 'toasts.password_updated', 'toasts.password_updated_desc', [], true); + } + + header('Location: /settings/system'); + } + + // Update Global Settings + public function updateGlobal() { + $settingModel = new \App\Models\Setting(); + + if (isset($_POST['quick_print_mode'])) { + $settingModel->set('quick_print_mode', $_POST['quick_print_mode']); + \App\Helpers\FlashHelper::set('success', 'toasts.settings_saved', 'toasts.settings_saved_desc', [], true); + } + + header('Location: /settings/system'); + } + + + public function edit() { + // ID passed via query param or route param? + // Our router supports {id} but let's check how we handle it. + // Router: /settings/edit/{id} + // In Router.php, params are passed to method. + // So method signature should be edit($id) + + // Wait, Router.php passes matches as params array to invokeCallback. + // So we need to capture arguments here. + $args = func_get_args(); + $id = $args[0] ?? null; + + if (!$id) { + header('Location: /settings/routers'); + exit; + } + + $configModel = new Config(); + $session = $configModel->getSessionById($id); + + if (!$session) { + header('Location: /settings/routers'); + exit; + } + + return $this->view('settings/form', ['router' => $session]); + } + + public function update() { + $id = $_POST['id']; + + // Sanitize Session Name + $rawSess = $_POST['sessname'] ?? ''; + $sessName = preg_replace('/[^a-z0-9-]/', '', strtolower(str_replace(' ', '-', $rawSess))); + + $data = [ + 'session_name' => $sessName, + 'ip_address' => $_POST['ipmik'], + 'username' => $_POST['usermik'], + 'password' => $_POST['passmik'], // Can be empty if not changing + 'hotspot_name' => $_POST['hotspotname'], + 'dns_name' => $_POST['dnsname'], + 'currency' => $_POST['currency'], + 'reload_interval' => $_POST['areload'], + 'interface' => $_POST['iface'], + 'description' => 'Updated via Remake', + 'quick_access' => isset($_POST['quick_access']) ? 1 : 0 + ]; + + $configModel = new Config(); + try { + $configModel->updateSession($id, $data); + + $redirect = '/settings/routers'; + if (isset($_POST['action']) && $_POST['action'] === 'connect') { + $redirect = '/' . urlencode($data['session_name']) . '/dashboard'; + } + + \App\Helpers\FlashHelper::set('success', 'toasts.router_updated', 'toasts.router_updated_desc', ['name' => $data['session_name']], true); + header("Location: $redirect"); + } catch (\Exception $e) { + echo "Error updating session: " . $e->getMessage(); + } + } + + public function delete() { + $id = $_POST['id']; + $configModel = new Config(); + $configModel->deleteSession($id); + \App\Helpers\FlashHelper::set('success', 'toasts.router_deleted', 'toasts.router_deleted_desc', [], true); + header('Location: /settings/routers'); + } + + public function backup() { + $backupName = 'mivo_backup_' . date('d-m-Y') . '.mivo'; + $json = []; + + // Backup Settings + $settingModel = new \App\Models\Setting(); + $settings = $settingModel->getAll(); + $json['settings'] = $settings; + + // Backup Sessions + $configModel = new Config(); + $sessions = $configModel->getAllSessions(); + + // Decrypt passwords for portability + foreach ($sessions as &$session) { + if (!empty($session['password'])) { + $session['password'] = \App\Helpers\EncryptionHelper::decrypt($session['password']); + } + } + $json['sessions'] = $sessions; + + // Backup Voucher Templates + $templateModel = new \App\Models\VoucherTemplateModel(); + $json['voucher_templates'] = $templateModel->getAll(); + + // Backup Logos + $logoModel = new \App\Models\Logo(); + $logos = $logoModel->getAll(); + foreach ($logos as &$logo) { + $filePath = ROOT . '/public' . $logo['path']; + if (file_exists($filePath)) { + $logo['data'] = base64_encode(file_get_contents($filePath)); + } + } + $json['logos'] = $logos; + + // Encode + $jsonString = json_encode($json, JSON_PRETTY_PRINT); + + // Encrypt the entire file content for security + // Decrypted data inside (like passwords) remain plaintext relative to the JSON structure + // ensuring portability if decrypted successfully. + $content = \App\Helpers\EncryptionHelper::encrypt($jsonString); + + // Force Download + header('Content-Description: File Transfer'); + header('Content-Type: application/octet-stream'); + header('Content-Disposition: attachment; filename='.basename($backupName)); + header('Content-Transfer-Encoding: binary'); + header('Expires: 0'); + header('Cache-Control: must-revalidate'); + header('Pragma: public'); + header('Content-Length: ' . strlen($content)); + ob_clean(); + flush(); + echo $content; + exit; + } + + public function restore() { + if (!isset($_FILES['backup_file']) || $_FILES['backup_file']['error'] !== UPLOAD_ERR_OK) { + \App\Helpers\FlashHelper::set('error', 'toasts.restore_failed', 'toasts.no_file_selected', [], true); + header('Location: /settings/system'); + exit; + } + + $file = $_FILES['backup_file']; + $filename = $file['name']; + $extension = strtolower(pathinfo($filename, PATHINFO_EXTENSION)); + $mime = $file['type']; + + // Validate Extension & MIME + $allowedExtensions = ['mivo']; + $allowedMimes = ['application/octet-stream', 'text/plain']; // text/plain fallback for some OS/Browsers + + if (!in_array($extension, $allowedExtensions) || (!empty($mime) && !in_array($mime, $allowedMimes))) { + \App\Helpers\FlashHelper::set('error', 'toasts.restore_failed', 'toasts.invalid_file_type_mivo', [], true); + header('Location: /settings/system'); + exit; + } + + $rawValue = file_get_contents($file['tmp_name']); + if (empty($rawValue)) { + \App\Helpers\FlashHelper::set('error', 'toasts.restore_failed', 'toasts.file_empty', [], true); + header('Location: /settings/system'); + exit; + } + + // Attempt to decrypt. If file is old (JSON plaintext), decrypt() returns it as-is. + $content = \App\Helpers\EncryptionHelper::decrypt($rawValue); + + $json = json_decode($content, true); + + if (!$json || (!isset($json['settings']) && !isset($json['sessions']))) { + \App\Helpers\FlashHelper::set('error', 'toasts.restore_failed', 'toasts.file_corrupted', [], true); + header('Location: /settings/system'); + exit; + } + + // Restore Settings + if (isset($json['settings'])) { + $settingModel = new \App\Models\Setting(); + // Assuming we check if data exists + // We might need to iterate and update + foreach ($json['settings'] as $key => $val) { + $settingModel->set($key, $val); + } + } + + // Restore Sessions + if (isset($json['sessions'])) { + $configModel = new Config(); + foreach ($json['sessions'] as $session) { + unset($session['id']); // Let system generate new ID + try { + $configModel->addSession($session); + } catch (\Exception $e) { + error_log("Failed to restore session: " . ($session['session_name'] ?? 'unknown')); + } + } + } + + // Restore Voucher Templates + if (isset($json['voucher_templates'])) { + $templateModel = new \App\Models\VoucherTemplateModel(); + foreach ($json['voucher_templates'] as $tmpl) { + // Check if template exists by name and session + $db = \App\Core\Database::getInstance(); + $existing = $db->query("SELECT id FROM voucher_templates WHERE name = ? AND session_name = ?", [$tmpl['name'], $tmpl['session_name']])->fetch(); + + if ($existing) { + $templateModel->update($existing['id'], $tmpl); + } else { + $templateModel->add($tmpl); + } + } + } + + // Restore Logos + if (isset($json['logos'])) { + $logoModel = new \App\Models\Logo(); + $uploadDir = ROOT . '/public/assets/img/logos/'; + if (!file_exists($uploadDir)) { + mkdir($uploadDir, 0777, true); + } + + foreach ($json['logos'] as $logo) { + if (empty($logo['data'])) continue; + + // Decode data + $binaryData = base64_decode($logo['data']); + if (!$binaryData) continue; + + // Determine filename (try to keep original ID/name or generate new) + $extension = $logo['type'] ?? 'png'; + $filename = $logo['id'] . '.' . $extension; + $targetPath = $uploadDir . $filename; + + // Save file + if (file_put_contents($targetPath, $binaryData)) { + // Update DB + $db = \App\Core\Database::getInstance(); + $db->query("INSERT INTO logos (id, name, path, type, size) VALUES (:id, :name, :path, :type, :size) + ON CONFLICT(id) DO UPDATE SET name=excluded.name, path=excluded.path, type=excluded.type, size=excluded.size", [ + 'id' => $logo['id'], + 'name' => $logo['name'], + 'path' => '/assets/img/logos/' . $filename, + 'type' => $extension, + 'size' => $logo['size'] + ]); + } + } + } + + \App\Helpers\FlashHelper::set('success', 'toasts.restore_success', 'toasts.restore_success_desc', [], true); + header('Location: /settings/system'); + } + + // --- Logo Management --- + + public function logos() { + $logoModel = new \App\Models\Logo(); // Fully qualified to avoid import issues for now or add import + $logoModel->syncFiles(); // Ensure FS and DB are in sync + $logos = $logoModel->getAll(); + + // Format size for display (since DB stores raw bytes or maybe we want helper there) + // Actually model stored bytes, we format in View or here. + // Let's format here for consistency with previous view. + foreach ($logos as &$logo) { + $logo['formatted_size'] = FormatHelper::formatBytes($logo['size']); + } + + return $this->view('settings/logos', ['logos' => $logos]); + } + + public function uploadLogo() { + if (!isset($_FILES['logo_file'])) { + header('Location: /settings/logos'); + exit; + } + + $logoModel = new \App\Models\Logo(); + try { + $logoModel->add($_FILES['logo_file']); + } catch (\Exception $e) { + // Ideally flash error message to session + // For now, redirect (logging error via debug or ignoring as per simple req) + // session_start() is implicit in Middleware usually or index + // $_SESSION['error'] = $e->getMessage(); + } + + \App\Helpers\FlashHelper::set('success', 'toasts.logo_uploaded', 'toasts.logo_uploaded_desc', [], true); + header('Location: /settings/logos'); + } + + public function deleteLogo() { + $id = $_POST['id']; // Changed from filename to id + + $logoModel = new \App\Models\Logo(); + $logoModel->delete($id); + + \App\Helpers\FlashHelper::set('success', 'toasts.logo_deleted', 'toasts.logo_deleted_desc', [], true); + header('Location: /settings/logos'); + } + + // --- API CORS Management --- + + public function apiCors() { + $db = \App\Core\Database::getInstance(); + $rules = $db->query("SELECT * FROM api_cors ORDER BY created_at DESC")->fetchAll(); + + // Decode JSON methods and headers for view + foreach ($rules as &$rule) { + $rule['methods_arr'] = json_decode($rule['methods'], true) ?: []; + $rule['headers_arr'] = json_decode($rule['headers'], true) ?: []; + } + + return $this->view('settings/api_cors', ['rules' => $rules]); + } + + public function storeApiCors() { + $origin = $_POST['origin'] ?? ''; + $methods = isset($_POST['methods']) ? json_encode($_POST['methods']) : '["GET","POST"]'; + $headers = isset($_POST['headers']) ? json_encode(array_map('trim', explode(',', $_POST['headers']))) : '["*"]'; + $maxAge = (int)($_POST['max_age'] ?? 3600); + + if (!empty($origin)) { + $db = \App\Core\Database::getInstance(); + $db->query("INSERT INTO api_cors (origin, methods, headers, max_age) VALUES (?, ?, ?, ?)", [ + $origin, $methods, $headers, $maxAge + ]); + \App\Helpers\FlashHelper::set('success', 'toasts.cors_rule_added', 'toasts.cors_rule_added_desc', ['origin' => $origin], true); + } + + header('Location: /settings/api-cors'); + } + + public function updateApiCors() { + $id = $_POST['id'] ?? null; + $origin = $_POST['origin'] ?? ''; + $methods = isset($_POST['methods']) ? json_encode($_POST['methods']) : '["GET","POST"]'; + $headers = isset($_POST['headers']) ? json_encode(array_map('trim', explode(',', $_POST['headers']))) : '["*"]'; + $maxAge = (int)($_POST['max_age'] ?? 3600); + + if ($id && !empty($origin)) { + $db = \App\Core\Database::getInstance(); + $db->query("UPDATE api_cors SET origin = ?, methods = ?, headers = ?, max_age = ? WHERE id = ?", [ + $origin, $methods, $headers, $maxAge, $id + ]); + \App\Helpers\FlashHelper::set('success', 'toasts.cors_rule_updated', 'toasts.cors_rule_updated_desc', ['origin' => $origin], true); + } + + header('Location: /settings/api-cors'); + } + + public function deleteApiCors() { + $id = $_POST['id'] ?? null; + if ($id) { + $db = \App\Core\Database::getInstance(); + $db->query("DELETE FROM api_cors WHERE id = ?", [$id]); + \App\Helpers\FlashHelper::set('success', 'toasts.cors_rule_deleted', 'toasts.cors_rule_deleted_desc', [], true); + } + header('Location: /settings/api-cors'); + } +} diff --git a/app/Controllers/SystemController.php b/app/Controllers/SystemController.php new file mode 100644 index 0000000..eea2eb0 --- /dev/null +++ b/app/Controllers/SystemController.php @@ -0,0 +1,47 @@ +executeCommand($session, '/system/reboot'); + } + + // Shutdown Router + public function shutdown($session) + { + $this->executeCommand($session, '/system/shutdown'); + } + + private function executeCommand($session, $command) + { + $configModel = new Config(); + $config = $configModel->getSession($session); + if (!$config) { + header('Content-Type: application/json'); + echo json_encode(['error' => 'Session not found']); + return; + } + + $API = new RouterOSAPI(); + if ($API->connect($config['ip_address'], $config['username'], $config['password'])) { + $API->write($command); + // Wait for command to be processed before cutting connection + sleep(2); + $API->disconnect(); + + header('Content-Type: application/json'); + echo json_encode(['success' => true]); + } else { + header('Content-Type: application/json'); + echo json_encode(['error' => 'Connection failed']); + } + } +} diff --git a/app/Controllers/TemplateController.php b/app/Controllers/TemplateController.php new file mode 100644 index 0000000..a27849e --- /dev/null +++ b/app/Controllers/TemplateController.php @@ -0,0 +1,132 @@ +getAll(); + + $data = [ + 'templates' => $templates + ]; + return $this->view('settings/templates/index', $data); + } + + public function preview($id) { + $content = ''; + if ($id === 'default') { + $content = \App\Helpers\TemplateHelper::getDefaultTemplate(); + } else { + $templateModel = new VoucherTemplateModel(); + $tpl = $templateModel->getById($id); + if ($tpl) { + $content = $tpl['content']; + } + } + + echo \App\Helpers\TemplateHelper::getPreviewPage($content); + } + + public function add() { + $logoModel = new \App\Models\Logo(); + $logos = $logoModel->getAll(); + $logoMap = []; + foreach ($logos as $l) { + $logoMap[$l['id']] = $l['path']; + } + + $data = [ + 'logoMap' => $logoMap + ]; + return $this->view('settings/templates/add', $data); // Note: add.php likely includes edit.php or is alias. View above says 'Template Editor (Shared)' + } + + public function store() { + if ($_SERVER['REQUEST_METHOD'] !== 'POST') return; + + $name = $_POST['name'] ?? 'Untitled'; + $content = $_POST['content'] ?? ''; + + // Session context could be 'global' or specific. For now, let's treat settings templates as global or assign to 'global' session name if column exists. + // My migration made 'session_name' NOT NULL. + // I will use 'global' for templates created in Settings. + + $data = [ + 'session_name' => 'global', + 'name' => $name, + 'content' => $content + ]; + + $templateModel = new VoucherTemplateModel(); + $templateModel->add($data); + + \App\Helpers\FlashHelper::set('success', 'toasts.template_created', 'toasts.template_created_desc', ['name' => $name], true); + header("Location: /settings/templates"); + exit; + } + + public function edit($id) { + $templateModel = new VoucherTemplateModel(); + $template = $templateModel->getById($id); + + if (!$template) { + header("Location: /settings/templates"); + exit; + } + + $logoModel = new \App\Models\Logo(); + $logos = $logoModel->getAll(); + $logoMap = []; + foreach ($logos as $l) { + $logoMap[$l['id']] = $l['path']; + } + + $data = [ + 'template' => $template, + 'logoMap' => $logoMap + ]; + return $this->view('settings/templates/edit', $data); + } + + public function update() { + if ($_SERVER['REQUEST_METHOD'] !== 'POST') return; + + $id = $_POST['id'] ?? ''; + $name = $_POST['name'] ?? ''; + $content = $_POST['content'] ?? ''; + + $data = [ + 'name' => $name, + 'content' => $content + ]; + + $templateModel = new VoucherTemplateModel(); + $templateModel->update($id, $data); + + \App\Helpers\FlashHelper::set('success', 'toasts.template_updated', 'toasts.template_updated_desc', ['name' => $name], true); + header("Location: /settings/templates"); + exit; + } + + public function delete() { + if ($_SERVER['REQUEST_METHOD'] !== 'POST') return; + $id = $_POST['id'] ?? ''; + + $templateModel = new VoucherTemplateModel(); + $templateModel->delete($id); + + \App\Helpers\FlashHelper::set('success', 'toasts.template_deleted', 'toasts.template_deleted_desc', [], true); + header("Location: /settings/templates"); + exit; + } +} diff --git a/app/Controllers/TrafficController.php b/app/Controllers/TrafficController.php new file mode 100644 index 0000000..f271458 --- /dev/null +++ b/app/Controllers/TrafficController.php @@ -0,0 +1,87 @@ +getSession($session); + + if (!$config) { + http_response_code(404); + echo json_encode(['error' => 'Session not found']); + return; + } + + // 2. Connect to RouterOS + $API = new RouterOSAPI(); + // $API->debug = true; + + // Fast Fail for Traffic Monitor to prevent blocking PHP server + $API->attempts = 1; + $API->timeout = 2; + + if ($API->connect($config['ip_address'], $config['username'], $config['password'])) { + // 3. Get Interface Name from GET param > Config > default 'ether1' + $interface = $_GET['interface'] ?? $config['interface'] ?? 'ether1'; + + // 4. Fetch Traffic + // /interface/monitor-traffic interface=ether1 once + $traffic = $API->comm('/interface/monitor-traffic', [ + "interface" => $interface, + "once" => "", + ]); + + $API->disconnect(); + + // 5. Return JSON + if (!empty($traffic) && !isset($traffic['!trap'])) { + header('Content-Type: application/json'); + echo json_encode($traffic[0]); + } else { + echo json_encode(['error' => 'No data']); + } + } else { + http_response_code(500); + echo json_encode(['error' => 'Connection failed']); + } + } + + public function getInterfaces($session) + { + // 1. Get Session Config + $configModel = new Config(); + $config = $configModel->getSession($session); + + if (!$config) { + http_response_code(404); + echo json_encode(['error' => 'Session not found']); + return; + } + + // 2. Connect + $API = new RouterOSAPI(); + if ($API->connect($config['ip_address'], $config['username'], $config['password'])) { + // 3. Fetch Interfaces + // Use comm() to safely handle response parsing and filtering + $interfaces = $API->comm('/interface/print', [ + ".proplist" => "name,type" + ]); + $API->disconnect(); + + // 4. Return + header('Content-Type: application/json'); + echo json_encode($interfaces); + } else { + http_response_code(500); + echo json_encode(['error' => 'Connection failed']); + } + } +} diff --git a/app/Core/Autoloader.php b/app/Core/Autoloader.php new file mode 100644 index 0000000..a1f3ca6 --- /dev/null +++ b/app/Core/Autoloader.php @@ -0,0 +1,31 @@ + app/Core/Router.php + // We assume ROOT is defined externally + if (!defined('ROOT')) { + return; + } + + $prefix = 'App\\'; + $base_dir = ROOT . '/app/'; + + $len = strlen($prefix); + if (strncmp($prefix, $class, $len) !== 0) { + return; + } + + $relative_class = substr($class, $len); + $file = $base_dir . str_replace('\\', '/', $relative_class) . '.php'; + + if (file_exists($file)) { + require_once $file; + } + }); + } +} diff --git a/app/Core/Console.php b/app/Core/Console.php new file mode 100644 index 0000000..a15fac6 --- /dev/null +++ b/app/Core/Console.php @@ -0,0 +1,240 @@ +printBanner(); + + switch ($command) { + case 'serve': + $this->commandServe($args); + break; + + case 'key:generate': + $this->commandKeyGenerate(); + break; + + case 'admin:reset': + $this->commandAdminReset($args); + break; + + case 'install': + $this->commandInstall($args); + break; + + case 'help': + default: + $this->commandHelp(); + break; + } + } + + private function printBanner() { + echo "\n"; + echo self::COLOR_BOLD . " MIVO Helper " . self::COLOR_RESET . self::COLOR_GRAY . "v1.0" . self::COLOR_RESET . "\n\n"; + } + + private function commandServe($args) { + $host = '0.0.0.0'; + $port = 8000; + + foreach ($args as $arg) { + if (strpos($arg, '--port=') === 0) { + $port = (int) substr($arg, 7); + } + if (strpos($arg, '--host=') === 0) { + $host = substr($arg, 7); + } + } + + echo " " . self::COLOR_GREEN . "Server running on:" . self::COLOR_RESET . "\n"; + echo " - Local: " . self::COLOR_BLUE . "http://localhost:$port" . self::COLOR_RESET . "\n"; + + $hostname = gethostname(); + $ip = gethostbyname($hostname); + if ($ip !== '127.0.0.1' && $ip !== 'localhost') { + echo " - Network: " . self::COLOR_BLUE . "http://$ip:$port" . self::COLOR_RESET . "\n"; + } + + echo "\n " . self::COLOR_GRAY . "Press Ctrl+C to stop" . self::COLOR_RESET . "\n\n"; + + $cmd = sprintf("php -S %s:%d -t public public/index.php", $host, $port); + passthru($cmd); + } + + private function commandKeyGenerate() { + echo self::COLOR_YELLOW . "Generating new application key..." . self::COLOR_RESET . "\n"; + + // Generate 32 bytes of random data for AES-256 + $key = bin2hex(random_bytes(16)); // 32 chars hex + + $envPath = ROOT . '/.env'; + $examplePath = ROOT . '/.env.example'; + + // Copy example if .env doesn't exist + if (!file_exists($envPath)) { + echo self::COLOR_BLUE . "Copying .env.example to .env..." . self::COLOR_RESET . "\n"; + if (file_exists($examplePath)) { + copy($examplePath, $envPath); + } else { + echo self::COLOR_RED . "Error: .env.example not found." . self::COLOR_RESET . "\n"; + return; + } + } + + // Read .env + $content = file_get_contents($envPath); + + // Replace or Append APP_KEY + if (strpos($content, 'APP_KEY=') !== false) { + $newContent = preg_replace( + "/APP_KEY=.*/", + "APP_KEY=$key", + $content + ); + } else { + $newContent = $content . "\nAPP_KEY=$key"; + } + + file_put_contents($envPath, $newContent); + + echo self::COLOR_GREEN . "Application key set successfully in .env." . self::COLOR_RESET . "\n"; + echo self::COLOR_GRAY . "Key: " . $key . self::COLOR_RESET . "\n"; + echo self::COLOR_YELLOW . "Please ensure .env is not committed to version control." . self::COLOR_RESET . "\n"; + } + + private function commandAdminReset($args) { + $username = 'admin'; + $password = $args[0] ?? 'admin'; + + echo self::COLOR_YELLOW . "Resetting password for user '$username'..." . self::COLOR_RESET . "\n"; + + try { + $db = \App\Core\Database::getInstance(); + $hash = password_hash($password, PASSWORD_DEFAULT); + + // Check if user exists first + $check = $db->query("SELECT id FROM users WHERE username = ?", [$username])->fetch(); + + if ($check) { + $db->query("UPDATE users SET password = ? WHERE username = ?", [$hash, $username]); + echo self::COLOR_GREEN . "Password updated successfully." . self::COLOR_RESET . "\n"; + } else { + // Determine if we should create it + echo self::COLOR_YELLOW . "User '$username' not found. Creating..." . self::COLOR_RESET . "\n"; + $db->query("INSERT INTO users (username, password, created_at) VALUES (?, ?, ?)", [ + $username, $hash, date('Y-m-d H:i:s') + ]); + echo self::COLOR_GREEN . "User created successfully." . self::COLOR_RESET . "\n"; + } + + echo "New Password: " . self::COLOR_BOLD . $password . self::COLOR_RESET . "\n"; + + } catch (\Exception $e) { + echo self::COLOR_RED . "Error: " . $e->getMessage() . self::COLOR_RESET . "\n"; + } + } + + private function commandInstall($args) { + echo self::COLOR_BLUE . "=== MIVO Installer ===" . self::COLOR_RESET . "\n"; + + // 1. Database Migration + echo "Setting up database...\n"; + try { + if (\App\Core\Migrations::up()) { + echo self::COLOR_GREEN . "Database schema created successfully." . self::COLOR_RESET . "\n"; + } + } catch (\Exception $e) { + echo self::COLOR_RED . "Migration Error: " . $e->getMessage() . self::COLOR_RESET . "\n"; + return; + } + + // 2. Encryption Key + echo "Generating encryption key...\n"; + + $envPath = ROOT . '/.env'; + $keyExists = false; + + if (file_exists($envPath)) { + $envIds = parse_ini_file($envPath); + if (!empty($envIds['APP_KEY']) && $envIds['APP_KEY'] !== 'mikhmonv3remake_secret_key_32bytes') { + $keyExists = true; + } + } + + if (!$keyExists) { + $this->commandKeyGenerate(); + } else { + echo self::COLOR_YELLOW . "Secret key already set in .env. Skipping." . self::COLOR_RESET . "\n"; + } + + // 3. Admin Account + echo "Create Admin Account? [Y/n] "; + $handle = fopen("php://stdin", "r"); + $line = trim(fgets($handle)); + + if (strtolower($line) != 'n') { + echo "Username [admin]: "; + $user = trim(fgets($handle)); + if (empty($user)) $user = 'admin'; + + echo "Password [admin]: "; + $pass = trim(fgets($handle)); + if (empty($pass)) $pass = 'admin'; + + // Re-use admin reset logic slightly modified or called directly + $this->commandAdminReset([$pass]); // Simplification: admin:reset implementation uses hardcoded user='admin' currently, need to update it to support custom username if we want full flexibility. + // Wait, my commandAdminReset implementation uses hardcoded 'admin'. + // I should update commandAdminReset to accept username as argument or just replicate logic here. + // Replicating logic for clarity here. + + /* Actually, commandAdminReset as currently implemented takes password as arg[0] and uses 'admin' as username. + User requested robust install. I will just run the logic manually here to respect the inputted username. */ + + try { + $db = \App\Core\Database::getInstance(); + $hash = password_hash($pass, PASSWORD_DEFAULT); + $check = $db->query("SELECT id FROM users WHERE username = ?", [$user])->fetch(); + if ($check) { + $db->query("UPDATE users SET password = ? WHERE username = ?", [$hash, $user]); + echo self::COLOR_GREEN . "User '$user' updated." . self::COLOR_RESET . "\n"; + } else { + $db->query("INSERT INTO users (username, password, created_at) VALUES (?, ?, ?)", [$user, $hash, date('Y-m-d H:i:s')]); + echo self::COLOR_GREEN . "User '$user' created." . self::COLOR_RESET . "\n"; + } + } catch (\Exception $e) { + echo self::COLOR_RED . "Error creating user: " . $e->getMessage() . self::COLOR_RESET . "\n"; + } + } + + echo "\n" . self::COLOR_GREEN . "Installation Completed Successfully!" . self::COLOR_RESET . "\n"; + echo "You can now run: " . self::COLOR_YELLOW . "php mivo serve" . self::COLOR_RESET . "\n"; + } + + private function commandHelp() { + echo self::COLOR_YELLOW . "Usage:" . self::COLOR_RESET . "\n"; + echo " php mivo [command] [options]\n\n"; + + echo self::COLOR_YELLOW . "Available commands:" . self::COLOR_RESET . "\n"; + echo " " . self::COLOR_GREEN . "install " . self::COLOR_RESET . " Install MIVO (Setup DB & Admin)\n"; + echo " " . self::COLOR_GREEN . "serve " . self::COLOR_RESET . " Start the development server\n"; + echo " " . self::COLOR_GREEN . "key:generate " . self::COLOR_RESET . " Set the application key\n"; + echo " " . self::COLOR_GREEN . "admin:reset " . self::COLOR_RESET . " Reset admin password (default: admin)\n"; + echo " " . self::COLOR_GREEN . "help " . self::COLOR_RESET . " Show this help message\n"; + echo "\n"; + } +} diff --git a/app/Core/Controller.php b/app/Core/Controller.php new file mode 100644 index 0000000..250464e --- /dev/null +++ b/app/Core/Controller.php @@ -0,0 +1,20 @@ +pdo = new PDO("sqlite:" . $dbPath); + $this->pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + $this->pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC); + } catch (PDOException $e) { + die("Database Connection Failed: " . $e->getMessage()); + } + } + + public static function getInstance() { + if (self::$instance === null) { + self::$instance = new self(); + } + return self::$instance; + } + + public function getConnection() { + return $this->pdo; + } + + // Helper to run query with params + public function query($sql, $params = []) { + $stmt = $this->pdo->prepare($sql); + $stmt->execute($params); + return $stmt; + } +} diff --git a/app/Core/Env.php b/app/Core/Env.php new file mode 100644 index 0000000..08a12be --- /dev/null +++ b/app/Core/Env.php @@ -0,0 +1,48 @@ +query("SELECT * FROM api_cors WHERE origin = ? OR origin = '*' LIMIT 1", [$origin]); + $rule = $stmt->fetch(); + + if ($rule) { + header("Access-Control-Allow-Origin: " . ($rule['origin'] === '*' ? '*' : $origin)); + + $methods = json_decode($rule['methods'], true) ?: ['GET', 'POST']; + header("Access-Control-Allow-Methods: " . implode(', ', $methods)); + + $headers = json_decode($rule['headers'], true) ?: ['*']; + header("Access-Control-Allow-Headers: " . implode(', ', $headers)); + + header("Access-Control-Max-Age: " . ($rule['max_age'] ?? 3600)); + + // Handle preflight requests + if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { + http_response_code(200); + exit(); + } + } + } +} diff --git a/app/Core/Migrations.php b/app/Core/Migrations.php new file mode 100644 index 0000000..a2af0e6 --- /dev/null +++ b/app/Core/Migrations.php @@ -0,0 +1,101 @@ +getConnection(); + + // 1. Users Table (Admin Credentials) + $pdo->exec("CREATE TABLE IF NOT EXISTS users ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + username TEXT NOT NULL UNIQUE, + password TEXT NOT NULL, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP + )"); + + // 2. Routers (Sessions) Table + $pdo->exec("CREATE TABLE IF NOT EXISTS routers ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + session_name TEXT NOT NULL UNIQUE, + ip_address TEXT, + username TEXT, + password TEXT, + hotspot_name TEXT, + dns_name TEXT, + currency TEXT DEFAULT 'RP', + reload_interval INTEGER DEFAULT 60, + interface TEXT, + description TEXT, + quick_access INTEGER DEFAULT 0 + )"); + + // 3. Quick Access (Dashboard Shortcuts) + $pdo->exec("CREATE TABLE IF NOT EXISTS quick_access ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + label TEXT NOT NULL, + url TEXT NOT NULL, + icon TEXT, + category TEXT DEFAULT 'general', + active INTEGER DEFAULT 1 + )"); + + // 4. Settings (Key-Value Store) + $pdo->exec("CREATE TABLE IF NOT EXISTS settings ( + key TEXT PRIMARY KEY, + value TEXT NOT NULL + )"); + + // 5. Logos (Branding) + $pdo->exec("CREATE TABLE IF NOT EXISTS logos ( + id TEXT PRIMARY KEY, + name TEXT NOT NULL, + path TEXT NOT NULL, + type TEXT, + size INTEGER, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP + )"); + + // 6. Quick Prints (Voucher Printing Profiles) + $pdo->exec("CREATE TABLE IF NOT EXISTS quick_prints ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + session_name TEXT NOT NULL, + name TEXT NOT NULL, + server TEXT NOT NULL, + profile TEXT NOT NULL, + prefix TEXT DEFAULT '', + char_length INTEGER DEFAULT 4, + price INTEGER DEFAULT 0, + time_limit TEXT DEFAULT '', + data_limit TEXT DEFAULT '', + comment TEXT DEFAULT '', + color TEXT DEFAULT 'bg-blue-500', + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP + )"); + + // 7. Voucher Templates + $pdo->exec("CREATE TABLE IF NOT EXISTS voucher_templates ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + session_name TEXT NOT NULL, + name TEXT NOT NULL, + content TEXT NOT NULL, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP + )"); + + // 8. API CORS Rules + $pdo->exec("CREATE TABLE IF NOT EXISTS api_cors ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + origin TEXT NOT NULL, + methods TEXT DEFAULT '[\"GET\",\"POST\"]', + headers TEXT DEFAULT '[\"*\"]', + max_age INTEGER DEFAULT 3600, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP + )"); + + return true; + } +} diff --git a/app/Core/Router.php b/app/Core/Router.php new file mode 100644 index 0000000..af6e1b0 --- /dev/null +++ b/app/Core/Router.php @@ -0,0 +1,67 @@ +routes['GET'][$path] = $callback; + } + + public function post($path, $callback) { + $this->routes['POST'][$path] = $callback; + } + + public function dispatch($uri, $method) { + $path = parse_url($uri, PHP_URL_PATH); + + // Handle subdirectory + $scriptName = dirname($_SERVER['SCRIPT_NAME']); + if (strpos($path, $scriptName) === 0) { + $path = substr($path, strlen($scriptName)); + } + $path = '/' . trim($path, '/'); + + // Global Install Check: Redirect if database is missing + $dbPath = ROOT . '/app/Database/database.sqlite'; + if (!file_exists($dbPath)) { + // Whitelist /install route and assets to prevent infinite loop + if ($path !== '/install' && strpos($path, '/assets/') !== 0) { + header('Location: /install'); + exit; + } + } + + // Check exact match first + if (isset($this->routes[$method][$path])) { + $callback = $this->routes[$method][$path]; + return $this->invokeCallback($callback); + } + + // Check dynamic routes + foreach ($this->routes[$method] as $route => $callback) { + // Convert route syntax to regex + // e.g. /dashboard/{session} -> #^/dashboard/([^/]+)$# + $pattern = preg_replace('/\{([a-zA-Z0-9_]+)\}/', '([^/]+)', $route); + $pattern = "#^" . $pattern . "$#"; + + if (preg_match($pattern, $path, $matches)) { + array_shift($matches); // Remove full match + $matches = array_map('urldecode', $matches); + return $this->invokeCallback($callback, $matches); + } + } + + \App\Helpers\ErrorHelper::show(404); + } + + protected function invokeCallback($callback, $params = []) { + if (is_array($callback)) { + $controller = new $callback[0](); + $method = $callback[1]; + return call_user_func_array([$controller, $method], $params); + } + return call_user_func_array($callback, $params); + } +} diff --git a/app/Helpers/EncryptionHelper.php b/app/Helpers/EncryptionHelper.php new file mode 100644 index 0000000..6a483f4 --- /dev/null +++ b/app/Helpers/EncryptionHelper.php @@ -0,0 +1,54 @@ + $type, + 'title' => $title, + 'message' => $message, + 'params' => $params, + 'isTranslated' => $isTranslated + ]; + } + + /** + * Check if a flash message exists. + * + * @return boolean + */ + public static function has() { + if (session_status() === PHP_SESSION_NONE) { + session_start(); + } + return isset($_SESSION[self::SESSION_KEY]); + } + + /** + * Get the flash message and clear it from session. + * + * @return array|null + */ + public static function get() { + if (session_status() === PHP_SESSION_NONE) { + session_start(); + } + + if (self::has()) { + $notification = $_SESSION[self::SESSION_KEY]; + unset($_SESSION[self::SESSION_KEY]); + return $notification; + } + + return null; + } +} diff --git a/app/Helpers/FormatHelper.php b/app/Helpers/FormatHelper.php new file mode 100644 index 0000000..6049dae --- /dev/null +++ b/app/Helpers/FormatHelper.php @@ -0,0 +1,194 @@ + "3 Weeks 1 Day 8 Hours 56 Minutes 19 Seconds" + * + * @param string $string + * @return string + */ + public static function elapsedTime($string) + { + if (empty($string)) return '-'; + + // Mikrotik formats: + // 1. "3w1d8h56m19s" (Full) + // 2. "00:05:00" (Simple H:i:s) + // 3. "1d 05:00:00" (Hybrid) + // 4. "sep/02/2023 10:00:00" (Absolute date, rarely used for uptime but useful to catch) + + // Maps Mikrotik abbreviations to Human terms (Plural handled in logic) + $maps = [ + 'w' => 'Week', + 'd' => 'Day', + 'h' => 'Hour', + 'm' => 'Minute', + 's' => 'Second' + ]; + + // Result container + $parts = []; + + // Check for simple colon format (H:i:s) + if (strpos($string, ':') !== false && strpos($string, 'w') === false && strpos($string, 'd') === false) { + return $string; // Return as is or parse H:i:s if needed + } + + // Parse regex for w, d, h, m, s + //preg_match_all('/(\d+)([wdhms])/', $string, $matches, PREG_SET_ORDER); + + // Manual parsing to handle mixed cases more robustly or just regex + foreach ($maps as $key => $label) { + if (preg_match('/(\d+)'.$key.'/', $string, $match)) { + $value = intval($match[1]); + if ($value > 0) { + $parts[] = $value . ' ' . $label . ($value > 1 ? 's' : ''); + } + } + } + + // If no matches found, straightforward return (maybe it's raw seconds or weird format) + if (empty($parts)) { + if ($string === '0s' || $string === '00:00:00') return '-'; + return $string; + } + + return implode(' ', $parts); + } + + /** + * Capitalize each word (Title Case) + * @param string $string + * @return string + */ + public static function capitalize($string) + { + return ucwords(strtolower($string)); + } + + /** + * Format Currency + * @param int|float $number + * @param string $prefix + * @return string + */ + public static function formatCurrency($number, $prefix = '') + { + return $prefix . ' ' . number_format($number, 0, ',', '.'); + } + + /** + * Format Bytes to KB, MB, GB + * @param int $bytes + * @param int $precision + * @return string + */ + public static function formatBytes($bytes, $precision = 2) + { + if ($bytes <= 0) return '-'; + + $base = log($bytes, 1024); + $suffixes = array('B', 'KB', 'MB', 'GB', 'TB'); + + return round(pow(1024, $base - floor($base)), $precision) . ' ' . $suffixes[floor($base)]; + } + + /** + * Format Date + * @param string $dateStr + * @param string $format + * @return string + */ + public static function formatDate($dateStr, $format = 'd M Y H:i') + { + if(empty($dateStr)) return '-'; + // Handle Mikrotik default date formats if needed, usually they are readable + // e.g. "jan/02/1970 00:00:00" + $time = strtotime($dateStr); + if(!$time) return $dateStr; + return date($format, $time); + } + /** + * Convert Seconds to Human Readable format + * @param int $seconds + * @return string + */ + public static function formatSeconds($seconds) { + if ($seconds <= 0) return '0s'; + + $w = floor($seconds / 604800); + $d = floor(($seconds % 604800) / 86400); + $h = floor(($seconds % 86400) / 3600); + $m = floor(($seconds % 3600) / 60); + $s = $seconds % 60; + + $parts = []; + if ($w > 0) $parts[] = $w . 'w'; + if ($d > 0) $parts[] = $d . 'd'; + if ($h > 0) $parts[] = $h . 'h'; + if ($m > 0) $parts[] = $m . 'm'; + if ($s > 0 || empty($parts)) $parts[] = $s . 's'; + + return implode('', $parts); + } + + /** + * Parse MikroTik duration string to Seconds (int) + + * Supports: 1d2h3m, 00:00:00, 1d 00:00:00 + */ + public static function parseDuration($string) { + if (empty($string)) return 0; + + $string = trim($string); + $totalSeconds = 0; + + // 1. Handle "00:00:00" or "1d 00:00:00" (Colons) + if (strpos($string, ':') !== false) { + $parts = explode(' ', $string); + $timePart = end($parts); // 00:00:00 + + // Calc time part + $t = explode(':', $timePart); + if (count($t) === 3) { + $totalSeconds += ($t[0] * 3600) + ($t[1] * 60) + $t[2]; + } elseif (count($t) === 2) { // 00:00 (mm:ss or hh:mm? usually hh:mm in routeros logs, but 00:00:59 is uptime) + // Assumption: if 2 parts, treat as MM:SS if small, or HH:MM? + // RouterOS uptime is usually HH:MM:SS. Let's assume standard time ref. + // Actually RouterOS uptime often drops hours if 0. + // SAFE BET: Just Parse standard 3 parts. + $totalSeconds += ($t[0] * 60) + $t[1]; + } + + // Calc Day part "1d" + if (count($parts) > 1) { + $dayPart = $parts[0]; // 1d + $totalSeconds += intval($dayPart) * 86400; + } + return $totalSeconds; + } + + // 2. Handle "1w2d3h4m5s" (Letters) + if (preg_match_all('/(\d+)([wdhms])/', $string, $matches, PREG_SET_ORDER)) { + foreach ($matches as $m) { + $val = intval($m[1]); + $unit = $m[2]; + switch ($unit) { + case 'w': $totalSeconds += $val * 604800; break; + case 'd': $totalSeconds += $val * 86400; break; + case 'h': $totalSeconds += $val * 3600; break; + case 'm': $totalSeconds += $val * 60; break; + case 's': $totalSeconds += $val; break; + } + } + return $totalSeconds; + } + + // 3. Raw number? + return intval($string); + } +} diff --git a/app/Helpers/HotspotHelper.php b/app/Helpers/HotspotHelper.php new file mode 100644 index 0000000..fec276e --- /dev/null +++ b/app/Helpers/HotspotHelper.php @@ -0,0 +1,106 @@ + $data[1] ?? '', + 'price' => $clean($data[2] ?? ''), + 'validity' => self::formatValidity($clean($data[3] ?? '')), + 'selling_price' => $clean($data[4] ?? ''), + 'lock_user' => $data[6] ?? '', + ]; + } + return []; + } + + /** + * Format validity string (e.g., 3d2h5m -> 3d 2h 5m) + */ + public static function formatValidity($val) { + if (empty($val)) return ''; + // Insert space after letters + $val = preg_replace('/([a-z]+)/i', '$1 ', $val); + return trim($val); + } + + /** + * Format expired mode code to readable text + */ + public static function formatExpiredMode($mode) { + switch ($mode) { + case 'rem': return 'Remove'; + case 'ntf': return 'Notice'; + case 'remc': return 'Remove & Record'; + case 'ntfc': return 'Notice & Record'; + default: return $mode; + } + } + + /** + * Format bytes to human readable string (KB, MB, GB) + */ + public static function formatBytes($bytes, $precision = 2) { + if (empty($bytes) || $bytes === '0') return '0 B'; + + $units = ['B', 'KB', 'MB', 'GB', 'TB']; + + $bytes = max($bytes, 0); + $pow = floor(($bytes ? log($bytes) : 0) / log(1024)); + $pow = min($pow, count($units) - 1); + + $bytes /= pow(1024, $pow); + + return round($bytes, $precision) . ' ' . $units[$pow]; + } + + /** + * Get User Status Code + * Returns: active, limited, locked, expired + */ + public static function getUserStatus($user) { + // 1. Check for specific comment keywords (Highest Priority - usually set by scripts) + $comment = strtolower($user['comment'] ?? ''); + + // "exp" explicitly means expired by script + if (strpos($comment, 'exp') !== false) { + return 'expired'; + } + + // 2. Check Data Limit (Quota) + $limitBytes = isset($user['limit-bytes-total']) ? (int)$user['limit-bytes-total'] : 0; + if ($limitBytes > 0) { + $bytesIn = isset($user['bytes-in']) ? (int)$user['bytes-in'] : 0; + $bytesOut = isset($user['bytes-out']) ? (int)$user['bytes-out'] : 0; + if (($bytesIn + $bytesOut) >= $limitBytes) { + return 'limited'; + } + } + + // 3. Check Disabled state + if (($user['disabled'] ?? 'false') === 'true') { + return 'locked'; + } + + // 4. Default + return 'active'; + } +} diff --git a/app/Helpers/LanguageHelper.php b/app/Helpers/LanguageHelper.php new file mode 100644 index 0000000..cc2fec0 --- /dev/null +++ b/app/Helpers/LanguageHelper.php @@ -0,0 +1,45 @@ + $code, + 'name' => $name, + 'flag' => $flag + ]; + } + } + + return $languages; + } +} diff --git a/app/Helpers/TemplateHelper.php b/app/Helpers/TemplateHelper.php new file mode 100644 index 0000000..2ee1ec8 --- /dev/null +++ b/app/Helpers/TemplateHelper.php @@ -0,0 +1,102 @@ + + .voucher { width: 250px; background: #fff; padding: 10px; border: 1px solid #ccc; font-family: "Courier New", Courier, monospace; color: #000; } + .header { text-align: center; font-weight: bold; margin-bottom: 5px; font-size: 14px; } + .row { display: flex; justify-content: space-between; margin-bottom: 2px; font-size: 12px; } + .code { font-size: 16px; font-weight: bold; text-align: center; margin: 10px 0; border: 1px dashed #000; padding: 5px; } + .qr { text-align: center; margin-top: 5px; } + +
+
{{server_name}}
+
Profile: {{profile}}
+
Valid: {{validity}}
+
Price: {{price}}
+
+ User: {{username}}
+ Pass: {{password}} +
+
{{qrcode}}
+
+ Login: http://{{dns_name}}/login +
+
'; + } + + public static function getMockContent($content) { + if (empty($content)) return ''; + + // Dummy Data + $dummyData = [ + '{{server_name}}' => 'Hotspot', + '{{dns_name}}' => 'hotspot.lan', + '{{username}}' => 'u-5829', + '{{password}}' => '5912', + '{{price}}' => '5.000', + '{{validity}}' => '12 Hours', + '{{profile}}' => 'Small-Packet', + '{{time_limit}}' => '12h', + '{{data_limit}}' => '1 GB', + '{{ip_address}}' => '192.168.88.254', + '{{mac_address}}' => 'AA:BB:CC:DD:EE:FF', + '{{comment}}' => 'Thank You', + '{{copyright}}' => 'Mikhmon', + ]; + + $content = str_replace(array_keys($dummyData), array_values($dummyData), $content); + + // QR Code replacement + $content = preg_replace('/\{\{\s*qrcode.*?\}\}/i', '', $content); + + return $content; + } + + public static function getPreviewPage($content) { + $mockContent = self::getMockContent($content); + + return ' + + + + + + +
' . $mockContent . '
+ + + '; + } +} diff --git a/app/Helpers/ViewHelper.php b/app/Helpers/ViewHelper.php new file mode 100644 index 0000000..e13c125 --- /dev/null +++ b/app/Helpers/ViewHelper.php @@ -0,0 +1,32 @@ + ['class' => 'bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-400', 'icon' => 'check-circle'], + 'limited' => ['class' => 'bg-orange-100 text-orange-800 dark:bg-orange-900/30 dark:text-orange-400', 'icon' => 'pie-chart'], + 'locked' => ['class' => 'bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-400', 'icon' => 'lock'], + 'expired' => ['class' => 'bg-accents-2 text-accents-6', 'icon' => 'clock'], + 'default' => ['class' => 'bg-blue-100 text-blue-800', 'icon' => 'info'] + ]; + + $style = $styles[$status] ?? $styles['default']; + $text = $label ?? ucfirst($status === 'limited' ? 'Quota' : $status); + + return sprintf( + ' %s', + $style['class'], + $style['icon'], + $text + ); + } +} diff --git a/app/Libraries/RouterOSAPI.php b/app/Libraries/RouterOSAPI.php new file mode 100644 index 0000000..14d037c --- /dev/null +++ b/app/Libraries/RouterOSAPI.php @@ -0,0 +1,324 @@ +debug) { + echo $text . "\n"; + } + } + + public function encodeLength($length) + { + if ($length < 0x80) { + $length = chr($length); + } elseif ($length < 0x4000) { + $length |= 0x8000; + $length = chr(($length >> 8) & 0xFF) . chr($length & 0xFF); + } elseif ($length < 0x200000) { + $length |= 0xC00000; + $length = chr(($length >> 16) & 0xFF) . chr(($length >> 8) & 0xFF) . chr($length & 0xFF); + } elseif ($length < 0x10000000) { + $length |= 0xE0000000; + $length = chr(($length >> 24) & 0xFF) . chr(($length >> 16) & 0xFF) . chr(($length >> 8) & 0xFF) . chr($length & 0xFF); + } elseif ($length >= 0x10000000) { + $length = chr(0xF0) . chr(($length >> 24) & 0xFF) . chr(($length >> 16) & 0xFF) . chr(($length >> 8) & 0xFF) . chr($length & 0xFF); + } + + return $length; + } + + public function connect($ip, $login, $password) + { + for ($ATTEMPT = 1; $ATTEMPT <= $this->attempts; $ATTEMPT++) { + $this->connected = false; + $PROTOCOL = ($this->ssl ? 'ssl://' : '' ); + $context = stream_context_create(array('ssl' => array('ciphers' => 'ADH:ALL', 'verify_peer' => false, 'verify_peer_name' => false))); + $this->debug('Connection attempt #' . $ATTEMPT . ' to ' . $PROTOCOL . $ip . ':' . $this->port . '...'); + $this->socket = @stream_socket_client($PROTOCOL . $ip.':'. $this->port, $this->error_no, $this->error_str, $this->timeout, STREAM_CLIENT_CONNECT,$context); + if ($this->socket) { + socket_set_timeout($this->socket, $this->timeout); + $this->write('/login', false); + $this->write('=name=' . $login, false); + $this->write('=password=' . $password); + $RESPONSE = $this->read(false); + if (isset($RESPONSE[0])) { + if ($RESPONSE[0] == '!done') { + if (!isset($RESPONSE[1])) { + // Login method post-v6.43 + $this->connected = true; + break; + } else { + // Login method pre-v6.43 + $MATCHES = array(); + if (preg_match_all('/[^=]+/i', $RESPONSE[1], $MATCHES)) { + if ($MATCHES[0][0] == 'ret' && strlen($MATCHES[0][1]) == 32) { + $this->write('/login', false); + $this->write('=name=' . $login, false); + $this->write('=response=00' . md5(chr(0) . $password . pack('H*', $MATCHES[0][1]))); + $RESPONSE = $this->read(false); + if (isset($RESPONSE[0]) && $RESPONSE[0] == '!done') { + $this->connected = true; + break; + } + } + } + } + } + } + fclose($this->socket); + } + sleep($this->delay); + } + + if ($this->connected) { + $this->debug('Connected...'); + } else { + $this->debug('Error...'); + } + return $this->connected; + } + + public function disconnect() + { + if( is_resource($this->socket) ) { + fclose($this->socket); + } + $this->connected = false; + $this->debug('Disconnected...'); + } + + public function parseResponse($response) + { + if (is_array($response)) { + $PARSED = array(); + $CURRENT = null; + $singlevalue = null; + foreach ($response as $x) { + if (in_array($x, array('!fatal','!re','!trap'))) { + if ($x == '!re') { + $CURRENT =& $PARSED[]; + } else { + $CURRENT =& $PARSED[$x][]; + } + } elseif ($x != '!done') { + $MATCHES = array(); + if (preg_match_all('/[^=]+/i', $x, $MATCHES)) { + if ($MATCHES[0][0] == 'ret') { + $singlevalue = $MATCHES[0][1]; + } + $CURRENT[$MATCHES[0][0]] = (isset($MATCHES[0][1]) ? $MATCHES[0][1] : ''); + } + } + } + + if (empty($PARSED) && !is_null($singlevalue)) { + $PARSED = $singlevalue; + } + + return $PARSED; + } else { + return array(); + } + } + + public function arrayChangeKeyName(&$array) + { + if (is_array($array)) { + foreach ($array as $k => $v) { + $tmp = str_replace("-", "_", $k); + $tmp = str_replace("/", "_", $tmp); + if ($tmp) { + $array_new[$tmp] = $v; + } else { + $array_new[$k] = $v; + } + } + return $array_new; + } else { + return $array; + } + } + + public function read($parse = true) + { + $RESPONSE = array(); + $receiveddone = false; + while (true) { + $BYTE = ord(fread($this->socket, 1)); + $LENGTH = 0; + if ($BYTE & 128) { + if (($BYTE & 192) == 128) { + $LENGTH = (($BYTE & 63) << 8) + ord(fread($this->socket, 1)); + } else { + if (($BYTE & 224) == 192) { + $LENGTH = (($BYTE & 31) << 8) + ord(fread($this->socket, 1)); + $LENGTH = ($LENGTH << 8) + ord(fread($this->socket, 1)); + } else { + if (($BYTE & 240) == 224) { + $LENGTH = (($BYTE & 15) << 8) + ord(fread($this->socket, 1)); + $LENGTH = ($LENGTH << 8) + ord(fread($this->socket, 1)); + $LENGTH = ($LENGTH << 8) + ord(fread($this->socket, 1)); + } else { + $LENGTH = ord(fread($this->socket, 1)); + $LENGTH = ($LENGTH << 8) + ord(fread($this->socket, 1)); + $LENGTH = ($LENGTH << 8) + ord(fread($this->socket, 1)); + $LENGTH = ($LENGTH << 8) + ord(fread($this->socket, 1)); + } + } + } + } else { + $LENGTH = $BYTE; + } + + $_ = ""; + + if ($LENGTH > 0) { + $_ = ""; + $retlen = 0; + while ($retlen < $LENGTH) { + $toread = $LENGTH - $retlen; + $_ .= fread($this->socket, $toread); + $retlen = strlen($_); + } + $RESPONSE[] = $_; + $this->debug('>>> [' . $retlen . '/' . $LENGTH . '] bytes read.'); + } + + if ($_ == "!done") { + $receiveddone = true; + } + + $STATUS = socket_get_status($this->socket); + if ($LENGTH > 0) { + $this->debug('>>> [' . $LENGTH . ', ' . $STATUS['unread_bytes'] . ']' . $_); + } + + if ((!$this->connected && !$STATUS['unread_bytes']) || ($this->connected && !$STATUS['unread_bytes'] && $receiveddone)) { + break; + } + } + + if ($parse) { + $RESPONSE = $this->parseResponse($RESPONSE); + } + + return $RESPONSE; + } + + public function write($command, $param2 = true) + { + if ($command) { + $data = explode("\n", $command); + foreach ($data as $com) { + $com = trim($com); + fwrite($this->socket, $this->encodeLength(strlen($com)) . $com); + $this->debug('<<< [' . strlen($com) . '] ' . $com); + } + + if (gettype($param2) == 'integer') { + fwrite($this->socket, $this->encodeLength(strlen('.tag=' . $param2)) . '.tag=' . $param2 . chr(0)); + $this->debug('<<< [' . strlen('.tag=' . $param2) . '] .tag=' . $param2); + } elseif (gettype($param2) == 'boolean') { + fwrite($this->socket, ($param2 ? chr(0) : '')); + } + + return true; + } else { + return false; + } + } + + public function comm($com, $arr = array()) + { + $count = count($arr); + $this->write($com, !$arr); + $i = 0; + if ($this->isIterable($arr)) { + foreach ($arr as $k => $v) { + switch ($k[0]) { + case "?": + $el = "$k=$v"; + break; + case "~": + $el = "$k~$v"; + break; + default: + $el = "=$k=$v"; + break; + } + + $last = ($i++ == $count - 1); + $this->write($el, $last); + } + } + + return $this->read(); + } + + public function __destruct() + { + $this->disconnect(); + } + + // Helpers included in original file + public static function encrypt($string, $key=128) { + $result = ''; + for($i=0, $k= strlen($string); $i<$k; $i++) { + $char = substr($string, $i, 1); + $keychar = substr($key, ($i % strlen($key))-1, 1); + $char = chr(ord($char)+ord($keychar)); + $result .= $char; + } + return base64_encode($result); + } + + public static function decrypt($string, $key=128) { + $result = ''; + $string = base64_decode($string); + for($i=0, $k=strlen($string); $i< $k ; $i++) { + $char = substr($string, $i, 1); + $keychar = substr($key, ($i % strlen($key))-1, 1); + $char = chr(ord($char)-ord($keychar)); + $result .= $char; + } + return $result; + } +} diff --git a/app/Models/Config.php b/app/Models/Config.php new file mode 100644 index 0000000..4a4f3da --- /dev/null +++ b/app/Models/Config.php @@ -0,0 +1,171 @@ +configPath = ROOT . '/include/config.php'; + } + + public function getSession($sessionName) { + // 1. Check SQLite Database First + try { + $db = \App\Core\Database::getInstance(); + $stmt = $db->query("SELECT * FROM routers WHERE session_name = ?", [$sessionName]); + $router = $stmt->fetch(); + + if ($router) { + return [ + 'ip' => $router['ip_address'], + 'ip_address' => $router['ip_address'], // Alias + 'user' => $router['username'], + 'username' => $router['username'], // Alias + 'password' => \App\Helpers\EncryptionHelper::decrypt($router['password']), + 'hotspot_name' => $router['hotspot_name'], + 'dns_name' => $router['dns_name'], + 'currency' => $router['currency'], + 'reload' => $router['reload_interval'], + 'interface' => $router['interface'], + 'info' => $router['description'], + 'quick_access' => $router['quick_access'] ?? 0, + 'source' => 'sqlite' + ]; + } + } catch (\Exception $e) { + // Ignore DB error and fallback + } + + // 2. Fallback to Legacy Config + if (!file_exists($this->configPath)) { + return null; + } + + include $this->configPath; + + if (isset($data) && isset($data[$sessionName]) && is_array($data[$sessionName])) { + $s = $data[$sessionName]; + return [ + 'ip' => isset($s[1]) ? explode('!', $s[1])[1] : '', + 'ip_address' => isset($s[1]) ? explode('!', $s[1])[1] : '', // Alias + 'user' => isset($s[2]) ? explode('@|@', $s[2])[1] : '', + 'username' => isset($s[2]) ? explode('@|@', $s[2])[1] : '', // Alias + 'password' => isset($s[3]) ? explode('#|#', $s[3])[1] : '', + 'hotspot_name' => isset($s[4]) ? explode('%', $s[4])[1] : '', + 'dns_name' => isset($s[5]) ? explode('^', $s[5])[1] : '', + 'currency' => isset($s[6]) ? explode('&', $s[6])[1] : '', + 'reload' => isset($s[7]) ? explode('*', $s[7])[1] : '', + 'interface' => isset($s[8]) ? explode('(', $s[8])[1] : '', + 'info' => isset($s[9]) ? explode(')', $s[9])[1] : '', + 'source' => 'legacy' + ]; + } + + return null; + } + + public function getAllSessions() { + // SQLite + try { + $db = \App\Core\Database::getInstance(); + $stmt = $db->query("SELECT * FROM routers"); + return $stmt->fetchAll(); + } catch (\Exception $e) { + return []; + } + } + + public function getSessionById($id) { + $db = \App\Core\Database::getInstance(); + $stmt = $db->query("SELECT * FROM routers WHERE id = ?", [$id]); + $router = $stmt->fetch(); + + if ($router) { + return [ + 'id' => $router['id'], + 'session_name' => $router['session_name'], + 'ip_address' => $router['ip_address'], + 'username' => $router['username'], + 'password' => \App\Helpers\EncryptionHelper::decrypt($router['password']), + 'hotspot_name' => $router['hotspot_name'], + 'dns_name' => $router['dns_name'], + 'currency' => $router['currency'], + 'reload_interval' => $router['reload_interval'], + 'interface' => $router['interface'], + 'interface' => $router['interface'], + 'description' => $router['description'], + 'quick_access' => $router['quick_access'] ?? 0 + ]; + } + return null; + } + + public function addSession($data) { + $db = \App\Core\Database::getInstance(); + $sql = "INSERT INTO routers (session_name, ip_address, username, password, hotspot_name, dns_name, currency, reload_interval, interface, description, quick_access) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"; + + return $db->query($sql, [ + $data['session_name'] ?? 'New Session', + $data['ip_address'] ?? '', + $data['username'] ?? '', + \App\Helpers\EncryptionHelper::encrypt($data['password'] ?? ''), + $data['hotspot_name'] ?? '', + $data['dns_name'] ?? '', + $data['currency'] ?? 'RP', + $data['reload_interval'] ?? 60, + $data['interface'] ?? 'ether1', + $data['description'] ?? '', + $data['quick_access'] ?? 0 + ]); + } + + public function updateSession($id, $data) { + $db = \App\Core\Database::getInstance(); + + // If password is provided, encrypt it. If empty, don't update it (keep existing). + if (!empty($data['password'])) { + $sql = "UPDATE routers SET session_name=?, ip_address=?, username=?, password=?, hotspot_name=?, dns_name=?, currency=?, reload_interval=?, interface=?, description=?, quick_access=? WHERE id=?"; + $params = [ + $data['session_name'], + $data['ip_address'], + $data['username'], + \App\Helpers\EncryptionHelper::encrypt($data['password']), + $data['hotspot_name'], + $data['dns_name'], + $data['currency'], + $data['reload_interval'], + $data['interface'], + $data['description'], + $data['quick_access'] ?? 0, + $id + ]; + } else { + $sql = "UPDATE routers SET session_name=?, ip_address=?, username=?, hotspot_name=?, dns_name=?, currency=?, reload_interval=?, interface=?, description=?, quick_access=? WHERE id=?"; + $params = [ + $data['session_name'], + $data['ip_address'], + $data['username'], + $data['hotspot_name'], + $data['dns_name'], + $data['currency'], + $data['reload_interval'], + $data['interface'], + $data['description'], + $data['quick_access'] ?? 0, + $id + ]; + } + + return $db->query($sql, $params); + } + + public function deleteSession($id) { + $db = \App\Core\Database::getInstance(); + return $db->query("DELETE FROM routers WHERE id = ?", [$id]); + } + +} diff --git a/app/Models/Logo.php b/app/Models/Logo.php new file mode 100644 index 0000000..ba5b3d4 --- /dev/null +++ b/app/Models/Logo.php @@ -0,0 +1,145 @@ +db = \App\Core\Database::getInstance(); + $this->initTable(); + } + + // Connect method removed as we use shared instance + private function initTable() { + $query = "CREATE TABLE IF NOT EXISTS {$this->table} ( + id TEXT PRIMARY KEY, + name TEXT NOT NULL, + path TEXT NOT NULL, + type TEXT, + size INTEGER, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP + )"; + $this->db->query($query); + } + + public function generateId($length = 6) { + $characters = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'; + $charactersLength = strlen($characters); + $randomString = ''; + for ($i = 0; $i < $length; $i++) { + $randomString .= $characters[rand(0, $charactersLength - 1)]; + } + return $randomString; + } + + public function getAll() { + $stmt = $this->db->query("SELECT * FROM {$this->table} ORDER BY created_at DESC"); + return $stmt->fetchAll(PDO::FETCH_ASSOC); + } + + public function getById($id) { + $stmt = $this->db->query("SELECT * FROM {$this->table} WHERE id = :id", ['id' => $id]); + return $stmt->fetch(PDO::FETCH_ASSOC); + } + + public function add($file) { + // Security: Strict MIME Type Check + $finfo = new \finfo(FILEINFO_MIME_TYPE); + $mimeType = $finfo->file($file['tmp_name']); + + $allowedMimes = [ + 'image/jpeg' => 'jpg', + 'image/png' => 'png', + 'image/gif' => 'gif', + 'image/svg+xml' => 'svg', + 'image/webp' => 'webp' + ]; + + if (!array_key_exists($mimeType, $allowedMimes)) { + throw new Exception("Invalid file type: " . $mimeType); + } + + // Use extension mapped from MIME type or sanitize original + // Better to trust MIME mapping for extensions to avoid double extension attacks + $extension = $allowedMimes[$mimeType]; + + // Generate Unique Short ID + do { + $id = $this->generateId(); + $exists = $this->getById($id); + } while ($exists); + + $uploadDir = ROOT . '/public/assets/img/logos/'; + if (!file_exists($uploadDir)) { + mkdir($uploadDir, 0777, true); + } + + $filename = $id . '.' . $extension; + $targetPath = $uploadDir . $filename; + + if (move_uploaded_file($file['tmp_name'], $targetPath)) { + $this->db->query("INSERT INTO {$this->table} (id, name, path, type, size) VALUES (:id, :name, :path, :type, :size)", [ + 'id' => $id, + 'name' => $file['name'], + 'path' => '/assets/img/logos/' . $filename, + 'type' => $extension, + 'size' => $file['size'] + ]); + return $id; + } + + return false; + } + + public function syncFiles() { + // One-time sync: scan folder, if file not in DB, add it. + $logoDir = ROOT . '/public/assets/img/logos/'; + if (!file_exists($logoDir)) return; + + $files = glob($logoDir . '*.{jpg,jpeg,png,gif,svg}', GLOB_BRACE); + + foreach ($files as $file) { + $filename = basename($file); + $extension = pathinfo($filename, PATHINFO_EXTENSION); + + // Check if file is registered (maybe by path match) + $webPath = '/assets/img/logos/' . $filename; + $stmt = $this->db->query("SELECT COUNT(*) FROM {$this->table} WHERE path = :path", ['path' => $webPath]); + + if ($stmt->fetchColumn() == 0) { + // Not in DB, register it. + // Ideally we'd rename it to a hashID, but since it's existing, let's generate an ID and map it. + do { + $id = $this->generateId(); + $exists = $this->getById($id); + } while ($exists); + + $this->db->query("INSERT INTO {$this->table} (id, name, path, type, size) VALUES (:id, :name, :path, :type, :size)", [ + 'id' => $id, + 'name' => $filename, + 'path' => $webPath, + 'type' => $extension, + 'size' => filesize($file) + ]); + } + } + } + + public function delete($id) { + $logo = $this->getById($id); + if ($logo) { + $filePath = ROOT . '/public' . $logo['path']; + if (file_exists($filePath)) { + unlink($filePath); + } + $this->db->query("DELETE FROM {$this->table} WHERE id = :id", ['id' => $id]); + return true; + } + return false; + } +} diff --git a/app/Models/QuickPrintModel.php b/app/Models/QuickPrintModel.php new file mode 100644 index 0000000..4437c4e --- /dev/null +++ b/app/Models/QuickPrintModel.php @@ -0,0 +1,64 @@ +query("SELECT * FROM quick_prints WHERE session_name = ?", [$sessionName]); + return $stmt->fetchAll(); + } + + public function getById($id) { + $db = Database::getInstance(); + $stmt = $db->query("SELECT * FROM quick_prints WHERE id = ?", [$id]); + return $stmt->fetch(); + } + + public function add($data) { + $db = Database::getInstance(); + $sql = "INSERT INTO quick_prints (session_name, name, server, profile, prefix, char_length, price, time_limit, data_limit, comment, color) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"; + + return $db->query($sql, [ + $data['session_name'], + $data['name'], + $data['server'], + $data['profile'], + $data['prefix'] ?? '', + $data['char_length'] ?? 4, + $data['price'] ?? 0, + $data['time_limit'] ?? '', + $data['data_limit'] ?? '', + $data['comment'] ?? '', + $data['color'] ?? 'bg-blue-500' + ]); + } + + public function update($id, $data) { + $db = Database::getInstance(); + $sql = "UPDATE quick_prints SET name=?, server=?, profile=?, prefix=?, char_length=?, price=?, time_limit=?, data_limit=?, comment=?, color=?, updated_at=CURRENT_TIMESTAMP WHERE id=?"; + + return $db->query($sql, [ + $data['name'], + $data['server'], + $data['profile'], + $data['prefix'] ?? '', + $data['char_length'] ?? 4, + $data['price'] ?? 0, + $data['time_limit'] ?? '', + $data['data_limit'] ?? '', + $data['comment'] ?? '', + $data['color'] ?? 'bg-blue-500', + $id + ]); + } + + public function delete($id) { + $db = Database::getInstance(); + return $db->query("DELETE FROM quick_prints WHERE id = ?", [$id]); + } +} diff --git a/app/Models/Setting.php b/app/Models/Setting.php new file mode 100644 index 0000000..cad400f --- /dev/null +++ b/app/Models/Setting.php @@ -0,0 +1,46 @@ +db = Database::getInstance(); + $this->initTable(); + } + + private function initTable() { + $sql = "CREATE TABLE IF NOT EXISTS {$this->table} ( + key TEXT PRIMARY KEY, + value TEXT + )"; + $this->db->query($sql); + } + + public function get($key, $default = null) { + $stmt = $this->db->query("SELECT value FROM {$this->table} WHERE key = ?", [$key]); + $row = $stmt->fetch(); + return $row ? $row['value'] : $default; + } + + public function set($key, $value) { + // SQLite Upsert + $sql = "INSERT INTO {$this->table} (key, value) VALUES (:key, :value) + ON CONFLICT(key) DO UPDATE SET value = excluded.value"; + return $this->db->query($sql, ['key' => $key, 'value' => $value]); + } + + public function getAll() { + $stmt = $this->db->query("SELECT * FROM {$this->table}"); + $results = $stmt->fetchAll(); + $settings = []; + foreach ($results as $row) { + $settings[$row['key']] = $row['value']; + } + return $settings; + } +} diff --git a/app/Models/User.php b/app/Models/User.php new file mode 100644 index 0000000..f5ab8d9 --- /dev/null +++ b/app/Models/User.php @@ -0,0 +1,24 @@ +db = Database::getInstance(); + } + + public function attempt($username, $password) { + $stmt = $this->db->query("SELECT * FROM users WHERE username = ?", [$username]); + $user = $stmt->fetch(); + + if ($user && password_verify($password, $user['password'])) { + return $user; + } + + return false; + } +} diff --git a/app/Models/VoucherTemplateModel.php b/app/Models/VoucherTemplateModel.php new file mode 100644 index 0000000..6c30b9f --- /dev/null +++ b/app/Models/VoucherTemplateModel.php @@ -0,0 +1,52 @@ +query("SELECT * FROM voucher_templates"); + return $stmt->fetchAll(); + } + + public function getBySession($sessionName) { + // Templates can be global or session specific, but allow session filtering + $db = Database::getInstance(); + $stmt = $db->query("SELECT * FROM voucher_templates WHERE session_name = ? OR session_name = 'global'", [$sessionName]); + return $stmt->fetchAll(); + } + + public function getById($id) { + $db = Database::getInstance(); + $stmt = $db->query("SELECT * FROM voucher_templates WHERE id = ?", [$id]); + return $stmt->fetch(); + } + + public function add($data) { + $db = Database::getInstance(); + $sql = "INSERT INTO voucher_templates (session_name, name, content) VALUES (?, ?, ?)"; + return $db->query($sql, [ + $data['session_name'], + $data['name'], + $data['content'] + ]); + } + + public function update($id, $data) { + $db = Database::getInstance(); + $sql = "UPDATE voucher_templates SET name=?, content=?, updated_at=CURRENT_TIMESTAMP WHERE id=?"; + return $db->query($sql, [ + $data['name'], + $data['content'], + $id + ]); + } + + public function delete($id) { + $db = Database::getInstance(); + return $db->query("DELETE FROM voucher_templates WHERE id = ?", [$id]); + } +} diff --git a/app/Views/dashboard.php b/app/Views/dashboard.php new file mode 100644 index 0000000..c95c530 --- /dev/null +++ b/app/Views/dashboard.php @@ -0,0 +1,331 @@ + + +
+
+

Dashboard

+

Session:

+
+
+ +
+ +
+
+ +

System Info

+
+
+
+ Model + +
+
+ Board Name + +
+
+ RouterOS + +
+
+ Architecture + +
+
+ Uptime + +
+
+
+ + +
+
+ +

Resources

+
+ + +
+
+ CPU Load + % +
+
+
+
+
+ +
+
+ Memory + Free +
+
+ +
+
+
+ +
+
+ HDD + Free +
+
+ +
+
+
+
+ + +
+
+ +

Hotspot

+
+ +
+ +
+ +
+ +
+
+
Active
+
+ + +
+ +
+ +
+
+
Users
+
+ + +
+
+ +
+
0
+
Income Today
+
+
+
+ + +
+
+
+
+ +

Traffic Monitor

+
+
+ +
+
+
+ Rx (Download) + Tx (Upload) +
+
+
+ +
+
+
+ + + + + diff --git a/app/Views/design_system.php b/app/Views/design_system.php new file mode 100644 index 0000000..8b682a7 --- /dev/null +++ b/app/Views/design_system.php @@ -0,0 +1,396 @@ + + +
+
+
+

Design System

+

Component library and style guide for Mikhmon v3.

+
+
+ + +
+
+ + +
+

Typography

+
+
+

Heading 1 (text-4xl)

+

Used for landing page titles.

+
+
+

Heading 2 (text-3xl)

+

Used for page titles.

+
+
+

Heading 3 (text-2xl)

+

Used for section headers.

+
+
+

Heading 4 (text-xl)

+

Used for card titles.

+
+
+

Body Text (text-base)

+

The quick brown fox jumps over the lazy dog. Used for specific content.

+
+
+

Small Text (text-sm)

+

The quick brown fox jumps over the lazy dog. Used for descriptions.

+
+
+
+ + +
+

Colors (Theming)

+
+
+
Background
+
bg-background
+
+
+
Foreground
+
bg-foreground
+
+
+
Accents-1
+
bg-accents-1
+
+
+
Accents-2
+
bg-accents-2
+
+ +
+
Blue (Info)
+
+
+
Green (Success)
+
+
+
Yellow (Warning)
+
+
+
Red (Danger)
+
+
+
+ + +
+

Buttons

+
+
+ + + + +
+
+ + + +
+
+
+ + +
+

Forms

+
+
+

Inputs

+ + +
+ + +
+ + +
+ + +
+ + +
+ +
+ + +
+
+ + +
+ + +
+
+ +
+

States & Toggles

+ + +
+ + +

This field is required.

+
+ + +
+ + +
+ + +
+ + +
+
+
+
+ + +
+

Cards

+
+ +
+

Simple Card

+

Just a div with `card` class.

+
+ + +
+

Hoverable Card

+

Add `hover:border-foreground` for interactive feel.

+
+ + +
+
+ +
+
+

Icon Card

+

Layout with flexbox.

+
+
+
+
+ + +
+

Nested Cards

+
+

Parent Glass Card

+

This is the main container card.

+ +
+
+

Nested Card 1

+

Standard content inside a generic sub-card container.

+
+ +
+
+ +
+
+

Nested with Icon

+

Additional context here.

+
+
+
+ +
+
+

Full Width Sub-Card

+
+
+

This sub-card has a header and content area, simulating a mini-panel.

+
+
+
+
+ + +
+

Data Table

+ +
+

Detailed List

+

Using .table-glass class for a premium look.

+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameStatusRoleAction
+
+
+ JD +
+
+
Jane Cooper
+
jane.cooper@example.com
+
+
+
+ + Active + + Admin + +
+
+
+ CW +
+
+
Cody Fisher
+
cody.fisher@example.com
+
+
+
+ + Offline + + User + +
+
+
+ EW +
+
+
Esther Howard
+
esther.howard@example.com
+
+
+
+ + On Leave + + Editor + +
+
+ +
+
Showing 1 to 3 of 12 results
+
+ + +
+
+
+
+ + +
+

Alerts & Confirmations (JS Helper)

+
+

You can trigger standardized premium alerts using the global Mivo helper.

+ +
+ + + + +
+ +
+

Confirmation Example

+ +
+
+ +
+

Stacking Toasts (Custom Helper)

+
+

Premium non-disruptive notifications that stack from the bottom-right.

+ +
+ + + + +
+
+
+ +
+ + diff --git a/app/Views/errors/default.php b/app/Views/errors/default.php new file mode 100644 index 0000000..181af78 --- /dev/null +++ b/app/Views/errors/default.php @@ -0,0 +1,36 @@ + + +
+
+
+ +
+ +

+

+ +

+ +

+ +
+ + Return Home + + +
+
+
+ + diff --git a/app/Views/errors/development.php b/app/Views/errors/development.php new file mode 100644 index 0000000..2a8e3dd --- /dev/null +++ b/app/Views/errors/development.php @@ -0,0 +1,214 @@ + + + + + + System Error - MIVO + + + + + + + + +
+
+
+ +
+
+ MIVO +
+
+ +
+ + + System Error + +
+ +
+ + + + +
+ + +
+
+
+ + getMessage(); + $file = $exception->getFile(); + $line = $exception->getLine(); + $trace = $exception->getTraceAsString(); + + // Code Snippet Logic + $snippet = []; + if (file_exists($file)) { + $lines = file($file); + $start = max(0, $line - 6); + $end = min(count($lines), $line + 5); + + for ($i = $start; $i < $end; $i++) { + $snippet[$i + 1] = $lines[$i]; + } + } + ?> + + +
+ +
+ + +
+
+
+
+ +
+
+
+
+ + FATAL EXCEPTION + + + + +
+ +

+ +

+ +

+ +

+ +
+
+ + Line +
+
+ + $code): ?> + + + + + + +
+
+
+
+
+
+ + +
+
+

+ + Stack Trace +

+ +
+ +
+
+ PHP Stack Trace +
+
+
+
+ + + +
+ +
+ + + + + + + diff --git a/app/Views/home.php b/app/Views/home.php new file mode 100644 index 0000000..597b147 --- /dev/null +++ b/app/Views/home.php @@ -0,0 +1,90 @@ + + +
+
+
+ Mikhmon Logo + +
+
+ +

+

+ A modern, lightweight MikroTik Hotspot Manager built for performance and simplicity. +

+ + + + + + + +
+

Quick Access

+
+ + + + + + + + + + + + + + + + + + + +
Session NameHotspot NameIP Address + Actions +
+
+
+ +
+
+
+
ID:
+
+
+
+
+
+
+
+ + Open + +
+
+
+ +
+ + diff --git a/app/Views/hotspot/cookies.php b/app/Views/hotspot/cookies.php new file mode 100644 index 0000000..5cab4d7 --- /dev/null +++ b/app/Views/hotspot/cookies.php @@ -0,0 +1,209 @@ + + +
+
+

Hotspot Cookies

+

Active authentication cookies for:

+
+
+ + + Dashboard + +
+
+ + +
+ + +
+ + +
+ +
+ +
+
+ +
+ +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + +
UserMAC AddressIP AddressExpires InAction
+ + + + + + + + +
+
+ + + +
+
+
+ + +
+
+ Showing 0 to 0 of 0 cookies +
+
+ +
+ +
+
+
+
+ + + diff --git a/app/Views/hotspot/generate.php b/app/Views/hotspot/generate.php new file mode 100644 index 0000000..ada1443 --- /dev/null +++ b/app/Views/hotspot/generate.php @@ -0,0 +1,260 @@ + + + + + + +
+
+

Generate Vouchers

+

Create multiple hotspot vouchers in batch for:

+
+ + + Back to Users + +
+ +
+ +
+
+
+

+
+ +
+ Batch Generation Settings +

+
+ +
+ + +
+ +
+

Core Config

+ + +
+ +
+ +
Users
+
+

Count of vouchers to generate.

+
+ + +
+ + +

Target Hotspot Instance.

+
+ + +
+ + +

Login credential format.

+
+ + +
+ +
+ + + + +
+

Note for this batch.

+
+
+ + +
+

User Format

+ + +
+ + +

Length of username/password.

+
+ + +
+ +
+ + + + +
+

Prefix for generated usernames.

+
+ + +
+ + +

Character types to include.

+
+
+
+ + +
+

Limits & Profile

+ +
+ +
+ + +

Apply speed limits from profile.

+
+ + + + + +
+ +
+ +
+ +
D
+
+ +
+ +
H
+
+ +
+ +
M
+
+
+

Max uptime (e.g. 1h, 30m).

+
+ + +
+ +
+
+
+ +
+ +
+ +
+

Max data transfer (MB).

+
+
+
+ + +
+ Cancel + +
+
+
+
+ + +
+
+
+

+ + Quick Tips +

+
+
+

User Mode

+
    +
  • + + User Mode: UP (separate), VC (same). +
  • +
+
+
+

User Format

+
    +
  • + + Format Examples: abcd (lower), 1234 (num), Mix (upper/lower/num). +
  • +
+
+
+

Limits

+
    +
  • + + Limits: Time (e.g. 1h, 30m), Data (e.g. 100MB). Leave empty to use Profile default. +
  • +
+
+
+
+
+
+
+ + + + diff --git a/app/Views/hotspot/profiles/add.php b/app/Views/hotspot/profiles/add.php new file mode 100644 index 0000000..ec04793 --- /dev/null +++ b/app/Views/hotspot/profiles/add.php @@ -0,0 +1,236 @@ + + +
+
+
+

Add Profile

+

Create a new hotspot user profile for:

+
+ + Back + +
+ +
+ +
+
+

+
+ +
+ New Profile Settings +

+ +
+ + + +
+

General

+ + +
+ + +
+ + +
+
+ + +
+
+ +
+ + + + +
+
+
+
+ + +
+

Limits & Queues

+ +
+ +
+ +
+ + + + +
+
+ + +
+ + +
+
+
+ + +
+

Pricing & Validity

+ + +
+ + +

Action when validity expires.

+
+ + + + + +
+
+ +
+ + + + +
+
+
+ +
+ + + + +
+
+
+ + +
+ + +

Lock user to one specific MAC address.

+
+
+ +
+ Cancel + +
+
+
+
+ + +
+
+
+

+ + Quick Tips +

+
    +
  • + + Rate Limit: Rx/Tx (Upload/Download). Example: 512k/1M +
  • +
  • + + Expired Mode: Select 'Remove' or 'Notice' to enable Validity. +
  • +
  • + + Parent Queue: Assigns users to a specific parent queue for bandwidth management. +
  • +
+
+
+
+
+
+ + + diff --git a/app/Views/hotspot/profiles/edit.php b/app/Views/hotspot/profiles/edit.php new file mode 100644 index 0000000..080f490 --- /dev/null +++ b/app/Views/hotspot/profiles/edit.php @@ -0,0 +1,241 @@ + + +
+
+
+

Edit Profile

+

"}'>Edit hotspot user profile:

+
+ + Back + +
+ +
+ +
+
+

+
+ +
+ Edit Profile +

+ +
+ + + + +
+

General

+ + +
+ + +
+ + +
+
+ + +
+
+ +
+ + + + +
+
+
+
+ + +
+

Limits & Queues

+ +
+ +
+ +
+ + + + +
+
+ + +
+ + +
+
+
+ + +
+

Pricing & Validity

+ + +
+ + + +

Action when validity expires.

+
+ + + + + +
+
+ +
+ + + + +
+
+
+ +
+ + + + +
+
+
+ + +
+ + + +

Lock user to one specific MAC address.

+
+
+ +
+ Cancel + +
+
+
+
+ + +
+
+
+

+ + Quick Tips +

+
    +
  • + + Rate Limit: Rx/Tx (Upload/Download). Example: 512k/1M +
  • +
  • + + Expired Mode: Select 'Remove' or 'Notice' to enable Validity. +
  • +
  • + + Parent Queue: Assigns users to a specific parent queue for bandwidth management. +
  • +
+
+
+
+
+
+ + + diff --git a/app/Views/hotspot/profiles/index.php b/app/Views/hotspot/profiles/index.php new file mode 100644 index 0000000..1b9f68d --- /dev/null +++ b/app/Views/hotspot/profiles/index.php @@ -0,0 +1,311 @@ + + +
+
+

User Profiles

+

Manage hotspot rate limits and pricing for session

+
+ +
+ + +
+ + +
+ + + +
+ +
+ +
+
+ +
+ +
+ + +
+
+ +
+
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameShared UsersRate LimitParent QueueExpired ModeValidityPriceSelling PriceLock UserActions
+
+
+ +
+
+ + + +
+
+
+ + dev + + + + + + + - + + + + + + + + + + + + + + +
+ + + +
+ + + +
+
+
+ + +
+
+ Showing 0 to 0 of 0 profiles +
+
+ +
+ +
+
+
+
+ + + diff --git a/app/Views/hotspot/users/add.php b/app/Views/hotspot/users/add.php new file mode 100644 index 0000000..2f0b50a --- /dev/null +++ b/app/Views/hotspot/users/add.php @@ -0,0 +1,171 @@ + + +
+
+
+

Add User

+

Generate a new voucher/user for session:

+
+ + Back to List + +
+ +
+
+
+

+
+ +
+ User Details +

+ +
+ + +
+ +
+ +
+ + + + +
+

Unique username for login.

+
+ +
+ +
+ + + + +
+

Strong password for security.

+
+ + +
+ + + +

Profile determines speed limit and shared user policy.

+
+ + +
+ +
+ +
+ D + +
+ +
+ H + +
+ +
+ M + +
+
+

Total allowed uptime (Days, Hours, Minutes).

+
+ + +
+ +
+
+ + + + +
+
+ +
+
+

Limit data usage (0 for unlimited).

+
+ + +
+ +
+ + + + +
+

Additional notes or contact info.

+
+
+ +
+ Cancel + +
+
+
+
+ + +
+
+

+ + Quick Tips +

+
    +
  • + + Profiles define the default speed limits, session timeout, and shared users policy. +
  • +
  • + + Time Limit is the total accumulated uptime allowed for this user. +
  • +
  • + + Data Limit will override the profile's data limit settings if specified here. Set to 0 to use profile default. +
  • +
+
+
+
+
+ + + diff --git a/app/Views/hotspot/users/edit.php b/app/Views/hotspot/users/edit.php new file mode 100644 index 0000000..518ad4c --- /dev/null +++ b/app/Views/hotspot/users/edit.php @@ -0,0 +1,134 @@ + + + + + +
+
+

Edit Hotspot User

+

"}'>Update user details for:

+
+ + + Cancel + +
+ +
+
+ + + +
+ +
+ +
+
+ +
+ +
+
+ + +
+ +
+
+ +
+ +
+
+ + +
+ + +
+ + +
+ + +
+ + +
+ +
+ +
+ D + +
+ +
+ H + +
+ +
+ M + +
+
+
+ + +
+ +
+
+ + + + +
+
+ +
+
+
+ + +
+ +
+
+ +
+ +
+
+
+ + +
+ Cancel + +
+
+
+ + diff --git a/app/Views/hotspot/users/users.php b/app/Views/hotspot/users/users.php new file mode 100644 index 0000000..891b45c --- /dev/null +++ b/app/Views/hotspot/users/users.php @@ -0,0 +1,449 @@ + + +
+
+

Hotspot Users

+

Manage vouchers and user accounts for session:

+
+ +
+ + +
+ + +
+ + + +
+ 0 Selected +
+ + +
+ + +
+ +
+ +
+
+ +
+ +
+ + +
+
+ +
+
+ +
+
+
+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + NameProfileUptime / LimitBytes In/OutComment + Actions +
+ + +
+
+ +
+
+
+
+ +
+
+
+
+
+ + + + +
+
Limit:
+
+
+ + +
+
+
+
+
+ + + + +
+ + + +
+
+
+ + +
+
+ Showing 0 to 0 of 0 users +
+
+ +
+ +
+
+
+
+ + + diff --git a/app/Views/install.php b/app/Views/install.php new file mode 100644 index 0000000..070412a --- /dev/null +++ b/app/Views/install.php @@ -0,0 +1,75 @@ + + +
+
+ +
+ +
+
+ + MIVO Logo + +
+
+

Welcome to MIVO

+

System Installation & Setup

+
+ +
+
+ + +
+
+
1
+
+

Database Setup

+

Tables will be created automatically (SQLite).

+
+
+ +
+
2
+
+

Encryption Key

+

Secure key generation for passwords.

+
+
+ +
+
3
+
+

Admin Account

+ +
+
+ + +
+
+ + +
+
+
+
+
+ +
+ +
+
+
+ +
+
+ + + + diff --git a/app/Views/layouts/footer_main.php b/app/Views/layouts/footer_main.php new file mode 100644 index 0000000..0aede91 --- /dev/null +++ b/app/Views/layouts/footer_main.php @@ -0,0 +1,226 @@ + + + + + + + + +
+
+

+
+
+ + + + + + diff --git a/app/Views/layouts/footer_public.php b/app/Views/layouts/footer_public.php new file mode 100644 index 0000000..2e13eb0 --- /dev/null +++ b/app/Views/layouts/footer_public.php @@ -0,0 +1,82 @@ +
+ +
+ + + + diff --git a/app/Views/layouts/header_main.php b/app/Views/layouts/header_main.php new file mode 100644 index 0000000..c125856 --- /dev/null +++ b/app/Views/layouts/header_main.php @@ -0,0 +1,135 @@ + + + + + + + <?= $title; ?> + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+
+
+ '; + } + } + ?> + + diff --git a/app/Views/layouts/header_public.php b/app/Views/layouts/header_public.php new file mode 100644 index 0000000..7a65938 --- /dev/null +++ b/app/Views/layouts/header_public.php @@ -0,0 +1,68 @@ + + + + + + <?= $title ?? 'MIVO' ?> + + + + + + + + + + + + +
+ +
+
+
+
+ + + + + diff --git a/app/Views/layouts/navbar_main.php b/app/Views/layouts/navbar_main.php new file mode 100644 index 0000000..1ab49d4 --- /dev/null +++ b/app/Views/layouts/navbar_main.php @@ -0,0 +1,127 @@ + + + diff --git a/app/Views/layouts/sidebar_session.php b/app/Views/layouts/sidebar_session.php new file mode 100644 index 0000000..30ea865 --- /dev/null +++ b/app/Views/layouts/sidebar_session.php @@ -0,0 +1,475 @@ +getAllSessions(); + +// Find current session details to get Hotspot Name / IP +$currentSessionDetails = []; +foreach ($allSessions as $s) { + if (isset($session) && $s['session_name'] === $session) { + $currentSessionDetails = $s; + break; + } +} +// Determine label: Hotspot Name > IP Address > 'MIVO' +$sessionLabel = $currentSessionDetails['hotspot_name'] ?? $currentSessionDetails['ip_address'] ?? 'MIVO'; +if (empty($sessionLabel)) { + $sessionLabel = $currentSessionDetails['ip_address'] ?? 'MIVO'; +} + +// Helper for Session Initials (Kebab-friendly) +$getInitials = function($name) { + if (empty($name)) return 'UN'; + if (strpos($name, '-') !== false) { + $parts = explode('-', $name); + $initials = ''; + foreach ($parts as $part) { + if (!empty($part)) $initials .= substr($part, 0, 1); + } + return strtoupper(substr($initials, 0, 2)); + } + return strtoupper(substr($name, 0, 2)); +}; +?> +
+ + + + + + + +
+ +
+
+ + + MIVO +
+
+ +
+ +
+ + +
+ +
+ + +
+
+
+ +
+
+ +
+
+
+ +
+
+ + +
+
+ + diff --git a/app/Views/layouts/sidebar_settings.php b/app/Views/layouts/sidebar_settings.php new file mode 100644 index 0000000..4b28801 --- /dev/null +++ b/app/Views/layouts/sidebar_settings.php @@ -0,0 +1,87 @@ + 'routers_title', 'url' => '/settings', 'namespace' => 'settings'], + ['label' => 'system', 'url' => '/settings/system', 'namespace' => 'settings'], + ['label' => 'templates_title', 'url' => '/settings/templates', 'namespace' => 'settings'], + ['label' => 'logos_title', 'url' => '/settings/logos', 'namespace' => 'settings'], + ['label' => 'api_cors_title', 'url' => '/settings/api-cors', 'namespace' => 'settings'], +]; +?> + + + diff --git a/app/Views/login.php b/app/Views/login.php new file mode 100644 index 0000000..33c88c4 --- /dev/null +++ b/app/Views/login.php @@ -0,0 +1,80 @@ + + + +
+
+ +
+ +
+
+ + MIVO Logo + +
+
+ +

Welcome back, please sign in to continue.

+
+ +
+
+
+ +
+
+ +
+ +
+
+
+ +
+
+ +
+ + +
+
+ +
+
+ +
+
+ + + + + + + diff --git a/app/Views/network/dhcp.php b/app/Views/network/dhcp.php new file mode 100644 index 0000000..df31b9e --- /dev/null +++ b/app/Views/network/dhcp.php @@ -0,0 +1,247 @@ + + +
+
+

DHCP Leases

+

Active DHCP leases for:

+
+
+ + + Dashboard + +
+
+ + +
+ + +
+ + +
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + +
AddressMAC AddressServerStatusHost Name
+
+
+
+ + +
+
+ + + + + + +
+
+ + +
+
+ Showing 0 to 0 of 0 leases +
+
+ +
+ +
+
+
+
+ + + diff --git a/app/Views/print/custom.php b/app/Views/print/custom.php new file mode 100644 index 0000000..f45c8b0 --- /dev/null +++ b/app/Views/print/custom.php @@ -0,0 +1,144 @@ + + + + + + Print Voucher + + + + + + +
+ $u): ?> +
+ $u['username'], + '{{password}}' => $u['password'], + '{{price}}' => $u['price'], + '{{validity}}' => $u['validity'], + '{{timelimit}}' => $u['timelimit'] ?? $u['validity'], // Fallback if missing + '{{datalimit}}' => $u['datalimit'] ?? '', + '{{profile}}' => $u['profile'], + '{{comment}}' => $u['comment'], + '{{hotspotname}}' => $u['hotspotname'], + '{{dns_name}}' => $u['dns_name'], + '{{login_url}}' => $u['login_url'], + '{{num}}' => ($index + 1), + '{{logo}}' => '', // Default Logo placeholder + ]; + + // 1. Handle {{logo id=...}} + $html = preg_replace_callback('/\{\{logo\s+id=[\'"]?([^\'"\s]+)[\'"]?\}\}/i', function($matches) use ($logoMap) { + $id = $matches[1]; + if (isset($logoMap[$id])) { + return ''; // Default style, user can wrap in div + } + return ''; // Return empty if not found + }, $html); + + foreach ($replacements as $key => $val) { + $html = str_replace($key, $val, $html); + } + + // 2. Handle QR Code with Logo support + $html = preg_replace_callback('/\{\{qrcode(?:\s+(.*?))?\}\}/i', function($matches) use ($index, $u, $logoMap) { + $qrId = "qr-custom-" . $index . "-" . uniqid(); + $qrCodeValue = $u['login_url'] . "?user=" . $u['username'] . "&password=" . $u['password']; + + // Default Options + $opts = [ + 'element' => 'document.getElementById("'.$qrId.'")', + 'value' => $qrCodeValue, + 'size' => 100, + 'foreground' => 'black', + 'background' => 'white', + 'padding' => null, + 'logo' => null // Logo ID + ]; + + $rounded = ''; + + // Parse Attributes + if (!empty($matches[1])) { + $attrs = $matches[1]; + if (preg_match('/fg\s*=\s*[\'"]?([^\'"\s]+)[\'"]?/i', $attrs, $m)) $opts['foreground'] = $m[1]; + if (preg_match('/bg\s*=\s*[\'"]?([^\'"\s]+)[\'"]?/i', $attrs, $m)) $opts['background'] = $m[1]; + if (preg_match('/size\s*=\s*[\'"]?(\d+)[\'"]?/i', $attrs, $m)) $opts['size'] = $m[1]; + if (preg_match('/padding\s*=\s*[\'"]?(\d+)[\'"]?/i', $attrs, $m)) $opts['padding'] = $m[1]; + if (preg_match('/rounded\s*=\s*[\'"]?(\d+)[\'"]?/i', $attrs, $m)) $rounded = 'border-radius: ' . $m[1] . 'px;'; + if (preg_match('/logo\s*=\s*[\'"]?([^\'"\s]+)[\'"]?/i', $attrs, $m)) $opts['logo'] = $m[1]; + } + + // CSS Styles + $cssPadding = $opts['padding'] ? ('padding: ' . $opts['padding'] . 'px; ') : ''; + $cssBg = 'background-color: ' . $opts['background'] . '; '; + $baseStyle = 'display: inline-block; vertical-align: middle; ' . $cssBg . $cssPadding . $rounded; + + // JS Generation + $qrJs = " + (function() { + var qr = new QRious({ + element: document.getElementById('$qrId'), + value: \"{$opts['value']}\", + size: {$opts['size']}, + foreground: \"{$opts['foreground']}\", + backgroundAlpha: 0 + }); + "; + + // If Logo is requested and found + if ($opts['logo'] && isset($logoMap[$opts['logo']])) { + $logoPath = $logoMap[$opts['logo']]; + $qrJs .= " + var img = new Image(); + img.src = '$logoPath'; + img.onload = function() { + var canvas = document.getElementById('$qrId'); + var ctx = canvas.getContext('2d'); + var size = {$opts['size']}; + var logoSize = size * 0.2; // Logo is 20% of QR size + var logoPos = (size - logoSize) / 2; + ctx.drawImage(img, logoPos, logoPos, logoSize, logoSize); + }; + "; + } + + $qrJs .= "})();"; + + return ''; + }, $html); + + echo $html; + ?> +
+ +
+ + diff --git a/app/Views/print/default.php b/app/Views/print/default.php new file mode 100644 index 0000000..87fe848 --- /dev/null +++ b/app/Views/print/default.php @@ -0,0 +1,76 @@ + + + + + + Print Voucher + + + + + + +
+ $u): ?> +
+
+
Profile:
+
Valid:
+
Price:
+ +
+ User:
+ Pass: +
+ +
+ +
+ +
+ Login: +
+
+ + +
+ + + + diff --git a/app/Views/print/toolbar.php b/app/Views/print/toolbar.php new file mode 100644 index 0000000..fc5b581 --- /dev/null +++ b/app/Views/print/toolbar.php @@ -0,0 +1,45 @@ + +
+
+ + +
+ +
+ + +
+
+ + diff --git a/app/Views/public/status.php b/app/Views/public/status.php new file mode 100644 index 0000000..8b4b07f --- /dev/null +++ b/app/Views/public/status.php @@ -0,0 +1,215 @@ + + + +
+
+ +
+ + +
+
+ MIVO Logo + +
+
+ + +
+

Check Voucher Status

+

+ Monitor your data usage and voucher validity in real-time without needing to re-login. +

+
+ + +
+
+
+
+ +
+
+ +
+ +
+
+ + +
+
+
+ +
+
+
+ + + + + + diff --git a/app/Views/quick_print/index.php b/app/Views/quick_print/index.php new file mode 100644 index 0000000..ceba8ea --- /dev/null +++ b/app/Views/quick_print/index.php @@ -0,0 +1,106 @@ + + +
+ +
+
+

Quick Print

+

Instant voucher generation and printing.

+
+ +
+ + +
+ +
+ +

No Packages Found

+

Create a Quick Print package to get started.

+ + Create Package + +
+ + + +
+ +
+ +
+
+
+

+ +

+
+ Profile: +
+
+
+
+ 0 ? number_format($pkg['price'], 0, ',', '.') : 'Free') ?> +
+
+ +
+
+
+ + +
+
+ Prefix + +
+
+ Server + +
+
+ + + + + +

+ +
+
+ + +
+
+ + + + + diff --git a/app/Views/quick_print/list.php b/app/Views/quick_print/list.php new file mode 100644 index 0000000..9efb8aa --- /dev/null +++ b/app/Views/quick_print/list.php @@ -0,0 +1,329 @@ + + +
+ +
+
+

Manage Packages

+

Configure your Quick Print voucher packages for:

+
+
+ + Back + + +
+
+ + +
+ +
+
+ +
+ +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameProfilePrefixPriceTime LimitActions
No packages found.
+
+
+ +
+
0 ? number_format($pkg['price'], 0, ',', '.') : 'Free') ?> +
+ +
+ + + +
+ +
+
+ + +
+
+ Showing 0 to 0 of 0 packages +
+
+ +
+ +
+
+
+
+ + + + + + + diff --git a/app/Views/reports/resume.php b/app/Views/reports/resume.php new file mode 100644 index 0000000..9c592d8 --- /dev/null +++ b/app/Views/reports/resume.php @@ -0,0 +1,113 @@ + + +
+
+

Resume Report

+

Overview of aggregated income.

+
+
+ + Total Income: + +
+
+ + +
+ +
+ + +
+
+ + + + + + + + + $total): ?> + + + + + + +
DateTotal
+
+
+ + + + + + + + + + diff --git a/app/Views/reports/selling.php b/app/Views/reports/selling.php new file mode 100644 index 0000000..1de5f58 --- /dev/null +++ b/app/Views/reports/selling.php @@ -0,0 +1,205 @@ + + +
+
+

Selling Report

+

Sales summary and details for:

+
+
+ + +
+
+ + +
+
+
Total Income
+
+ +
+
+
+
Total Vouchers Sold
+
+ +
+
+
+ +
+ +
+ +
+
+ +
+ +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + +
Date / Batch (Comment)QtyTotal
No sales data found.
+ +
+ + +
+
+ Showing 0 to 0 of 0 rows +
+
+ +
+ +
+
+
+
+ + + + diff --git a/app/Views/reports/user_log.php b/app/Views/reports/user_log.php new file mode 100644 index 0000000..fe69488 --- /dev/null +++ b/app/Views/reports/user_log.php @@ -0,0 +1,246 @@ + + +
+
+

User Log

+

Login and logout history for:

+
+
+ + + Dashboard + +
+
+ + +
+ + +
+ + +
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+
+
+ +
+ + + + + + + + + + + + + + + + + + + + +
TimeTopicsMessage
+ + + + + + + +
+ + +
+
+ Showing 0 to 0 of 0 logs +
+
+ +
+ +
+
+
+
+ + + diff --git a/app/Views/security/bindings.php b/app/Views/security/bindings.php new file mode 100644 index 0000000..4b22091 --- /dev/null +++ b/app/Views/security/bindings.php @@ -0,0 +1,340 @@ + + +
+
+

IP Bindings

+

Manage IP bindings (bypass/blocked) for:

+
+ +
+ + +
+ + +
+ + +
+ +
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
MAC AddressAddressTo Address +
Type
+
+
Comment
+
Actions
+
+
+ +
+ +
+
+ + + + + +
+
+ + + +
+
+
+ +
+
+ Showing 0 to 0 of 0 +
+
+ +
+ +
+
+
+
+ + +
+
+
+
+ +
+

Add Binding

+
+ +
+ + +
+ +
+ + + + +
+

Target device MAC address.

+
+ +
+ + +

Target IP address (optional).

+
+ +
+ + +

Translate to this IP (optional).

+
+ +
+ + +
+ +
+ + +

Apply to specific Hotspot server.

+
+ +
+ + +

Note for this binding.

+
+ +
+ +
+ + +
+

+ Tips +

+
    +
  • Bypassed: Access without login.
  • +
  • Blocked: Deny access completely.
  • +
  • Regular: Normal hotspot client.
  • +
+
+
+
+
+
+ + + diff --git a/app/Views/security/walled_garden.php b/app/Views/security/walled_garden.php new file mode 100644 index 0000000..5043caf --- /dev/null +++ b/app/Views/security/walled_garden.php @@ -0,0 +1,338 @@ + + +
+
+

Walled Garden

+

Manage allowed destinations (bypass without login) for:

+
+ +
+ + +
+ + +
+ + +
+ +
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + +
Dst. Host / IPProtocol / Port +
Action
+
+
Comment
+
Act
+
+
+ +
+
+
+
+
:
+
+ + + + + +
+
+ + + +
+
+
+ + +
+
+ Showing 0 to 0 of 0 +
+
+ +
+ +
+
+
+
+ + +
+
+
+
+ +
+

Add Entry

+
+ +
+ + +
+ +
+ + + + +
+

Domain to allow (wildcard supported).

+
+
+ + +

Destination IP Address.

+
+ +
+
+ + +
+
+ + +
+
+ +
+ + +

Allow (bypass) or Deny access.

+
+ +
+ + +

Apply to specific Hotspot server.

+
+ +
+ + +

Note for this rule.

+
+ +
+ +
+ + +
+

+ Tips +

+
    +
  • Dst. Host: Domain name (e.g. *.google.com).
  • +
  • Dst. IP: Specific IP address.
  • +
  • Action: Allow to bypass auth.
  • +
+
+
+
+
+
+ + + diff --git a/app/Views/settings/api_cors.php b/app/Views/settings/api_cors.php new file mode 100644 index 0000000..b8f3c2f --- /dev/null +++ b/app/Views/settings/api_cors.php @@ -0,0 +1,220 @@ + + + + + +
+ +
+

API CORS

+

Manage Cross-Origin Resource Sharing for API access.

+
+ + +
+
+ +
+ +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + +
OriginAllowed MethodsAllowed HeadersActions
+
+
Max Age: s
+
+
+ + + +
+
+
+
+
+ +
+ + +
+
+
+
+ +

No CORS rules found. Add your first origin to allow external API access.

+
+
+
+
+ + + + + + + + + + diff --git a/app/Views/settings/form.php b/app/Views/settings/form.php new file mode 100644 index 0000000..4b9b36b --- /dev/null +++ b/app/Views/settings/form.php @@ -0,0 +1,123 @@ + + +
+
+ + Back to Settings + +

+

Connect Mikhmon to your RouterOS device.

+
+ +
+ + + + +
+
+

Session Settings

+
+
+ + +

Unique ID. Preview: ...

+
+
+ value="1"> + +
+
+
+ +
+

Connection Details

+
+
+ + +
+ +
+
+ + +
+
+ + /> + +

Leave empty to keep existing password.

+ +
+
+
+
+ +
+

Hotspot Information

+
+
+ + +
+ +
+
+ + +
+
+ +
+
+ +
+ +
+
+
+ +
+
+ + +
+
+ + +
+
+
+
+ +
+ Cancel + + +
+
+
+
+ + + + diff --git a/app/Views/settings/index.php b/app/Views/settings/index.php new file mode 100644 index 0000000..5c37951 --- /dev/null +++ b/app/Views/settings/index.php @@ -0,0 +1,110 @@ + + + + + +
+ +
+
+

Router Sessions

+

Manage your stored MikroTik connections.

+
+
+ + +
+ + + +
+
+ +
+

No routers configured

+

Connect your first MikroTik router to start managing hotspots and vouchers.

+ + Connect Router + +
+ +
+ + + + + + + + + + + + + + + + + + + +
Session NameHotspot NameIP Address + Actions +
+
+
+ +
+
+
+ + + + +
+
ID:
+
+
+
+
+
+
+
+ + Open + + + + +
+ + +
+
+
+
+ Showing all stored sessions +
+ + Add New + +
+
+ +
+
+ + diff --git a/app/Views/settings/logos.php b/app/Views/settings/logos.php new file mode 100644 index 0000000..de6b69d --- /dev/null +++ b/app/Views/settings/logos.php @@ -0,0 +1,121 @@ + + + + + +
+ +
+

Logo Management

+

Upload and manage logos for your hotspots and vouchers.

+
+ + +
+
+ + + +
+
+
+ +
+ +
+
+ +
+

Upload New Logo

+

Drag and drop or click to select file

+

Supports PNG, JPG, SVG, GIF

+
+
+
+ + +
+ +
+

No logos uploaded yet.

+
+ +
+ +
+ +
+ <?= htmlspecialchars($logo['name']) ?> + + +
+ +
+ +
+ + +
+
+
+
+ + +
+
+ + +
+

+
+ +
+
+
+ +
+ +
+
+
+ + + + diff --git a/app/Views/settings/systems.php b/app/Views/settings/systems.php new file mode 100644 index 0000000..1eb129f --- /dev/null +++ b/app/Views/settings/systems.php @@ -0,0 +1,122 @@ + + + + + +
+ +
+

General Settings

+

System-wide configurations and security.

+
+ + +
+
+ + +
+

Security & Access

+
+ + +
+
+
+
+ +
+ + +
+

+ + For security reasons, the administrator username cannot be changed. +

+
+ +
+ +
+ + +
+
+
+
+ +
+
+
+ + +
+
+
+
+ +
+ +
+

Enable direct printing for voucher generation.

+
+
+ +
+ +
+
+
+ + +
+
+

Data Management

+

Backup or restore your application data.

+
+ +
+ +
+
+

Backup Data

+

Download a configuration file (.mivo) containing your database and settings.

+
+ + Download Backup + +
+ + +
+
+

Restore Data

+

Upload a previously backup file (.mivo). Overwrites or adds to existing data.

+
+
+
+ +
+ +
+
+
+
+ +
+
+ + diff --git a/app/Views/settings/templates/add.php b/app/Views/settings/templates/add.php new file mode 100644 index 0000000..9cc6292 --- /dev/null +++ b/app/Views/settings/templates/add.php @@ -0,0 +1,3 @@ + +

{{dns_name}}

+

User: {{username}}

+

Pass: {{password}}

+

Price: {{price}}

+

Valid: {{validity}}

+
'; +require_once ROOT . '/app/Views/layouts/header_main.php'; +?> + +
+ +
+
+ + + +

+
+ +
+ + + + + + + +
+
+ + +
+ + +
+
+ HTML Source + + +
+
+ + + + + + + + + + + + + +
+
+
+ +
+ + +
+
+ Live Preview + +
+ +
+
+ +
+
+
+
+
+ + +
+ + + + + + + diff --git a/app/Views/settings/templates/index.php b/app/Views/settings/templates/index.php new file mode 100644 index 0000000..fd780c3 --- /dev/null +++ b/app/Views/settings/templates/index.php @@ -0,0 +1,181 @@ + + + + + +
+ +
+

Voucher Templates

+

Manage and customize your voucher print designs.

+
+ + +
+
+ + + +
+ +
+
+ +
+ +
+ +
+
+
+

Default Template

+ System +
+

Standard thermal printer friendly template.

+ +
+
+ + + +
+
+ +
+ +
+ +
+ +
+ + +
+ +
+ +
+
+
+

+ Custom +
+

Created:

+ +
+ + Edit + +
+ + + +
+
+
+
+ + +
+
+
+ + + + + + + diff --git a/app/Views/status/active.php b/app/Views/status/active.php new file mode 100644 index 0000000..81a57ea --- /dev/null +++ b/app/Views/status/active.php @@ -0,0 +1,270 @@ + + +
+
+

Active Users

+

Monitor currently active hotspot sessions

+
+ +
+ + +
+ + +
+ + +
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
ServerUserAddress / MACUptime / LeftBytes In/Out + Actions +
+ + +
+
+ +
+
+
+
+
+
+
+
+ +
Left:
+ +
+
+ + +
+
+
+
+ + + +
+
+
+ +
+
+ Showing 0 to 0 of 0 active +
+
+ +
+ +
+
+
+
+ + + diff --git a/app/Views/status/hosts.php b/app/Views/status/hosts.php new file mode 100644 index 0000000..3660807 --- /dev/null +++ b/app/Views/status/hosts.php @@ -0,0 +1,241 @@ + + +
+
+

Hotspot Hosts

+

Devices connected to the hotspot network for:

+
+ +
+ + +
+ + +
+ + +
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + +
MAC AddressAddressTo AddressServerComment
+
+ + +
+
+
+
+
+
+ + + + +
+
+ + +
+
+ Showing 0 to 0 of 0 hosts +
+
+ +
+ +
+
+
+
+ + + diff --git a/app/Views/system/scheduler.php b/app/Views/system/scheduler.php new file mode 100644 index 0000000..833180a --- /dev/null +++ b/app/Views/system/scheduler.php @@ -0,0 +1,354 @@ + + +
+
+

Scheduler

+

Manage RouterOS automated tasks for:

+
+
+ + +
+
+ + +
+ + +
+ + +
+ +
+ +
+
+ +
+ +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + +
NameIntervalNext RunStatusActions
+
+
+
+ + Disabled + + Enabled + + +
+ +
+ + +
+
+
+ + +
+
+ Showing 0 to 0 of 0 tasks +
+
+ +
+ +
+
+
+
+ + + + + + + + + + diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..586a3b0 --- /dev/null +++ b/composer.json @@ -0,0 +1,24 @@ +{ + "name": "dyzulk/mivo", + "description": "MIVO - Modern Mikrotik Voucher Management System", + "type": "project", + "license": "MIT", + "authors": [ + { + "name": "DyzulkDev", + "email": "dev@dyzulk.com" + } + ], + "require": { + "php": "^8.0", + "ext-sqlite3": "*", + "ext-openssl": "*", + "ext-json": "*" + }, + "autoload": { + "psr-4": { + "App\\": "app/" + } + }, + "minimum-stability": "stable" +} \ No newline at end of file diff --git a/deploy.ps1 b/deploy.ps1 new file mode 100644 index 0000000..278dc2c --- /dev/null +++ b/deploy.ps1 @@ -0,0 +1,53 @@ +$ErrorActionPreference = "Stop" + +# Configuration +$RemotePath = "/www/wwwroot/app.mivo.dyzulk.com" + +Write-Host "Starting Deployment to app.mivo.dyzulk.com..." -ForegroundColor Green + +# 1. Build Assets +Write-Host "Building assets..." -ForegroundColor Cyan +cmd /c "npm run build" +if ($LASTEXITCODE -ne 0) { + Write-Error "Build failed!" +} + +# 2. Create Archive +Write-Host "Creating deployment package..." -ForegroundColor Cyan +# Excluding potential garbage +$excludeParams = @("--exclude", "node_modules", "--exclude", ".git", "--exclude", ".github", "--exclude", "temp_debug", "--exclude", "deploy.ps1", "--exclude", "*.tar.gz") +tar -czf deploy_package.tar.gz @excludeParams app public routes mivo src package.json +if ($LASTEXITCODE -ne 0) { + Write-Error "Failed to create archive!" +} + +# 3. Upload +Write-Host "Uploading to server ($RemotePath)..." -ForegroundColor Cyan +scp deploy_package.tar.gz "aapanel:$RemotePath/" +if ($LASTEXITCODE -ne 0) { + Write-Error "SCP upload failed!" +} + +# 4. Extract and Cleanup on Server +Write-Host "Extracting and configuring permissions..." -ForegroundColor Cyan +# Commands: +# 1. cd to remote path +# 2. Extract +# 3. Set ownership to www:www +# 4. Set mivo executable +# 5. Set public folder to 755 (Laravel recommendation) +# 6. Cleanup archive +$remoteCommands = "cd $RemotePath && tar -xzf deploy_package.tar.gz && chown -R www:www . && chmod +x mivo && chmod -R 755 public && rm deploy_package.tar.gz" + +ssh aapanel $remoteCommands +if ($LASTEXITCODE -ne 0) { + Write-Error "Remote deployment failed!" +} + +# 5. Local Cleanup +Write-Host "Cleaning up local package..." -ForegroundColor Cyan +if (Test-Path deploy_package.tar.gz) { + Remove-Item deploy_package.tar.gz +} + +Write-Host "Deployment successfully completed!" -ForegroundColor Green diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..4a5de59 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,14 @@ +version: '3.8' + +services: + app: + build: . + container_name: mivo_app + restart: unless-stopped + ports: + - "8080:80" + volumes: + - ./app/Database:/var/www/html/app/Database + - ./.env:/var/www/html/.env + environment: + - APP_ENV=production diff --git a/docker/nginx.conf b/docker/nginx.conf new file mode 100644 index 0000000..80d85bc --- /dev/null +++ b/docker/nginx.conf @@ -0,0 +1,27 @@ +server { + listen 80; + server_name localhost; + root /var/www/html/public; + index index.php index.html; + + location / { + try_files $uri $uri/ /index.php?$query_string; + } + + location ~ \.php$ { + fastcgi_pass 127.0.0.1:9000; + fastcgi_index index.php; + fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; + include fastcgi_params; + } + + # Deny access to . files + location ~ /\. { + deny all; + } + + # Deny access to sensitive folders explicitly if root wasn't public (safety net) + location ~ ^/(app|docker|docs|routes|src|temp_debug)/ { + deny all; + } +} diff --git a/docker/supervisord.conf b/docker/supervisord.conf new file mode 100644 index 0000000..4f9564b --- /dev/null +++ b/docker/supervisord.conf @@ -0,0 +1,22 @@ +[supervisord] +nodaemon=true +logfile=/dev/null +pidfile=/var/run/supervisord.pid + +[program:php-fpm] +command=docker-php-entrypoint php-fpm +stdout_logfile=/dev/stdout +stdout_logfile_maxbytes=0 +stderr_logfile=/dev/stderr +stderr_logfile_maxbytes=0 +autostart=true +autorestart=true + +[program:nginx] +command=nginx -g 'daemon off;' +stdout_logfile=/dev/stdout +stdout_logfile_maxbytes=0 +stderr_logfile=/dev/stderr +stderr_logfile_maxbytes=0 +autostart=true +autorestart=true diff --git a/docs/INSTALLATION.md b/docs/INSTALLATION.md new file mode 100644 index 0000000..7bf63f4 --- /dev/null +++ b/docs/INSTALLATION.md @@ -0,0 +1,125 @@ +# MIVO Installation Guide + +This guide covers installation on various platforms. MIVO is designed to be lightweight and runs on almost any PHP-capable server. + +## 📋 General Requirements +* **PHP**: 8.0 or higher +* **Extensions**: `sqlite3`, `openssl`, `mbstring`, `json` +* **Database**: SQLite (File based, no server needed) + +--- + +## 🐋 Docker (Recommended) +The easiest way to run MIVO. + +1. **Build & Run** + ```bash + docker-compose up -d --build + ``` +2. **Access** + Go to `http://localhost:8080` + +*Note: The database is persisted in `app/Database` via volumes.* + +--- + +## 🪶 Apache / OpenLiteSpeed +1. **Document Root**: Set your web server's document root to the `public/` folder. +2. **Rewrite Rules**: Ensure `mod_rewrite` is enabled. MIVO includes a `.htaccess` file in `public/` that handles URL routing automatically. +3. **Permissions**: Ensure the web server user (e.g., `www-data`) has **write** access to: + * `app/Database/` (directory and file) + * `app/Config/` (if using installer) + * `.env` file + +--- + +## 🟢 Nginx +Nginx does not read `.htaccess`. Use this configuration block in your `server` block: + +```nginx +server { + listen 80; + server_name your-domain.com; + root /path/to/mivo/public; + index index.php; + + location / { + try_files $uri $uri/ /index.php?$query_string; + } + + location ~ \.php$ { + include snippets/fastcgi-php.conf; + fastcgi_pass unix:/run/php/php8.2-fpm.sock; # Adjust version + } + + location ~ /\.ht { + deny all; + } +} +``` + +--- + +## 🪟 IIS (Windows) +1. **Document Root**: Point the site to the `public/` folder. +2. **Web Config**: A `web.config` file has been provided in `public/` to handle URL Rewriting. +3. **Requirements**: Ensure **URL Rewrite Module 2.0** is installed on IIS. + +--- + +## 📱 STB / Android (Awebserver / Termux) + +### Awebserver +1. Copy the MIVO files to `/htdocs`. +2. Point the document root to `public` if supported, or access via `http://localhost:8080/public`. +3. Ensure PHP version is compatible. + +### Termux +1. Install PHP: `pkg install php` +2. Navigate to MIVO directory: `cd mivo` +3. Use the built-in server: + ```bash + php mivo serve --host=0.0.0.0 --port=8080 + ``` +4. Access via browser. + +--- + + +--- + +## 🌐 Shared Hosting (cPanel / DirectAdmin) +Most shared hosting uses Apache or OpenLiteSpeed, which is fully compatible. + +1. **Upload Files**: Upload the MIVO files to `public_html/mivo` (or a subdomain folder). +2. **Point Domain**: + * **Recommended**: Go to "Domains" or "Subdomains" in cPanel and set the **Document Root** to point strictly to the `public/` folder (e.g., `public_html/mivo/public`). + * **Alternative**: If you cannot change Document Root, you can move contents of `public/` to the root `public_html` and move `app/`, `routes/`, etc. one level up (not recommended for security). +3. **PHP Version**: Select PHP 8.0+ in "Select PHP Version" menu. +4. **Extensions**: Ensure `sqlite3` and `fileinfo` are checked. + +--- + +## 🎛️ aaPanel (VPS) +1. **Create Website**: Add site -> PHP-8.x. +2. **Site Directory**: + * Set **Running Directory** (bukan Site Directory) to `/public`. + * Uncheck "Anti-XSS" (sometimes blocks config saving). +3. **URL Rewrite**: Select `thinkphp` or `laravel` template (compatible) OR just use the Nginx config provided above. +4. **Permissions**: Chown `www` user to the site directory. + +--- + +## ☁️ PaaS Cloud (Railway / Render / Heroku) +**WARNING**: MIVO uses SQLite (File Database). Most PaaS cloud have **Ephemeral Filesytem** (Reset on restart). + +* **Requirement**: You MUST mount a **Persistent Volume/Disk**. +* **Mount Path**: Mount your volume to `/var/www/html/app/Database` (or wherever you put MIVO). +* **Docker**: Use the Docker deployment method, it works natively on these platforms. + +--- + +## 📥 Post-Installation +After setting up the server: +1. Copy `.env.example` to `.env`. +2. Run `php mivo install` OR access `/install` in your browser. diff --git a/mivo b/mivo new file mode 100644 index 0000000..b931be5 --- /dev/null +++ b/mivo @@ -0,0 +1,26 @@ +#!/usr/bin/env php +run($argv); diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..59a479b --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1242 @@ +{ + "name": "mivo", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "mivo", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "chart.js": "^4.5.1", + "flag-icons": "^7.5.0", + "lucide": "^0.562.0", + "qrious": "^4.0.2" + }, + "devDependencies": { + "autoprefixer": "^10.4.23", + "postcss": "^8.5.6", + "tailwindcss": "^3.4.17" + } + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@kurkle/color": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz", + "integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==", + "license": "MIT" + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true, + "license": "MIT" + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "dev": true, + "license": "MIT" + }, + "node_modules/autoprefixer": { + "version": "10.4.23", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.23.tgz", + "integrity": "sha512-YYTXSFulfwytnjAPlw8QHncHJmlvFKtczb8InXaAx9Q0LbfDnfEYDE55omerIJKihhmU61Ft+cAOSzQVaBUmeA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "browserslist": "^4.28.1", + "caniuse-lite": "^1.0.30001760", + "fraction.js": "^5.3.4", + "picocolors": "^1.1.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.9.14", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.14.tgz", + "integrity": "sha512-B0xUquLkiGLgHhpPBqvl7GWegWBUNuujQ6kXd/r1U38ElPT6Ok8KZ8e+FpUGEc2ZoRQUzq/aUnaKFc/svWUGSg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001764", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001764.tgz", + "integrity": "sha512-9JGuzl2M+vPL+pz70gtMF9sHdMFbY9FJaQBi186cHKH3pSzDvzoUJUPV6fqiKIMyXbud9ZLg4F3Yza1vJ1+93g==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chart.js": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.1.tgz", + "integrity": "sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==", + "license": "MIT", + "dependencies": { + "@kurkle/color": "^0.3.0" + }, + "engines": { + "pnpm": ">=8" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "dev": true, + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.267", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.267.tgz", + "integrity": "sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==", + "dev": true, + "license": "ISC" + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/flag-icons": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/flag-icons/-/flag-icons-7.5.0.tgz", + "integrity": "sha512-kd+MNXviFIg5hijH766tt+3x76ele1AXlo4zDdCxIvqWZhKt4T83bOtxUOOMlTx/EcFdUMH5yvQgYlFh1EqqFg==", + "license": "MIT" + }, + "node_modules/fraction.js": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", + "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/jiti": { + "version": "1.21.7", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", + "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "bin/jiti.js" + } + }, + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/lucide": { + "version": "0.562.0", + "resolved": "https://registry.npmjs.org/lucide/-/lucide-0.562.0.tgz", + "integrity": "sha512-k1Fb8ZMnRQovWRlea7Jr0b9UKA29IM7/cu79+mJrhVohvA2YC/Ti3Sk+G+h/SIu3IlrKT4RAbWMHUBBQd1O6XA==", + "license": "ISC" + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-releases": { + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-import": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-js": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz", + "integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "peerDependencies": { + "postcss": "^8.4.21" + } + }, + "node_modules/postcss-load-config": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz", + "integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "lilconfig": "^3.0.0", + "yaml": "^2.3.4" + }, + "engines": { + "node": ">= 14" + }, + "peerDependencies": { + "postcss": ">=8.0.9", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "postcss": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/postcss-nested": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", + "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.1.1" + }, + "engines": { + "node": ">=12.0" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/qrious": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/qrious/-/qrious-4.0.2.tgz", + "integrity": "sha512-xWPJIrK1zu5Ypn898fBp8RHkT/9ibquV2Kv24S/JY9VYEhMBMKur1gHVsOiNUh7PHP9uCgejjpZUHUIXXKoU/g==", + "license": "GPL-3.0" + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pify": "^2.3.0" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.11", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sucrase": { + "version": "3.35.1", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", + "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "tinyglobby": "^0.2.11", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tailwindcss": { + "version": "3.4.17", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz", + "integrity": "sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==", + "dev": true, + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "arg": "^5.0.2", + "chokidar": "^3.6.0", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.3.2", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "jiti": "^1.21.6", + "lilconfig": "^3.1.3", + "micromatch": "^4.0.8", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.1.1", + "postcss": "^8.4.47", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "postcss-load-config": "^4.0.2", + "postcss-nested": "^6.2.0", + "postcss-selector-parser": "^6.1.2", + "resolve": "^1.22.8", + "sucrase": "^3.35.0" + }, + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/yaml": { + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz", + "integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==", + "dev": true, + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..3d6ba20 --- /dev/null +++ b/package.json @@ -0,0 +1,27 @@ +{ + "name": "mivo", + "version": "1.0.0", + "description": "This is a complete rewrite of Mikhmon v3 using a modern MVC architecture.\r It runs on a lightweight custom core designed for performance on low-end devices (STB/Android).", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1", + "build": "npx tailwindcss -i ./src/input.css -o ./public/assets/css/styles.css", + "watch": "npx tailwindcss -i ./src/input.css -o ./public/assets/css/styles.css --watch", + "dev": "npx tailwindcss -i ./src/input.css -o ./public/assets/css/styles.css --watch" + }, + "keywords": [], + "author": "", + "license": "MIT", + "type": "commonjs", + "dependencies": { + "chart.js": "^4.5.1", + "flag-icons": "^7.5.0", + "lucide": "^0.562.0", + "qrious": "^4.0.2" + }, + "devDependencies": { + "autoprefixer": "^10.4.23", + "postcss": "^8.5.6", + "tailwindcss": "^3.4.17" + } +} diff --git a/public/.htaccess b/public/.htaccess new file mode 100644 index 0000000..3aec5e2 --- /dev/null +++ b/public/.htaccess @@ -0,0 +1,21 @@ + + + Options -MultiViews -Indexes + + + RewriteEngine On + + # Handle Authorization Header + RewriteCond %{HTTP:Authorization} . + RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}] + + # Redirect Trailing Slashes If Not A Folder... + RewriteCond %{REQUEST_FILENAME} !-d + RewriteCond %{REQUEST_URI} (.+)/$ + RewriteRule ^ %1 [L,R=301] + + # Send Requests To Front Controller... + RewriteCond %{REQUEST_FILENAME} !-d + RewriteCond %{REQUEST_FILENAME} !-f + RewriteRule ^ index.php [L] + diff --git a/public/assets/css/styles.css b/public/assets/css/styles.css new file mode 100644 index 0000000..6374a13 --- /dev/null +++ b/public/assets/css/styles.css @@ -0,0 +1,5055 @@ +*, ::before, ::after { + --tw-border-spacing-x: 0; + --tw-border-spacing-y: 0; + --tw-translate-x: 0; + --tw-translate-y: 0; + --tw-rotate: 0; + --tw-skew-x: 0; + --tw-skew-y: 0; + --tw-scale-x: 1; + --tw-scale-y: 1; + --tw-pan-x: ; + --tw-pan-y: ; + --tw-pinch-zoom: ; + --tw-scroll-snap-strictness: proximity; + --tw-gradient-from-position: ; + --tw-gradient-via-position: ; + --tw-gradient-to-position: ; + --tw-ordinal: ; + --tw-slashed-zero: ; + --tw-numeric-figure: ; + --tw-numeric-spacing: ; + --tw-numeric-fraction: ; + --tw-ring-inset: ; + --tw-ring-offset-width: 0px; + --tw-ring-offset-color: #fff; + --tw-ring-color: rgb(59 130 246 / 0.5); + --tw-ring-offset-shadow: 0 0 #0000; + --tw-ring-shadow: 0 0 #0000; + --tw-shadow: 0 0 #0000; + --tw-shadow-colored: 0 0 #0000; + --tw-blur: ; + --tw-brightness: ; + --tw-contrast: ; + --tw-grayscale: ; + --tw-hue-rotate: ; + --tw-invert: ; + --tw-saturate: ; + --tw-sepia: ; + --tw-drop-shadow: ; + --tw-backdrop-blur: ; + --tw-backdrop-brightness: ; + --tw-backdrop-contrast: ; + --tw-backdrop-grayscale: ; + --tw-backdrop-hue-rotate: ; + --tw-backdrop-invert: ; + --tw-backdrop-opacity: ; + --tw-backdrop-saturate: ; + --tw-backdrop-sepia: ; + --tw-contain-size: ; + --tw-contain-layout: ; + --tw-contain-paint: ; + --tw-contain-style: ; +} + +::backdrop { + --tw-border-spacing-x: 0; + --tw-border-spacing-y: 0; + --tw-translate-x: 0; + --tw-translate-y: 0; + --tw-rotate: 0; + --tw-skew-x: 0; + --tw-skew-y: 0; + --tw-scale-x: 1; + --tw-scale-y: 1; + --tw-pan-x: ; + --tw-pan-y: ; + --tw-pinch-zoom: ; + --tw-scroll-snap-strictness: proximity; + --tw-gradient-from-position: ; + --tw-gradient-via-position: ; + --tw-gradient-to-position: ; + --tw-ordinal: ; + --tw-slashed-zero: ; + --tw-numeric-figure: ; + --tw-numeric-spacing: ; + --tw-numeric-fraction: ; + --tw-ring-inset: ; + --tw-ring-offset-width: 0px; + --tw-ring-offset-color: #fff; + --tw-ring-color: rgb(59 130 246 / 0.5); + --tw-ring-offset-shadow: 0 0 #0000; + --tw-ring-shadow: 0 0 #0000; + --tw-shadow: 0 0 #0000; + --tw-shadow-colored: 0 0 #0000; + --tw-blur: ; + --tw-brightness: ; + --tw-contrast: ; + --tw-grayscale: ; + --tw-hue-rotate: ; + --tw-invert: ; + --tw-saturate: ; + --tw-sepia: ; + --tw-drop-shadow: ; + --tw-backdrop-blur: ; + --tw-backdrop-brightness: ; + --tw-backdrop-contrast: ; + --tw-backdrop-grayscale: ; + --tw-backdrop-hue-rotate: ; + --tw-backdrop-invert: ; + --tw-backdrop-opacity: ; + --tw-backdrop-saturate: ; + --tw-backdrop-sepia: ; + --tw-contain-size: ; + --tw-contain-layout: ; + --tw-contain-paint: ; + --tw-contain-style: ; +} + +/* +! tailwindcss v3.4.17 | MIT License | https://tailwindcss.com +*/ + +/* +1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4) +2. Allow adding a border to an element by just adding a border-width. (https://github.com/tailwindcss/tailwindcss/pull/116) +*/ + +*, +::before, +::after { + box-sizing: border-box; + /* 1 */ + border-width: 0; + /* 2 */ + border-style: solid; + /* 2 */ + border-color: #e5e7eb; + /* 2 */ +} + +::before, +::after { + --tw-content: ''; +} + +/* +1. Use a consistent sensible line-height in all browsers. +2. Prevent adjustments of font size after orientation changes in iOS. +3. Use a more readable tab size. +4. Use the user's configured `sans` font-family by default. +5. Use the user's configured `sans` font-feature-settings by default. +6. Use the user's configured `sans` font-variation-settings by default. +7. Disable tap highlights on iOS +*/ + +html, +:host { + line-height: 1.5; + /* 1 */ + -webkit-text-size-adjust: 100%; + /* 2 */ + -moz-tab-size: 4; + /* 3 */ + -o-tab-size: 4; + tab-size: 4; + /* 3 */ + font-family: Geist, sans-serif; + /* 4 */ + font-feature-settings: normal; + /* 5 */ + font-variation-settings: normal; + /* 6 */ + -webkit-tap-highlight-color: transparent; + /* 7 */ +} + +/* +1. Remove the margin in all browsers. +2. Inherit line-height from `html` so users can set them as a class directly on the `html` element. +*/ + +body { + margin: 0; + /* 1 */ + line-height: inherit; + /* 2 */ +} + +/* +1. Add the correct height in Firefox. +2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655) +3. Ensure horizontal rules are visible by default. +*/ + +hr { + height: 0; + /* 1 */ + color: inherit; + /* 2 */ + border-top-width: 1px; + /* 3 */ +} + +/* +Add the correct text decoration in Chrome, Edge, and Safari. +*/ + +abbr:where([title]) { + -webkit-text-decoration: underline dotted; + text-decoration: underline dotted; +} + +/* +Remove the default font size and weight for headings. +*/ + +h1, +h2, +h3, +h4, +h5, +h6 { + font-size: inherit; + font-weight: inherit; +} + +/* +Reset links to optimize for opt-in styling instead of opt-out. +*/ + +a { + color: inherit; + text-decoration: inherit; +} + +/* +Add the correct font weight in Edge and Safari. +*/ + +b, +strong { + font-weight: bolder; +} + +/* +1. Use the user's configured `mono` font-family by default. +2. Use the user's configured `mono` font-feature-settings by default. +3. Use the user's configured `mono` font-variation-settings by default. +4. Correct the odd `em` font sizing in all browsers. +*/ + +code, +kbd, +samp, +pre { + font-family: Geist Mono, monospace; + /* 1 */ + font-feature-settings: normal; + /* 2 */ + font-variation-settings: normal; + /* 3 */ + font-size: 1em; + /* 4 */ +} + +/* +Add the correct font size in all browsers. +*/ + +small { + font-size: 80%; +} + +/* +Prevent `sub` and `sup` elements from affecting the line height in all browsers. +*/ + +sub, +sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline; +} + +sub { + bottom: -0.25em; +} + +sup { + top: -0.5em; +} + +/* +1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297) +2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016) +3. Remove gaps between table borders by default. +*/ + +table { + text-indent: 0; + /* 1 */ + border-color: inherit; + /* 2 */ + border-collapse: collapse; + /* 3 */ +} + +/* +1. Change the font styles in all browsers. +2. Remove the margin in Firefox and Safari. +3. Remove default padding in all browsers. +*/ + +button, +input, +optgroup, +select, +textarea { + font-family: inherit; + /* 1 */ + font-feature-settings: inherit; + /* 1 */ + font-variation-settings: inherit; + /* 1 */ + font-size: 100%; + /* 1 */ + font-weight: inherit; + /* 1 */ + line-height: inherit; + /* 1 */ + letter-spacing: inherit; + /* 1 */ + color: inherit; + /* 1 */ + margin: 0; + /* 2 */ + padding: 0; + /* 3 */ +} + +/* +Remove the inheritance of text transform in Edge and Firefox. +*/ + +button, +select { + text-transform: none; +} + +/* +1. Correct the inability to style clickable types in iOS and Safari. +2. Remove default button styles. +*/ + +button, +input:where([type='button']), +input:where([type='reset']), +input:where([type='submit']) { + -webkit-appearance: button; + /* 1 */ + background-color: transparent; + /* 2 */ + background-image: none; + /* 2 */ +} + +/* +Use the modern Firefox focus style for all focusable elements. +*/ + +:-moz-focusring { + outline: auto; +} + +/* +Remove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737) +*/ + +:-moz-ui-invalid { + box-shadow: none; +} + +/* +Add the correct vertical alignment in Chrome and Firefox. +*/ + +progress { + vertical-align: baseline; +} + +/* +Correct the cursor style of increment and decrement buttons in Safari. +*/ + +::-webkit-inner-spin-button, +::-webkit-outer-spin-button { + height: auto; +} + +/* +1. Correct the odd appearance in Chrome and Safari. +2. Correct the outline style in Safari. +*/ + +[type='search'] { + -webkit-appearance: textfield; + /* 1 */ + outline-offset: -2px; + /* 2 */ +} + +/* +Remove the inner padding in Chrome and Safari on macOS. +*/ + +::-webkit-search-decoration { + -webkit-appearance: none; +} + +/* +1. Correct the inability to style clickable types in iOS and Safari. +2. Change font properties to `inherit` in Safari. +*/ + +::-webkit-file-upload-button { + -webkit-appearance: button; + /* 1 */ + font: inherit; + /* 2 */ +} + +/* +Add the correct display in Chrome and Safari. +*/ + +summary { + display: list-item; +} + +/* +Removes the default spacing and border for appropriate elements. +*/ + +blockquote, +dl, +dd, +h1, +h2, +h3, +h4, +h5, +h6, +hr, +figure, +p, +pre { + margin: 0; +} + +fieldset { + margin: 0; + padding: 0; +} + +legend { + padding: 0; +} + +ol, +ul, +menu { + list-style: none; + margin: 0; + padding: 0; +} + +/* +Reset default styling for dialogs. +*/ + +dialog { + padding: 0; +} + +/* +Prevent resizing textareas horizontally by default. +*/ + +textarea { + resize: vertical; +} + +/* +1. Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300) +2. Set the default placeholder color to the user's configured gray 400 color. +*/ + +input::-moz-placeholder, textarea::-moz-placeholder { + opacity: 1; + /* 1 */ + color: #9ca3af; + /* 2 */ +} + +input::placeholder, +textarea::placeholder { + opacity: 1; + /* 1 */ + color: #9ca3af; + /* 2 */ +} + +/* +Set the default cursor for buttons. +*/ + +button, +[role="button"] { + cursor: pointer; +} + +/* +Make sure disabled buttons don't get the pointer cursor. +*/ + +:disabled { + cursor: default; +} + +/* +1. Make replaced elements `display: block` by default. (https://github.com/mozdevs/cssremedy/issues/14) +2. Add `vertical-align: middle` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210) + This can trigger a poorly considered lint error in some tools but is included by design. +*/ + +img, +svg, +video, +canvas, +audio, +iframe, +embed, +object { + display: block; + /* 1 */ + vertical-align: middle; + /* 2 */ +} + +/* +Constrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14) +*/ + +img, +video { + max-width: 100%; + height: auto; +} + +/* Make elements with the HTML hidden attribute stay hidden by default */ + +[hidden]:where(:not([hidden="until-found"])) { + display: none; +} + +:root { + --background: #ffffff; + --foreground: #000000; + --accents-1: #fafafa; + --accents-2: #eaeaea; + --accents-3: #999999; + --accents-4: #888888; + --accents-5: #666666; + --accents-6: #444444; + --accents-7: #333333; + --accents-8: #111111; +} + +.dark { + --background: #000000; + --foreground: #ffffff; + --accents-1: #111111; + --accents-2: #333333; + --accents-3: #444444; + --accents-4: #666666; + --accents-5: #888888; + --accents-6: #999999; + --accents-7: #eaeaea; + --accents-8: #fafafa; +} + +body { + background-color: var(--background); + font-family: Geist, sans-serif; + color: var(--foreground); + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +.\!container { + width: 100% !important; +} + +.container { + width: 100%; +} + +@media (min-width: 640px) { + .\!container { + max-width: 640px !important; + } + + .container { + max-width: 640px; + } +} + +@media (min-width: 768px) { + .\!container { + max-width: 768px !important; + } + + .container { + max-width: 768px; + } +} + +@media (min-width: 1024px) { + .\!container { + max-width: 1024px !important; + } + + .container { + max-width: 1024px; + } +} + +@media (min-width: 1280px) { + .\!container { + max-width: 1280px !important; + } + + .container { + max-width: 1280px; + } +} + +@media (min-width: 1536px) { + .\!container { + max-width: 1536px !important; + } + + .container { + max-width: 1536px; + } +} + +.custom-select-wrapper { + flex-shrink: 0; +} + +.btn { + display: inline-flex; + height: 2.25rem; + align-items: center; + justify-content: center; + border-radius: 0.375rem; + padding-left: 1rem; + padding-right: 1rem; + padding-top: 0.5rem; + padding-bottom: 0.5rem; + font-size: 0.875rem; + line-height: 1.25rem; + font-weight: 500; + transition-property: all; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + transition-duration: 200ms; +} + +.btn:hover { + --tw-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1); + --tw-shadow-colored: 0 4px 6px -1px var(--tw-shadow-color), 0 2px 4px -2px var(--tw-shadow-color); + box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); +} + +.btn:focus-visible { + outline: 2px solid transparent; + outline-offset: 2px; + --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color); + --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color); + box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000); + --tw-ring-color: var(--foreground); +} + +.btn:active { + --tw-scale-x: .95; + --tw-scale-y: .95; + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); +} + +.btn:disabled { + pointer-events: none; + opacity: 0.5; +} + +.btn-primary { + background-color: var(--foreground); + color: var(--background); +} + +.btn-primary:hover { + --tw-translate-y: -0.125rem; + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); + opacity: 0.9; +} + +.btn-secondary { + border-width: 1px; + border-color: var(--accents-2); + background-color: var(--background); + color: var(--foreground); +} + +.btn-secondary:hover { + --tw-translate-y: -0.125rem; + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); + border-color: var(--foreground); + background-color: var(--accents-1); +} + +.btn-danger { + border-width: 1px; + border-color: transparent; + --tw-bg-opacity: 1; + background-color: rgb(239 68 68 / var(--tw-bg-opacity, 1)); + --tw-text-opacity: 1; + color: rgb(255 255 255 / var(--tw-text-opacity, 1)); +} + +.btn-danger:hover { + --tw-translate-y: -0.125rem; + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); + --tw-bg-opacity: 1; + background-color: rgb(220 38 38 / var(--tw-bg-opacity, 1)); + --tw-shadow-color: rgb(239 68 68 / 0.2); + --tw-shadow: var(--tw-shadow-colored); +} + +.btn-icon { + border-radius: 0.5rem; + padding: 0.5rem; + color: var(--accents-5); + transition-property: all; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + transition-duration: 200ms; +} + +.btn-icon:hover { + background-color: rgb(255 255 255 / 0.4); + color: var(--foreground); +} + +.btn-icon:active { + --tw-scale-x: .95; + --tw-scale-y: .95; + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); +} + +.btn-icon:hover:is(.dark *) { + background-color: rgb(255 255 255 / 0.1); +} + +.btn-icon-danger { + border-radius: 0.5rem; + padding: 0.5rem; + --tw-text-opacity: 1; + color: rgb(239 68 68 / var(--tw-text-opacity, 1)); + transition-property: all; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + transition-duration: 200ms; +} + +.btn-icon-danger:hover { + --tw-bg-opacity: 1; + background-color: rgb(254 242 242 / var(--tw-bg-opacity, 1)); + --tw-text-opacity: 1; + color: rgb(220 38 38 / var(--tw-text-opacity, 1)); +} + +.btn-icon-danger:active { + --tw-scale-x: .95; + --tw-scale-y: .95; + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); +} + +.btn-icon-danger:hover:is(.dark *) { + background-color: rgb(127 29 29 / 0.2); +} + +.form-label { + margin-bottom: 0.25rem; + display: block; + font-size: 0.875rem; + line-height: 1.25rem; + font-weight: 500; + color: var(--accents-5); + transition-property: color, background-color, border-color, text-decoration-color, fill, stroke; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + transition-duration: 200ms; +} + +.glass-label, .modal-glass .form-label { + font-weight: 600; + color: var(--foreground); + opacity: 0.8; + --tw-shadow-color: rgb(0 0 0 / 0.1); + --tw-shadow: var(--tw-shadow-colored); +} + +.glass-label:is(.dark *), .modal-glass .form-label:is(.dark *) { + opacity: 0.9; +} + +.form-control, .form-input, .form-input-search, .form-select { + display: flex; + height: 2.5rem; + width: 100%; + min-width: 0px; + align-items: center; + border-radius: 0.75rem; + border-width: 1px; + border-color: var(--accents-2); + background-color: rgb(255 255 255 / 0.4); + padding-left: 0.75rem; + padding-right: 0.75rem; + padding-top: 0.25rem; + padding-bottom: 0.25rem; + font-size: 0.875rem; + line-height: 1.25rem; + color: var(--foreground); + --tw-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05); + --tw-shadow-colored: 0 1px 2px 0 var(--tw-shadow-color); + box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); + --tw-backdrop-blur: blur(24px); + backdrop-filter: var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia); + transition-property: all; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + transition-duration: 300ms; +} + +.form-control::file-selector-button, .form-input::file-selector-button, .form-input-search::file-selector-button, .form-select::file-selector-button { + border-width: 0px; + background-color: transparent; + font-size: 0.875rem; + line-height: 1.25rem; + font-weight: 500; +} + +.form-control::-moz-placeholder, .form-input::-moz-placeholder, .form-input-search::-moz-placeholder, .form-select::-moz-placeholder { + color: var(--accents-5); +} + +.form-control::placeholder, .form-input::placeholder, .form-input-search::placeholder, .form-select::placeholder { + color: var(--accents-5); +} + +.form-control:hover, .form-input:hover, .form-input-search:hover, .form-select:hover { + border-color: var(--accents-4); + background-color: rgb(255 255 255 / 0.6); + --tw-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1); + --tw-shadow-colored: 0 4px 6px -1px var(--tw-shadow-color), 0 2px 4px -2px var(--tw-shadow-color); + box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); +} + +.form-control:focus-visible, .form-input:focus-visible, .form-input-search:focus-visible, .form-select:focus-visible { + border-color: var(--foreground); + outline: 2px solid transparent; + outline-offset: 2px; + --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color); + --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color); + box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000); + --tw-ring-color: var(--foreground); +} + +.form-control:disabled, .form-input:disabled, .form-input-search:disabled, .form-select:disabled { + cursor: not-allowed; + opacity: 0.5; +} + +.form-control:is(.dark *), .form-input:is(.dark *), .form-input-search:is(.dark *), .form-select:is(.dark *) { + border-color: rgb(255 255 255 / 0.1); + background-color: rgb(255 255 255 / 0.05); +} + +.form-control:hover:is(.dark *), .form-input:hover:is(.dark *), .form-input-search:hover:is(.dark *), .form-select:hover:is(.dark *) { + background-color: rgb(255 255 255 / 0.1); +} + +.form-input-search { + min-width: 0px; + border-radius: 9999px; + border-width: 2px; + border-color: rgb(255 255 255 / 0.2); + background-color: rgb(255 255 255 / 0.25); + color: var(--foreground); + --tw-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05); + --tw-shadow-colored: 0 1px 2px 0 var(--tw-shadow-color); + box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); + --tw-backdrop-blur: blur(40px); + backdrop-filter: var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia); + transition-property: all; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + transition-duration: 200ms; +} + +.form-input-search:hover { + border-color: rgb(255 255 255 / 0.4); + background-color: rgb(255 255 255 / 0.4); +} + +.form-input-search:focus-visible { + border-color: rgb(255 255 255 / 0.5); + outline: 2px solid transparent; + outline-offset: 2px; + --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color); + --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(0px + var(--tw-ring-offset-width)) var(--tw-ring-color); + box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000); +} + +.form-input-search:is(.dark *) { + border-color: rgb(255 255 255 / 0.1); + background-color: rgb(0 0 0 / 0.4); +} + +.form-input-search:hover:is(.dark *) { + background-color: rgb(0 0 0 / 0.6); +} + +.form-filter { + display: flex; + height: 2.5rem; + width: 100%; + min-width: 0px; + cursor: pointer; + align-items: center; + justify-content: space-between; + border-radius: 0.75rem; + border-width: 2px; + border-color: rgb(255 255 255 / 0.2); + background-color: rgb(255 255 255 / 0.25); + padding-left: 0.75rem; + padding-right: 0.75rem; + padding-top: 0.25rem; + padding-bottom: 0.25rem; + font-size: 0.875rem; + line-height: 1.25rem; + color: var(--foreground); + --tw-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05); + --tw-shadow-colored: 0 1px 2px 0 var(--tw-shadow-color); + box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); + --tw-backdrop-blur: blur(40px); + backdrop-filter: var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia); + transition-property: all; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + transition-duration: 200ms; +} + +.form-filter:hover { + border-color: rgb(255 255 255 / 0.4); + background-color: rgb(255 255 255 / 0.4); +} + +.form-filter:focus-visible { + border-color: rgb(255 255 255 / 0.5); + outline: 2px solid transparent; + outline-offset: 2px; + --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color); + --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(0px + var(--tw-ring-offset-width)) var(--tw-ring-color); + box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000); +} + +.form-filter:is(.dark *) { + border-color: rgb(255 255 255 / 0.1); + background-color: rgb(0 0 0 / 0.4); +} + +.form-filter:hover:is(.dark *) { + background-color: rgb(0 0 0 / 0.6); +} + +/* Autofill Fix - Glass Compatible */ + +input:-webkit-autofill, + input:-webkit-autofill:hover, + input:-webkit-autofill:focus, + input:-webkit-autofill:active { + -webkit-box-shadow: 0 0 0 30px transparent inset !important; + -webkit-text-fill-color: var(--foreground) !important; + -webkit-transition: background-color 5000s ease-in-out 0s; + transition: background-color 5000s ease-in-out 0s; + caret-color: var(--foreground); +} + +/* Input with Icon Global Style */ + +.input-group { + position: relative; + width: 100%; + color: var(--accents-6); + transition-property: color, background-color, border-color, text-decoration-color, fill, stroke; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + transition-duration: 200ms; +} + +.input-group:focus-within { + color: var(--foreground); +} + +.input-icon { + pointer-events: none; + position: absolute; + top: 0px; + bottom: 0px; + left: 0px; + z-index: 10; + display: flex; + width: 2.5rem; + align-items: center; + justify-content: center; +} + +.input-group .form-input, + .input-group .form-input-search { + padding-left: 2.5rem !important; +} + +.input-suffix { + pointer-events: none; + position: absolute; + top: 0px; + bottom: 0px; + right: 0px; + z-index: 10; + display: flex; + align-items: center; + padding-right: 0.75rem; + color: var(--accents-5); +} + +/* Merged Inputs (Side-by-side) */ + +.input-group-merged > .form-control:not(:first-child), + .input-group-merged > .form-input:not(:first-child), + .input-group-merged > select:not(:first-child) { + margin-left: -1px; + border-top-left-radius: 0px; + border-bottom-left-radius: 0px; +} + +.input-group-merged > .form-control:not(:last-child), + .input-group-merged > .form-input:not(:last-child), + .input-group-merged > select:not(:last-child) { + border-top-right-radius: 0px; + border-bottom-right-radius: 0px; +} + +/* File Input Specific Styling - Custom File Button */ + +.form-control-file { + display: block; + width: 100%; + cursor: pointer; + border-radius: 0.375rem; + border-width: 2px; + border-color: var(--accents-2); + font-size: 0.875rem; + line-height: 1.25rem; + color: var(--accents-5); + transition-property: color, background-color, border-color, text-decoration-color, fill, stroke; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + transition-duration: 150ms; +} + +.form-control-file::file-selector-button { + margin-right: 1rem; + border-radius: 0.375rem; + border-width: 0px; + background-color: var(--accents-2); + padding-top: 0.5rem; + padding-bottom: 0.5rem; + padding-left: 1rem; + padding-right: 1rem; + font-size: 0.75rem; + line-height: 1rem; + font-weight: 600; + color: var(--foreground); +} + +.form-control-file::file-selector-button:hover { + background-color: var(--accents-3); +} + +.checkbox { + height: 1.25rem; + width: 1.25rem; + flex-shrink: 0; + cursor: pointer; + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + border-radius: 0.25rem; + border-width: 1px; + border-color: var(--accents-5); + background-color: rgb(255 255 255 / 0.5); + --tw-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05); + --tw-shadow-colored: 0 1px 2px 0 var(--tw-shadow-color); + box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); + --tw-backdrop-blur: blur(4px); + backdrop-filter: var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia); + transition-property: all; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + transition-duration: 200ms; +} + +.checkbox:checked { + border-color: var(--foreground); + background-color: var(--foreground); +} + +.checkbox:hover { + border-color: var(--foreground); + --tw-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1); + --tw-shadow-colored: 0 4px 6px -1px var(--tw-shadow-color), 0 2px 4px -2px var(--tw-shadow-color); + box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); +} + +.checkbox:focus { + outline: 2px solid transparent; + outline-offset: 2px; + --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color); + --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color); + box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000); + --tw-ring-color: var(--accents-2); +} + +.checkbox:disabled { + cursor: not-allowed; + opacity: 0.5; +} + +.checkbox:is(.dark *) { + background-color: rgb(255 255 255 / 0.05); +} + +.checkbox { + background-position: center; + background-size: 100%; + background-repeat: no-repeat; +} + +.checkbox:checked { + background-image: url("data:image/svg+xml,%3csvg viewBox='0 0 16 16' fill='white' xmlns='http://www.w3.org/2000/svg'%3e%3cpath d='M12.207 4.793a1 1 0 010 1.414l-5 5a1 1 0 01-1.414 0l-2-2a1 1 0 011.414-1.414L6.5 9.086l4.293-4.293a1 1 0 011.414 0z'/%3e%3c/svg%3e"); +} + +.dark .checkbox:checked { + background-image: url("data:image/svg+xml,%3csvg viewBox='0 0 16 16' fill='black' xmlns='http://www.w3.org/2000/svg'%3e%3cpath d='M12.207 4.793a1 1 0 010 1.414l-5 5a1 1 0 01-1.414 0l-2-2a1 1 0 011.414-1.414L6.5 9.086l4.293-4.293a1 1 0 011.414 0z'/%3e%3c/svg%3e"); +} + +.card, .glass-card { + border-radius: 1rem; + border-width: 2px; + border-color: rgb(255 255 255 / 0.2); + background-color: rgb(255 255 255 / 0.25); + padding: 1.5rem; + color: var(--foreground); + --tw-shadow: 0 25px 50px -12px rgb(0 0 0 / 0.25); + --tw-shadow-colored: 0 25px 50px -12px var(--tw-shadow-color); + box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); + --tw-backdrop-blur: blur(40px); + backdrop-filter: var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia); + transition-property: all; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + transition-duration: 500ms; +} + +.card:hover, .glass-card:hover { + --tw-translate-y: -0.25rem; + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); + border-color: rgb(255 255 255 / 0.4); + background-color: rgb(255 255 255 / 0.4); + --tw-shadow-color: rgb(6 182 212 / 0.1); + --tw-shadow: var(--tw-shadow-colored); +} + +.card:is(.dark *), .glass-card:is(.dark *) { + border-color: rgb(255 255 255 / 0.1); + background-color: rgb(0 0 0 / 0.4); +} + +.card:hover:is(.dark *), .glass-card:hover:is(.dark *) { + border-color: rgb(255 255 255 / 0.2); + background-color: rgb(0 0 0 / 0.6); +} + +.sub-card, .card-nested { + border-radius: 0.75rem; + border-width: 2px; + border-color: rgb(255 255 255 / 0.4); + background-color: rgb(255 255 255 / 0.6); + padding: 1rem; + --tw-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1); + --tw-shadow-colored: 0 4px 6px -1px var(--tw-shadow-color), 0 2px 4px -2px var(--tw-shadow-color); + box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); + transition-property: all; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + transition-duration: 300ms; +} + +.sub-card:hover, .card-nested:hover { + --tw-scale-x: 1.01; + --tw-scale-y: 1.01; + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); + border-color: rgb(255 255 255 / 0.6); + background-color: rgb(255 255 255 / 0.8); + --tw-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1); + --tw-shadow-colored: 0 10px 15px -3px var(--tw-shadow-color), 0 4px 6px -4px var(--tw-shadow-color); + box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); +} + +.sub-card:is(.dark *), .card-nested:is(.dark *) { + border-color: rgb(255 255 255 / 0.1); + background-color: rgb(255 255 255 / 0.1); +} + +.sub-card:hover:is(.dark *), .card-nested:hover:is(.dark *) { + background-color: rgb(255 255 255 / 0.15); +} + +/* Custom Select Dropdown Global Style */ + +.custom-select-dropdown { + pointer-events: none; + visibility: hidden; + position: absolute; + z-index: 50; + margin-top: 0.25rem; + display: flex; + max-height: 15rem; + min-width: 100%; + transform-origin: top; + --tw-translate-y: -0.5rem; + --tw-scale-x: .95; + --tw-scale-y: .95; + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); + flex-direction: column; + overflow: hidden; + border-radius: 0.75rem; + border-width: 1px; + border-color: rgb(255 255 255 / 0.2); + background-color: rgb(255 255 255 / 0.8); + opacity: 0; + --tw-shadow: 0 25px 50px -12px rgb(0 0 0 / 0.25); + --tw-shadow-colored: 0 25px 50px -12px var(--tw-shadow-color); + box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); + --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color); + --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color); + box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000); + --tw-ring-color: rgb(0 0 0 / 0.05); + --tw-backdrop-blur: blur(40px); + backdrop-filter: var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia); + transition-property: all; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + transition-duration: 200ms; + transition-timing-function: cubic-bezier(0, 0, 0.2, 1); +} + +.custom-select-dropdown:is(.dark *) { + border-color: rgb(255 255 255 / 0.1); + background-color: rgb(0 0 0 / 0.8); +} + +.custom-select-dropdown.open { + pointer-events: auto; + visibility: visible; + --tw-translate-y: 0px; + --tw-scale-x: 1; + --tw-scale-y: 1; + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); + opacity: 1; +} + +/* Premium Control Pill & Segmented Switch */ + +.control-pill { + display: flex; + height: 2.5rem; + align-items: center; + gap: 0.25rem; + border-radius: 9999px; + border-width: 2px; + border-color: var(--accents-2); + background-color: rgb(255 255 255 / 0.6); + padding-left: 0.375rem; + padding-right: 0.375rem; + --tw-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05); + --tw-shadow-colored: 0 1px 2px 0 var(--tw-shadow-color); + box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); + --tw-backdrop-blur: blur(12px); + backdrop-filter: var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia); + transition-property: all; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + transition-duration: 300ms; +} + +.control-pill:hover { + border-color: var(--accents-3); + background-color: rgb(255 255 255 / 0.8); + --tw-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1); + --tw-shadow-colored: 0 4px 6px -1px var(--tw-shadow-color), 0 2px 4px -2px var(--tw-shadow-color); + box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); +} + +.control-pill:is(.dark *) { + background-color: rgb(0 0 0 / 0.4); +} + +.control-pill:hover:is(.dark *) { + background-color: rgb(0 0 0 / 0.6); +} + +.pill-divider { + margin-left: 0.25rem; + margin-right: 0.25rem; + height: 1rem; + width: 1px; + background-color: var(--accents-3); + opacity: 0.4; +} + +.segmented-switch { + position: relative; + display: flex; + height: 2rem; + width: 3.5rem; + align-items: center; + overflow: hidden; + border-radius: 9999px; + border-width: 1px; + border-color: var(--accents-2); + background-color: var(--accents-2); + padding: 0.125rem; + --tw-shadow: inset 0 2px 4px 0 rgb(0 0 0 / 0.05); + --tw-shadow-colored: inset 0 2px 4px 0 var(--tw-shadow-color); + box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); +} + +.segmented-switch:is(.dark *) { + border-color: rgb(255 255 255 / 0.1); + --tw-bg-opacity: 1; + background-color: rgb(0 0 0 / var(--tw-bg-opacity, 1)); +} + +.segmented-switch-slider { + position: absolute; + left: 0.125rem; + top: 0.125rem; + z-index: 0; + height: 1.75rem; + width: 1.75rem; + border-radius: 9999px; + background-color: var(--foreground); + --tw-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1); + --tw-shadow-colored: 0 4px 6px -1px var(--tw-shadow-color), 0 2px 4px -2px var(--tw-shadow-color); + box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); + transition-property: all; + transition-duration: 300ms; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); +} + +.dark .segmented-switch-slider { + --tw-translate-x: 24px; + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); +} + +.segmented-switch-btn { + position: relative; + z-index: 10; + display: flex; + height: 1.75rem; + width: 1.75rem; + align-items: center; + justify-content: center; + color: var(--accents-5); + transition-property: color, background-color, border-color, text-decoration-color, fill, stroke; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + transition-duration: 300ms; +} + +/* Active icon colors based on theme */ + +/* In Light Mode: Track is light, Slider is Black. We want Active icon to be WHITE on the Slider. */ + +.theme-toggle-light-icon { + --tw-text-opacity: 1; + color: rgb(255 255 255 / var(--tw-text-opacity, 1)); +} + +.dark .theme-toggle-light-icon { + color: var(--accents-5); +} + +.dark .theme-toggle-light-icon:hover { + --tw-text-opacity: 1; + color: rgb(255 255 255 / var(--tw-text-opacity, 1)); +} + +/* In Dark Mode: Track is dark, Slider is White. We want Active icon to be BLACK on the Slider. */ + +.theme-toggle-dark-icon { + color: var(--accents-5); +} + +.theme-toggle-dark-icon:hover { + --tw-text-opacity: 1; + color: rgb(0 0 0 / var(--tw-text-opacity, 1)); +} + +.dark .theme-toggle-dark-icon { + --tw-text-opacity: 1; + color: rgb(0 0 0 / var(--tw-text-opacity, 1)); +} + +.segmented-switch-btn.active { + /* This is a fallback but the specific icons above are better */ + color: var(--background); +} + +/* Language Switcher Pill Button */ + +.pill-lang-btn { + display: flex; + height: 2rem; + width: 2rem; + align-items: center; + justify-content: center; + border-radius: 9999px; + color: var(--foreground); + outline: 2px solid transparent; + outline-offset: 2px; + transition-property: all; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + transition-duration: 200ms; +} + +.pill-lang-btn:hover { + background-color: var(--accents-2); +} + +.pill-lang-btn:focus { + --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color); + --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color); + box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000); + --tw-ring-color: var(--accents-3); +} + +/* Glassmorphism Table */ + +.table-container { + width: 100%; + overflow-x: auto; + border-radius: 0.75rem; + border-width: 2px; + border-color: rgb(0 0 0 / 0.05); + --tw-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05); + --tw-shadow-colored: 0 1px 2px 0 var(--tw-shadow-color); + box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); +} + +.table-container:is(.dark *) { + border-color: rgb(255 255 255 / 0.1); +} + +.table-glass { + width: 100%; + border-collapse: collapse; + text-align: left; + font-size: 0.875rem; + line-height: 1.25rem; +} + +.table-glass thead { + border-bottom-width: 2px; + border-color: rgb(0 0 0 / 0.05); + background-color: rgb(255 255 255 / 0.8); + --tw-backdrop-blur: blur(12px); + backdrop-filter: var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia); +} + +.table-glass thead:is(.dark *) { + border-color: rgb(255 255 255 / 0.1); + background-color: rgb(255 255 255 / 0.05); +} + +.table-glass th { + padding-left: 1.5rem; + padding-right: 1.5rem; + padding-top: 1rem; + padding-bottom: 1rem; + font-size: 0.75rem; + line-height: 1rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.05em; + color: var(--accents-5); +} + +.table-glass tbody > :not([hidden]) ~ :not([hidden]) { + --tw-divide-y-reverse: 0; + border-top-width: calc(1px * calc(1 - var(--tw-divide-y-reverse))); + border-bottom-width: calc(1px * var(--tw-divide-y-reverse)); + border-color: rgb(0 0 0 / 0.05); +} + +.table-glass tbody { + background-color: rgb(255 255 255 / 0.4); + --tw-backdrop-blur: blur(4px); + backdrop-filter: var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia); +} + +.table-glass tbody:is(.dark *) > :not([hidden]) ~ :not([hidden]) { + border-color: rgb(255 255 255 / 0.1); +} + +.table-glass tbody:is(.dark *) { + background-color: rgb(255 255 255 / 0.05); +} + +.table-glass tr { + transition-property: color, background-color, border-color, text-decoration-color, fill, stroke; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + transition-duration: 200ms; +} + +.table-glass tr:hover { + background-color: rgb(255 255 255 / 0.6); +} + +.table-glass tr:hover:is(.dark *) { + background-color: rgb(255 255 255 / 0.1); +} + +.table-glass td { + white-space: nowrap; + padding-left: 1.5rem; + padding-right: 1.5rem; + padding-top: 1rem; + padding-bottom: 1rem; + color: var(--foreground); +} + +/* Global Table Actions Reveal Trigger */ + +.table-actions-reveal { + opacity: 0; + transition-property: opacity; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + transition-duration: 200ms; +} + +.table-glass tr:hover .table-actions-reveal { + opacity: 1; +} + +/* Global Glassmorphism Modal */ + +.modal-glass.open { + --tw-scale-x: 1; + --tw-scale-y: 1; + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); + opacity: 1; +} + +.modal-title { + font-size: 1.125rem; + line-height: 1.75rem; + font-weight: 700; + letter-spacing: -0.025em; + color: var(--foreground); +} + +.sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border-width: 0; +} + +.pointer-events-none { + pointer-events: none; +} + +.pointer-events-auto { + pointer-events: auto; +} + +.visible { + visibility: visible; +} + +.invisible { + visibility: hidden; +} + +.static { + position: static; +} + +.fixed { + position: fixed; +} + +.absolute { + position: absolute; +} + +.relative { + position: relative; +} + +.sticky { + position: sticky; +} + +.-inset-1 { + inset: -0.25rem; +} + +.inset-0 { + inset: 0px; +} + +.inset-x-0 { + left: 0px; + right: 0px; +} + +.inset-y-0 { + top: 0px; + bottom: 0px; +} + +.-bottom-10 { + bottom: -2.5rem; +} + +.-left-10 { + left: -2.5rem; +} + +.-left-\[10\%\] { + left: -10%; +} + +.-right-10 { + right: -2.5rem; +} + +.-right-\[15\%\] { + right: -15%; +} + +.-top-10 { + top: -2.5rem; +} + +.-top-\[20\%\] { + top: -20%; +} + +.bottom-6 { + bottom: 1.5rem; +} + +.left-0 { + left: 0px; +} + +.left-1\/2 { + left: 50%; +} + +.left-3 { + left: 0.75rem; +} + +.right-0 { + right: 0px; +} + +.right-2 { + right: 0.5rem; +} + +.right-3 { + right: 0.75rem; +} + +.right-4 { + right: 1rem; +} + +.right-6 { + right: 1.5rem; +} + +.top-0 { + top: 0px; +} + +.top-1\/2 { + top: 50%; +} + +.top-2 { + top: 0.5rem; +} + +.top-2\.5 { + top: 0.625rem; +} + +.top-4 { + top: 1rem; +} + +.top-6 { + top: 1.5rem; +} + +.top-\[10\%\] { + top: 10%; +} + +.top-\[30\%\] { + top: 30%; +} + +.top-\[64px\] { + top: 64px; +} + +.top-full { + top: 100%; +} + +.z-0 { + z-index: 0; +} + +.z-10 { + z-index: 10; +} + +.z-20 { + z-index: 20; +} + +.z-30 { + z-index: 30; +} + +.z-40 { + z-index: 40; +} + +.z-50 { + z-index: 50; +} + +.col-span-1 { + grid-column: span 1 / span 1; +} + +.col-span-2 { + grid-column: span 2 / span 2; +} + +.col-span-full { + grid-column: 1 / -1; +} + +.-mx-4 { + margin-left: -1rem; + margin-right: -1rem; +} + +.mx-1 { + margin-left: 0.25rem; + margin-right: 0.25rem; +} + +.mx-4 { + margin-left: 1rem; + margin-right: 1rem; +} + +.mx-auto { + margin-left: auto; + margin-right: auto; +} + +.-ml-px { + margin-left: -1px; +} + +.mb-0\.5 { + margin-bottom: 0.125rem; +} + +.mb-1 { + margin-bottom: 0.25rem; +} + +.mb-12 { + margin-bottom: 3rem; +} + +.mb-16 { + margin-bottom: 4rem; +} + +.mb-2 { + margin-bottom: 0.5rem; +} + +.mb-3 { + margin-bottom: 0.75rem; +} + +.mb-4 { + margin-bottom: 1rem; +} + +.mb-6 { + margin-bottom: 1.5rem; +} + +.mb-8 { + margin-bottom: 2rem; +} + +.ml-0\.5 { + margin-left: 0.125rem; +} + +.ml-1 { + margin-left: 0.25rem; +} + +.ml-2 { + margin-left: 0.5rem; +} + +.ml-auto { + margin-left: auto; +} + +.mr-1 { + margin-right: 0.25rem; +} + +.mr-2 { + margin-right: 0.5rem; +} + +.mr-3 { + margin-right: 0.75rem; +} + +.mt-0\.5 { + margin-top: 0.125rem; +} + +.mt-1 { + margin-top: 0.25rem; +} + +.mt-1\.5 { + margin-top: 0.375rem; +} + +.mt-2 { + margin-top: 0.5rem; +} + +.mt-3 { + margin-top: 0.75rem; +} + +.mt-4 { + margin-top: 1rem; +} + +.mt-6 { + margin-top: 1.5rem; +} + +.mt-8 { + margin-top: 2rem; +} + +.mt-auto { + margin-top: auto; +} + +.line-clamp-1 { + overflow: hidden; + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 1; +} + +.block { + display: block; +} + +.inline-block { + display: inline-block; +} + +.inline { + display: inline; +} + +.flex { + display: flex; +} + +.inline-flex { + display: inline-flex; +} + +.table { + display: table; +} + +.grid { + display: grid; +} + +.contents { + display: contents; +} + +.hidden { + display: none; +} + +.aspect-square { + aspect-ratio: 1 / 1; +} + +.aspect-video { + aspect-ratio: 16 / 9; +} + +.\!h-4 { + height: 1rem !important; +} + +.h-1 { + height: 0.25rem; +} + +.h-1\.5 { + height: 0.375rem; +} + +.h-10 { + height: 2.5rem; +} + +.h-11 { + height: 2.75rem; +} + +.h-12 { + height: 3rem; +} + +.h-16 { + height: 4rem; +} + +.h-2 { + height: 0.5rem; +} + +.h-2\.5 { + height: 0.625rem; +} + +.h-20 { + height: 5rem; +} + +.h-24 { + height: 6rem; +} + +.h-3 { + height: 0.75rem; +} + +.h-3\.5 { + height: 0.875rem; +} + +.h-32 { + height: 8rem; +} + +.h-4 { + height: 1rem; +} + +.h-5 { + height: 1.25rem; +} + +.h-6 { + height: 1.5rem; +} + +.h-64 { + height: 16rem; +} + +.h-8 { + height: 2rem; +} + +.h-9 { + height: 2.25rem; +} + +.h-\[400px\] { + height: 400px; +} + +.h-\[500px\] { + height: 500px; +} + +.h-\[60vw\] { + height: 60vw; +} + +.h-\[70vw\] { + height: 70vw; +} + +.h-full { + height: 100%; +} + +.h-screen { + height: 100vh; +} + +.max-h-0 { + max-height: 0px; +} + +.max-h-60 { + max-height: 15rem; +} + +.max-h-\[500px\] { + max-height: 500px; +} + +.max-h-\[80vh\] { + max-height: 80vh; +} + +.max-h-full { + max-height: 100%; +} + +.min-h-0 { + min-height: 0px; +} + +.min-h-\[300px\] { + min-height: 300px; +} + +.min-h-\[500px\] { + min-height: 500px; +} + +.min-h-screen { + min-height: 100vh; +} + +.\!w-4 { + width: 1rem !important; +} + +.w-1 { + width: 0.25rem; +} + +.w-1\.5 { + width: 0.375rem; +} + +.w-10 { + width: 2.5rem; +} + +.w-12 { + width: 3rem; +} + +.w-16 { + width: 4rem; +} + +.w-2 { + width: 0.5rem; +} + +.w-20 { + width: 5rem; +} + +.w-24 { + width: 6rem; +} + +.w-3 { + width: 0.75rem; +} + +.w-3\.5 { + width: 0.875rem; +} + +.w-32 { + width: 8rem; +} + +.w-4 { + width: 1rem; +} + +.w-40 { + width: 10rem; +} + +.w-48 { + width: 12rem; +} + +.w-5 { + width: 1.25rem; +} + +.w-6 { + width: 1.5rem; +} + +.w-64 { + width: 16rem; +} + +.w-8 { + width: 2rem; +} + +.w-9 { + width: 2.25rem; +} + +.w-\[60vw\] { + width: 60vw; +} + +.w-\[70vw\] { + width: 70vw; +} + +.w-\[calc\(100\%-1\.5rem\)\] { + width: calc(100% - 1.5rem); +} + +.w-auto { + width: auto; +} + +.w-fit { + width: -moz-fit-content; + width: fit-content; +} + +.w-full { + width: 100%; +} + +.w-px { + width: 1px; +} + +.min-w-0 { + min-width: 0px; +} + +.min-w-\[300px\] { + min-width: 300px; +} + +.max-w-2xl { + max-width: 42rem; +} + +.max-w-4xl { + max-width: 56rem; +} + +.max-w-5xl { + max-width: 64rem; +} + +.max-w-7xl { + max-width: 80rem; +} + +.max-w-\[140px\] { + max-width: 140px; +} + +.max-w-\[200px\] { + max-width: 200px; +} + +.max-w-full { + max-width: 100%; +} + +.max-w-lg { + max-width: 32rem; +} + +.max-w-md { + max-width: 28rem; +} + +.max-w-none { + max-width: none; +} + +.max-w-sm { + max-width: 24rem; +} + +.flex-1 { + flex: 1 1 0%; +} + +.flex-none { + flex: none; +} + +.\!flex-shrink-0 { + flex-shrink: 0 !important; +} + +.flex-shrink-0 { + flex-shrink: 0; +} + +.shrink-0 { + flex-shrink: 0; +} + +.flex-grow { + flex-grow: 1; +} + +.grow { + flex-grow: 1; +} + +.origin-right { + transform-origin: right; +} + +.origin-top { + transform-origin: top; +} + +.origin-top-left { + transform-origin: top left; +} + +.origin-top-right { + transform-origin: top right; +} + +.-translate-x-1\/2 { + --tw-translate-x: -50%; + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); +} + +.-translate-x-full { + --tw-translate-x: -100%; + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); +} + +.-translate-y-1\/2 { + --tw-translate-y: -50%; + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); +} + +.translate-x-full { + --tw-translate-x: 100%; + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); +} + +.translate-y-20 { + --tw-translate-y: 5rem; + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); +} + +.rotate-180 { + --tw-rotate: 180deg; + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); +} + +.rotate-90 { + --tw-rotate: 90deg; + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); +} + +.scale-100 { + --tw-scale-x: 1; + --tw-scale-y: 1; + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); +} + +.scale-75 { + --tw-scale-x: .75; + --tw-scale-y: .75; + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); +} + +.scale-90 { + --tw-scale-x: .9; + --tw-scale-y: .9; + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); +} + +.scale-95 { + --tw-scale-x: .95; + --tw-scale-y: .95; + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); +} + +.transform { + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); +} + +.animate-\[shimmer_2s_infinite\] { + animation: shimmer 2s infinite; +} + +@keyframes pulse { + 50% { + opacity: .5; + } +} + +.animate-pulse { + animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite; +} + +@keyframes spin { + to { + transform: rotate(360deg); + } +} + +.animate-spin { + animation: spin 1s linear infinite; +} + +.cursor-default { + cursor: default; +} + +.cursor-not-allowed { + cursor: not-allowed; +} + +.cursor-pointer { + cursor: pointer; +} + +.select-none { + -webkit-user-select: none; + -moz-user-select: none; + user-select: none; +} + +.select-all { + -webkit-user-select: all; + -moz-user-select: all; + user-select: all; +} + +.resize-none { + resize: none; +} + +.resize { + resize: both; +} + +.snap-x { + scroll-snap-type: x var(--tw-scroll-snap-strictness); +} + +.snap-start { + scroll-snap-align: start; +} + +.list-inside { + list-style-position: inside; +} + +.list-disc { + list-style-type: disc; +} + +.grid-cols-1 { + grid-template-columns: repeat(1, minmax(0, 1fr)); +} + +.grid-cols-2 { + grid-template-columns: repeat(2, minmax(0, 1fr)); +} + +.grid-cols-3 { + grid-template-columns: repeat(3, minmax(0, 1fr)); +} + +.grid-cols-\[auto_1fr_auto\] { + grid-template-columns: auto 1fr auto; +} + +.flex-row { + flex-direction: row; +} + +.flex-col { + flex-direction: column; +} + +.flex-wrap { + flex-wrap: wrap; +} + +.items-start { + align-items: flex-start; +} + +.items-end { + align-items: flex-end; +} + +.items-center { + align-items: center; +} + +.items-stretch { + align-items: stretch; +} + +.justify-end { + justify-content: flex-end; +} + +.justify-center { + justify-content: center; +} + +.justify-between { + justify-content: space-between; +} + +.gap-1 { + gap: 0.25rem; +} + +.gap-2 { + gap: 0.5rem; +} + +.gap-3 { + gap: 0.75rem; +} + +.gap-4 { + gap: 1rem; +} + +.gap-6 { + gap: 1.5rem; +} + +.gap-8 { + gap: 2rem; +} + +.space-x-2 > :not([hidden]) ~ :not([hidden]) { + --tw-space-x-reverse: 0; + margin-right: calc(0.5rem * var(--tw-space-x-reverse)); + margin-left: calc(0.5rem * calc(1 - var(--tw-space-x-reverse))); +} + +.space-x-4 > :not([hidden]) ~ :not([hidden]) { + --tw-space-x-reverse: 0; + margin-right: calc(1rem * var(--tw-space-x-reverse)); + margin-left: calc(1rem * calc(1 - var(--tw-space-x-reverse))); +} + +.space-y-1 > :not([hidden]) ~ :not([hidden]) { + --tw-space-y-reverse: 0; + margin-top: calc(0.25rem * calc(1 - var(--tw-space-y-reverse))); + margin-bottom: calc(0.25rem * var(--tw-space-y-reverse)); +} + +.space-y-1\.5 > :not([hidden]) ~ :not([hidden]) { + --tw-space-y-reverse: 0; + margin-top: calc(0.375rem * calc(1 - var(--tw-space-y-reverse))); + margin-bottom: calc(0.375rem * var(--tw-space-y-reverse)); +} + +.space-y-16 > :not([hidden]) ~ :not([hidden]) { + --tw-space-y-reverse: 0; + margin-top: calc(4rem * calc(1 - var(--tw-space-y-reverse))); + margin-bottom: calc(4rem * var(--tw-space-y-reverse)); +} + +.space-y-2 > :not([hidden]) ~ :not([hidden]) { + --tw-space-y-reverse: 0; + margin-top: calc(0.5rem * calc(1 - var(--tw-space-y-reverse))); + margin-bottom: calc(0.5rem * var(--tw-space-y-reverse)); +} + +.space-y-3 > :not([hidden]) ~ :not([hidden]) { + --tw-space-y-reverse: 0; + margin-top: calc(0.75rem * calc(1 - var(--tw-space-y-reverse))); + margin-bottom: calc(0.75rem * var(--tw-space-y-reverse)); +} + +.space-y-4 > :not([hidden]) ~ :not([hidden]) { + --tw-space-y-reverse: 0; + margin-top: calc(1rem * calc(1 - var(--tw-space-y-reverse))); + margin-bottom: calc(1rem * var(--tw-space-y-reverse)); +} + +.space-y-5 > :not([hidden]) ~ :not([hidden]) { + --tw-space-y-reverse: 0; + margin-top: calc(1.25rem * calc(1 - var(--tw-space-y-reverse))); + margin-bottom: calc(1.25rem * var(--tw-space-y-reverse)); +} + +.space-y-6 > :not([hidden]) ~ :not([hidden]) { + --tw-space-y-reverse: 0; + margin-top: calc(1.5rem * calc(1 - var(--tw-space-y-reverse))); + margin-bottom: calc(1.5rem * var(--tw-space-y-reverse)); +} + +.space-y-8 > :not([hidden]) ~ :not([hidden]) { + --tw-space-y-reverse: 0; + margin-top: calc(2rem * calc(1 - var(--tw-space-y-reverse))); + margin-bottom: calc(2rem * var(--tw-space-y-reverse)); +} + +.divide-y > :not([hidden]) ~ :not([hidden]) { + --tw-divide-y-reverse: 0; + border-top-width: calc(1px * calc(1 - var(--tw-divide-y-reverse))); + border-bottom-width: calc(1px * var(--tw-divide-y-reverse)); +} + +.divide-white\/10 > :not([hidden]) ~ :not([hidden]) { + border-color: rgb(255 255 255 / 0.1); +} + +.self-end { + align-self: flex-end; +} + +.overflow-auto { + overflow: auto; +} + +.overflow-hidden { + overflow: hidden; +} + +.overflow-x-auto { + overflow-x: auto; +} + +.overflow-y-auto { + overflow-y: auto; +} + +.overflow-x-hidden { + overflow-x: hidden; +} + +.truncate { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.whitespace-normal { + white-space: normal; +} + +.whitespace-nowrap { + white-space: nowrap; +} + +.whitespace-pre { + white-space: pre; +} + +.whitespace-pre-wrap { + white-space: pre-wrap; +} + +.break-normal { + overflow-wrap: normal; + word-break: normal; +} + +.break-words { + overflow-wrap: break-word; +} + +.break-all { + word-break: break-all; +} + +.rounded { + border-radius: 0.25rem; +} + +.rounded-2xl { + border-radius: 1rem; +} + +.rounded-full { + border-radius: 9999px; +} + +.rounded-lg { + border-radius: 0.5rem; +} + +.rounded-md { + border-radius: 0.375rem; +} + +.rounded-none { + border-radius: 0px; +} + +.rounded-sm { + border-radius: 0.125rem; +} + +.rounded-xl { + border-radius: 0.75rem; +} + +.rounded-l-none { + border-top-left-radius: 0px; + border-bottom-left-radius: 0px; +} + +.rounded-r-none { + border-top-right-radius: 0px; + border-bottom-right-radius: 0px; +} + +.rounded-t-md { + border-top-left-radius: 0.375rem; + border-top-right-radius: 0.375rem; +} + +.border { + border-width: 1px; +} + +.border-0 { + border-width: 0px; +} + +.border-2 { + border-width: 2px; +} + +.border-b { + border-bottom-width: 1px; +} + +.border-b-2 { + border-bottom-width: 2px; +} + +.border-l-0 { + border-left-width: 0px; +} + +.border-r { + border-right-width: 1px; +} + +.border-r-0 { + border-right-width: 0px; +} + +.border-t { + border-top-width: 1px; +} + +.border-dashed { + border-style: dashed; +} + +.border-none { + border-style: none; +} + +.\!border-red-500\/30 { + border-color: rgb(239 68 68 / 0.3) !important; +} + +.border-\[\#30363d\] { + --tw-border-opacity: 1; + border-color: rgb(48 54 61 / var(--tw-border-opacity, 1)); +} + +.border-accents-1 { + border-color: var(--accents-1); +} + +.border-accents-2 { + border-color: var(--accents-2); +} + +.border-accents-3 { + border-color: var(--accents-3); +} + +.border-blue-200 { + --tw-border-opacity: 1; + border-color: rgb(191 219 254 / var(--tw-border-opacity, 1)); +} + +.border-blue-500\/20 { + border-color: rgb(59 130 246 / 0.2); +} + +.border-emerald-200 { + --tw-border-opacity: 1; + border-color: rgb(167 243 208 / var(--tw-border-opacity, 1)); +} + +.border-green-200 { + --tw-border-opacity: 1; + border-color: rgb(187 247 208 / var(--tw-border-opacity, 1)); +} + +.border-green-500\/20 { + border-color: rgb(34 197 94 / 0.2); +} + +.border-orange-200 { + --tw-border-opacity: 1; + border-color: rgb(254 215 170 / var(--tw-border-opacity, 1)); +} + +.border-red-200 { + --tw-border-opacity: 1; + border-color: rgb(254 202 202 / var(--tw-border-opacity, 1)); +} + +.border-red-500 { + --tw-border-opacity: 1; + border-color: rgb(239 68 68 / var(--tw-border-opacity, 1)); +} + +.border-slate-200 { + --tw-border-opacity: 1; + border-color: rgb(226 232 240 / var(--tw-border-opacity, 1)); +} + +.border-transparent { + border-color: transparent; +} + +.border-white\/10 { + border-color: rgb(255 255 255 / 0.1); +} + +.border-white\/20 { + border-color: rgb(255 255 255 / 0.2); +} + +.\!bg-red-50\/50 { + background-color: rgb(254 242 242 / 0.5) !important; +} + +.bg-\[\#0d1117\] { + --tw-bg-opacity: 1; + background-color: rgb(13 17 23 / var(--tw-bg-opacity, 1)); +} + +.bg-accents-1 { + background-color: var(--accents-1); +} + +.bg-accents-2 { + background-color: var(--accents-2); +} + +.bg-accents-3 { + background-color: var(--accents-3); +} + +.bg-amber-500 { + --tw-bg-opacity: 1; + background-color: rgb(245 158 11 / var(--tw-bg-opacity, 1)); +} + +.bg-background { + background-color: var(--background); +} + +.bg-black\/50 { + background-color: rgb(0 0 0 / 0.5); +} + +.bg-black\/60 { + background-color: rgb(0 0 0 / 0.6); +} + +.bg-blue-100 { + --tw-bg-opacity: 1; + background-color: rgb(219 234 254 / var(--tw-bg-opacity, 1)); +} + +.bg-blue-50 { + --tw-bg-opacity: 1; + background-color: rgb(239 246 255 / var(--tw-bg-opacity, 1)); +} + +.bg-blue-500 { + --tw-bg-opacity: 1; + background-color: rgb(59 130 246 / var(--tw-bg-opacity, 1)); +} + +.bg-blue-500\/10 { + background-color: rgb(59 130 246 / 0.1); +} + +.bg-blue-500\/20 { + background-color: rgb(59 130 246 / 0.2); +} + +.bg-blue-600 { + --tw-bg-opacity: 1; + background-color: rgb(37 99 235 / var(--tw-bg-opacity, 1)); +} + +.bg-emerald-500 { + --tw-bg-opacity: 1; + background-color: rgb(16 185 129 / var(--tw-bg-opacity, 1)); +} + +.bg-emerald-500\/10 { + background-color: rgb(16 185 129 / 0.1); +} + +.bg-foreground { + background-color: var(--foreground); +} + +.bg-gray-200 { + --tw-bg-opacity: 1; + background-color: rgb(229 231 235 / var(--tw-bg-opacity, 1)); +} + +.bg-gray-800 { + --tw-bg-opacity: 1; + background-color: rgb(31 41 55 / var(--tw-bg-opacity, 1)); +} + +.bg-green-100 { + --tw-bg-opacity: 1; + background-color: rgb(220 252 231 / var(--tw-bg-opacity, 1)); +} + +.bg-green-500 { + --tw-bg-opacity: 1; + background-color: rgb(34 197 94 / var(--tw-bg-opacity, 1)); +} + +.bg-green-500\/10 { + background-color: rgb(34 197 94 / 0.1); +} + +.bg-green-600 { + --tw-bg-opacity: 1; + background-color: rgb(22 163 74 / var(--tw-bg-opacity, 1)); +} + +.bg-indigo-500 { + --tw-bg-opacity: 1; + background-color: rgb(99 102 241 / var(--tw-bg-opacity, 1)); +} + +.bg-orange-500\/10 { + background-color: rgb(249 115 22 / 0.1); +} + +.bg-pink-500 { + --tw-bg-opacity: 1; + background-color: rgb(236 72 153 / var(--tw-bg-opacity, 1)); +} + +.bg-purple-100 { + --tw-bg-opacity: 1; + background-color: rgb(243 232 255 / var(--tw-bg-opacity, 1)); +} + +.bg-purple-500 { + --tw-bg-opacity: 1; + background-color: rgb(168 85 247 / var(--tw-bg-opacity, 1)); +} + +.bg-purple-500\/20 { + background-color: rgb(168 85 247 / 0.2); +} + +.bg-red-100 { + --tw-bg-opacity: 1; + background-color: rgb(254 226 226 / var(--tw-bg-opacity, 1)); +} + +.bg-red-50 { + --tw-bg-opacity: 1; + background-color: rgb(254 242 242 / var(--tw-bg-opacity, 1)); +} + +.bg-red-500 { + --tw-bg-opacity: 1; + background-color: rgb(239 68 68 / var(--tw-bg-opacity, 1)); +} + +.bg-red-500\/10 { + background-color: rgb(239 68 68 / 0.1); +} + +.bg-red-500\/20 { + background-color: rgb(239 68 68 / 0.2); +} + +.bg-red-600 { + --tw-bg-opacity: 1; + background-color: rgb(220 38 38 / var(--tw-bg-opacity, 1)); +} + +.bg-slate-500\/10 { + background-color: rgb(100 116 139 / 0.1); +} + +.bg-transparent { + background-color: transparent; +} + +.bg-white { + --tw-bg-opacity: 1; + background-color: rgb(255 255 255 / var(--tw-bg-opacity, 1)); +} + +.bg-white\/10 { + background-color: rgb(255 255 255 / 0.1); +} + +.bg-white\/20 { + background-color: rgb(255 255 255 / 0.2); +} + +.bg-white\/30 { + background-color: rgb(255 255 255 / 0.3); +} + +.bg-white\/40 { + background-color: rgb(255 255 255 / 0.4); +} + +.bg-white\/5 { + background-color: rgb(255 255 255 / 0.05); +} + +.bg-white\/50 { + background-color: rgb(255 255 255 / 0.5); +} + +.bg-yellow-500 { + --tw-bg-opacity: 1; + background-color: rgb(234 179 8 / var(--tw-bg-opacity, 1)); +} + +.bg-zinc-900\/50 { + background-color: rgb(24 24 27 / 0.5); +} + +.bg-opacity-80 { + --tw-bg-opacity: 0.8; +} + +.bg-\[url\(\'data\:image\/svg\+xml\;base64\2c PHN2ZyB3aWR0aD0iMjAiIGhlaWdodD0iMjAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI\+PGNpcmNsZSBjeD0iMSIgY3k9IjEiIHI9IjEiIGZpbGw9InJnYmEoMCwwLDAsMC4zKSIvPjwvc3ZnPg\=\=\'\)\] { + background-image: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAiIGhlaWdodD0iMjAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGNpcmNsZSBjeD0iMSIgY3k9IjEiIHI9IjEiIGZpbGw9InJnYmEoMCwwLDAsMC4zKSIvPjwvc3ZnPg=='); +} + +.bg-gradient-to-r { + background-image: linear-gradient(to right, var(--tw-gradient-stops)); +} + +.from-blue-500 { + --tw-gradient-from: #3b82f6 var(--tw-gradient-from-position); + --tw-gradient-to: rgb(59 130 246 / 0) var(--tw-gradient-to-position); + --tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to); +} + +.to-indigo-600 { + --tw-gradient-to: #4f46e5 var(--tw-gradient-to-position); +} + +.to-purple-600 { + --tw-gradient-to: #9333ea var(--tw-gradient-to-position); +} + +.fill-current { + fill: currentColor; +} + +.object-contain { + -o-object-fit: contain; + object-fit: contain; +} + +.\!p-0 { + padding: 0px !important; +} + +.p-0 { + padding: 0px; +} + +.p-1 { + padding: 0.25rem; +} + +.p-1\.5 { + padding: 0.375rem; +} + +.p-12 { + padding: 3rem; +} + +.p-2 { + padding: 0.5rem; +} + +.p-3 { + padding: 0.75rem; +} + +.p-4 { + padding: 1rem; +} + +.p-5 { + padding: 1.25rem; +} + +.p-6 { + padding: 1.5rem; +} + +.p-8 { + padding: 2rem; +} + +.px-1 { + padding-left: 0.25rem; + padding-right: 0.25rem; +} + +.px-2 { + padding-left: 0.5rem; + padding-right: 0.5rem; +} + +.px-2\.5 { + padding-left: 0.625rem; + padding-right: 0.625rem; +} + +.px-3 { + padding-left: 0.75rem; + padding-right: 0.75rem; +} + +.px-4 { + padding-left: 1rem; + padding-right: 1rem; +} + +.px-6 { + padding-left: 1.5rem; + padding-right: 1.5rem; +} + +.px-8 { + padding-left: 2rem; + padding-right: 2rem; +} + +.py-0\.5 { + padding-top: 0.125rem; + padding-bottom: 0.125rem; +} + +.py-1 { + padding-top: 0.25rem; + padding-bottom: 0.25rem; +} + +.py-1\.5 { + padding-top: 0.375rem; + padding-bottom: 0.375rem; +} + +.py-12 { + padding-top: 3rem; + padding-bottom: 3rem; +} + +.py-16 { + padding-top: 4rem; + padding-bottom: 4rem; +} + +.py-2 { + padding-top: 0.5rem; + padding-bottom: 0.5rem; +} + +.py-2\.5 { + padding-top: 0.625rem; + padding-bottom: 0.625rem; +} + +.py-3 { + padding-top: 0.75rem; + padding-bottom: 0.75rem; +} + +.py-4 { + padding-top: 1rem; + padding-bottom: 1rem; +} + +.py-5 { + padding-top: 1.25rem; + padding-bottom: 1.25rem; +} + +.py-6 { + padding-top: 1.5rem; + padding-bottom: 1.5rem; +} + +.py-8 { + padding-top: 2rem; + padding-bottom: 2rem; +} + +.pb-1 { + padding-bottom: 0.25rem; +} + +.pb-2 { + padding-bottom: 0.5rem; +} + +.pb-4 { + padding-bottom: 1rem; +} + +.pb-5 { + padding-bottom: 1.25rem; +} + +.pb-6 { + padding-bottom: 1.5rem; +} + +.pl-10 { + padding-left: 2.5rem; +} + +.pl-3 { + padding-left: 0.75rem; +} + +.pl-9 { + padding-left: 2.25rem; +} + +.pr-10 { + padding-right: 2.5rem; +} + +.pr-16 { + padding-right: 4rem; +} + +.pr-3 { + padding-right: 0.75rem; +} + +.pr-8 { + padding-right: 2rem; +} + +.pt-2 { + padding-top: 0.5rem; +} + +.pt-4 { + padding-top: 1rem; +} + +.pt-6 { + padding-top: 1.5rem; +} + +.text-left { + text-align: left; +} + +.text-center { + text-align: center; +} + +.text-right { + text-align: right; +} + +.align-middle { + vertical-align: middle; +} + +.font-mono { + font-family: Geist Mono, monospace; +} + +.font-sans { + font-family: Geist, sans-serif; +} + +.text-2xl { + font-size: 1.5rem; + line-height: 2rem; +} + +.text-3xl { + font-size: 1.875rem; + line-height: 2.25rem; +} + +.text-4xl { + font-size: 2.25rem; + line-height: 2.5rem; +} + +.text-6xl { + font-size: 3.75rem; + line-height: 1; +} + +.text-\[10px\] { + font-size: 10px; +} + +.text-base { + font-size: 1rem; + line-height: 1.5rem; +} + +.text-lg { + font-size: 1.125rem; + line-height: 1.75rem; +} + +.text-sm { + font-size: 0.875rem; + line-height: 1.25rem; +} + +.text-xl { + font-size: 1.25rem; + line-height: 1.75rem; +} + +.text-xs { + font-size: 0.75rem; + line-height: 1rem; +} + +.font-black { + font-weight: 900; +} + +.font-bold { + font-weight: 700; +} + +.font-extrabold { + font-weight: 800; +} + +.font-medium { + font-weight: 500; +} + +.font-semibold { + font-weight: 600; +} + +.uppercase { + text-transform: uppercase; +} + +.italic { + font-style: italic; +} + +.leading-6 { + line-height: 1.5rem; +} + +.leading-none { + line-height: 1; +} + +.leading-relaxed { + line-height: 1.625; +} + +.leading-tight { + line-height: 1.25; +} + +.tracking-\[0\.15em\] { + letter-spacing: 0.15em; +} + +.tracking-tight { + letter-spacing: -0.025em; +} + +.tracking-tighter { + letter-spacing: -0.05em; +} + +.tracking-wide { + letter-spacing: 0.025em; +} + +.tracking-wider { + letter-spacing: 0.05em; +} + +.tracking-widest { + letter-spacing: 0.1em; +} + +.\!text-accents-6 { + color: var(--accents-6) !important; +} + +.\!text-black { + --tw-text-opacity: 1 !important; + color: rgb(0 0 0 / var(--tw-text-opacity, 1)) !important; +} + +.text-accents-2 { + color: var(--accents-2); +} + +.text-accents-3 { + color: var(--accents-3); +} + +.text-accents-4 { + color: var(--accents-4); +} + +.text-accents-5 { + color: var(--accents-5); +} + +.text-accents-6 { + color: var(--accents-6); +} + +.text-accents-7 { + color: var(--accents-7); +} + +.text-accents-8 { + color: var(--accents-8); +} + +.text-background { + color: var(--background); +} + +.text-black { + --tw-text-opacity: 1; + color: rgb(0 0 0 / var(--tw-text-opacity, 1)); +} + +.text-blue-500 { + --tw-text-opacity: 1; + color: rgb(59 130 246 / var(--tw-text-opacity, 1)); +} + +.text-blue-600 { + --tw-text-opacity: 1; + color: rgb(37 99 235 / var(--tw-text-opacity, 1)); +} + +.text-blue-800 { + --tw-text-opacity: 1; + color: rgb(30 64 175 / var(--tw-text-opacity, 1)); +} + +.text-emerald-600 { + --tw-text-opacity: 1; + color: rgb(5 150 105 / var(--tw-text-opacity, 1)); +} + +.text-foreground { + color: var(--foreground); +} + +.text-gray-300 { + --tw-text-opacity: 1; + color: rgb(209 213 219 / var(--tw-text-opacity, 1)); +} + +.text-gray-500 { + --tw-text-opacity: 1; + color: rgb(107 114 128 / var(--tw-text-opacity, 1)); +} + +.text-gray-600 { + --tw-text-opacity: 1; + color: rgb(75 85 99 / var(--tw-text-opacity, 1)); +} + +.text-gray-800 { + --tw-text-opacity: 1; + color: rgb(31 41 55 / var(--tw-text-opacity, 1)); +} + +.text-green-500 { + --tw-text-opacity: 1; + color: rgb(34 197 94 / var(--tw-text-opacity, 1)); +} + +.text-green-600 { + --tw-text-opacity: 1; + color: rgb(22 163 74 / var(--tw-text-opacity, 1)); +} + +.text-green-700 { + --tw-text-opacity: 1; + color: rgb(21 128 61 / var(--tw-text-opacity, 1)); +} + +.text-green-800 { + --tw-text-opacity: 1; + color: rgb(22 101 52 / var(--tw-text-opacity, 1)); +} + +.text-orange-500 { + --tw-text-opacity: 1; + color: rgb(249 115 22 / var(--tw-text-opacity, 1)); +} + +.text-orange-600 { + --tw-text-opacity: 1; + color: rgb(234 88 12 / var(--tw-text-opacity, 1)); +} + +.text-purple-500 { + --tw-text-opacity: 1; + color: rgb(168 85 247 / var(--tw-text-opacity, 1)); +} + +.text-purple-600 { + --tw-text-opacity: 1; + color: rgb(147 51 234 / var(--tw-text-opacity, 1)); +} + +.text-red-400 { + --tw-text-opacity: 1; + color: rgb(248 113 113 / var(--tw-text-opacity, 1)); +} + +.text-red-500 { + --tw-text-opacity: 1; + color: rgb(239 68 68 / var(--tw-text-opacity, 1)); +} + +.text-red-600 { + --tw-text-opacity: 1; + color: rgb(220 38 38 / var(--tw-text-opacity, 1)); +} + +.text-red-800 { + --tw-text-opacity: 1; + color: rgb(153 27 27 / var(--tw-text-opacity, 1)); +} + +.text-slate-600 { + --tw-text-opacity: 1; + color: rgb(71 85 105 / var(--tw-text-opacity, 1)); +} + +.text-success { + --tw-text-opacity: 1; + color: rgb(0 112 243 / var(--tw-text-opacity, 1)); +} + +.text-warning { + --tw-text-opacity: 1; + color: rgb(245 166 35 / var(--tw-text-opacity, 1)); +} + +.text-white { + --tw-text-opacity: 1; + color: rgb(255 255 255 / var(--tw-text-opacity, 1)); +} + +.text-yellow-500 { + --tw-text-opacity: 1; + color: rgb(234 179 8 / var(--tw-text-opacity, 1)); +} + +.underline { + text-decoration-line: underline; +} + +.decoration-0 { + text-decoration-thickness: 0px; +} + +.antialiased { + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +.opacity-0 { + opacity: 0; +} + +.opacity-100 { + opacity: 1; +} + +.opacity-25 { + opacity: 0.25; +} + +.opacity-50 { + opacity: 0.5; +} + +.opacity-60 { + opacity: 0.6; +} + +.opacity-70 { + opacity: 0.7; +} + +.opacity-80 { + opacity: 0.8; +} + +.shadow-2xl { + --tw-shadow: 0 25px 50px -12px rgb(0 0 0 / 0.25); + --tw-shadow-colored: 0 25px 50px -12px var(--tw-shadow-color); + box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); +} + +.shadow-inner { + --tw-shadow: inset 0 2px 4px 0 rgb(0 0 0 / 0.05); + --tw-shadow-colored: inset 0 2px 4px 0 var(--tw-shadow-color); + box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); +} + +.shadow-lg { + --tw-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1); + --tw-shadow-colored: 0 10px 15px -3px var(--tw-shadow-color), 0 4px 6px -4px var(--tw-shadow-color); + box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); +} + +.shadow-sm { + --tw-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05); + --tw-shadow-colored: 0 1px 2px 0 var(--tw-shadow-color); + box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); +} + +.shadow-xl { + --tw-shadow: 0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1); + --tw-shadow-colored: 0 20px 25px -5px var(--tw-shadow-color), 0 8px 10px -6px var(--tw-shadow-color); + box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); +} + +.outline { + outline-style: solid; +} + +.ring-1 { + --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color); + --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color); + box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000); +} + +.ring-black\/5 { + --tw-ring-color: rgb(0 0 0 / 0.05); +} + +.ring-foreground { + --tw-ring-color: var(--foreground); +} + +.ring-red-200 { + --tw-ring-opacity: 1; + --tw-ring-color: rgb(254 202 202 / var(--tw-ring-opacity, 1)); +} + +.ring-white\/10 { + --tw-ring-color: rgb(255 255 255 / 0.1); +} + +.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); +} + +.blur-3xl { + --tw-blur: blur(64px); + 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); +} + +.blur-\[100px\] { + --tw-blur: blur(100px); + 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); +} + +.blur-\[120px\] { + --tw-blur: blur(120px); + 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); +} + +.filter { + 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); +} + +.backdrop-blur { + --tw-backdrop-blur: blur(8px); + backdrop-filter: var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia); +} + +.backdrop-blur-\[40px\] { + --tw-backdrop-blur: blur(40px); + backdrop-filter: var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia); +} + +.backdrop-blur-md { + --tw-backdrop-blur: blur(12px); + backdrop-filter: var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia); +} + +.backdrop-blur-sm { + --tw-backdrop-blur: blur(4px); + backdrop-filter: var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia); +} + +.backdrop-blur-xl { + --tw-backdrop-blur: blur(24px); + backdrop-filter: var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia); +} + +.backdrop-filter { + backdrop-filter: var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia); +} + +.transition { + transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + transition-duration: 150ms; +} + +.transition-\[max-height\] { + transition-property: max-height; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + transition-duration: 150ms; +} + +.transition-all { + transition-property: all; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + transition-duration: 150ms; +} + +.transition-colors { + transition-property: color, background-color, border-color, text-decoration-color, fill, stroke; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + transition-duration: 150ms; +} + +.transition-opacity { + transition-property: opacity; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + transition-duration: 150ms; +} + +.transition-shadow { + transition-property: box-shadow; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + transition-duration: 150ms; +} + +.transition-transform { + transition-property: transform; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + transition-duration: 150ms; +} + +.duration-1000 { + transition-duration: 1000ms; +} + +.duration-200 { + transition-duration: 200ms; +} + +.duration-300 { + transition-duration: 300ms; +} + +.duration-500 { + transition-duration: 500ms; +} + +.ease-in { + transition-timing-function: cubic-bezier(0.4, 0, 1, 1); +} + +.ease-in-out { + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); +} + +.ease-out { + transition-timing-function: cubic-bezier(0, 0, 0.2, 1); +} + +/* Custom Scrollbar - Vercel Style */ + +::-webkit-scrollbar { + width: 6px; + height: 6px; +} + +::-webkit-scrollbar-track { + background: transparent; +} + +::-webkit-scrollbar-thumb { + border-radius: 9999px; + background-color: var(--accents-3); + -webkit-transition-property: color, background-color, border-color, text-decoration-color, fill, stroke; + transition-property: color, background-color, border-color, text-decoration-color, fill, stroke; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + transition-duration: 150ms; +} + +::-webkit-scrollbar-thumb:hover { + background-color: var(--accents-4); +} + +/* Ensure styles apply to specific containers if needed */ + +.scrollbar-hide::-webkit-scrollbar { + display: none; +} + +.scrollbar-hide { + -ms-overflow-style: none; + scrollbar-width: none; +} + +/* Mobile only scrollbar hide */ + +.\[mask-image\:linear-gradient\(to_bottom\2c white\2c transparent\)\] { + -webkit-mask-image: linear-gradient(to bottom,white,transparent); + mask-image: linear-gradient(to bottom,white,transparent); +} + +html { + overflow-y: scroll; + scrollbar-gutter: stable; +} + +/* Premium SweetAlert2 Overrides - Glassmorphism Style */ + +div.swal2-popup { + background-color: rgba(255, 255, 255, 0.5) !important; + /* High transparency for Maximum Glossy Light Mode */ + backdrop-filter: blur(16px) !important; + -webkit-backdrop-filter: blur(16px) !important; + color: var(--foreground) !important; + border: 1px solid rgba(255, 255, 255, 0.4) !important; + /* Subtle reflective border */ + border-radius: 0.75rem !important; + /* rounded-xl */ + box-shadow: 0 10px 30px -5px rgba(0, 0, 0, 0.1), 0 8px 10px -6px rgba(0, 0, 0, 0.05) !important; + /* Softer shadow */ + padding: 1.5rem !important; +} + +/* Dark mode background fix for glassmorphism */ + +.dark div.swal2-popup { + background-color: rgba(0, 0, 0, 0.75) !important; + border-color: rgba(255, 255, 255, 0.1) !important; +} + +div.swal2-title { + color: var(--foreground) !important; + font-family: inherit !important; + font-size: 1.25rem !important; + font-weight: 600 !important; + margin-top: 1rem !important; +} + +div.swal2-html-container { + color: var(--accents-5) !important; + font-family: inherit !important; + font-size: 0.875rem !important; + margin-top: 0.5rem !important; +} + +/* Hide default icon styling to make way for Lucide */ + +div.swal2-icon { + border: none !important; + background: transparent !important; + margin: 0 auto !important; +} + +/* Remove default animations/styles for icons */ + +.swal2-icon-content { + display: flex !important; + align-items: center; + justify-content: center; +} + +/* Target actions container for flex gap */ + +div.swal2-actions { + gap: 1rem !important; +} + +div.swal2-confirm, +div.swal2-cancel { + margin: 0px !important; + /* Reset margins to use flex gap */ +} + +div.swal2-confirm { + display: inline-flex; + height: 2.25rem; + align-items: center; + justify-content: center; + border-radius: 0.375rem; + padding-left: 1rem; + padding-right: 1rem; + padding-top: 0.5rem; + padding-bottom: 0.5rem; + font-size: 0.875rem; + line-height: 1.25rem; + font-weight: 500; + transition-property: all; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + transition-duration: 200ms; +} + +div.swal2-confirm:hover { + --tw-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1); + --tw-shadow-colored: 0 4px 6px -1px var(--tw-shadow-color), 0 2px 4px -2px var(--tw-shadow-color); + box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); +} + +div.swal2-confirm:focus-visible { + outline: 2px solid transparent; + outline-offset: 2px; + --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color); + --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color); + box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000); + --tw-ring-color: var(--foreground); +} + +div.swal2-confirm:active { + --tw-scale-x: .95; + --tw-scale-y: .95; + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); +} + +div.swal2-confirm:disabled { + pointer-events: none; + opacity: 0.5; +} + +div.swal2-confirm { + background-color: var(--foreground); + color: var(--background); +} + +div.swal2-confirm:hover { + --tw-translate-y: -0.125rem; + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); + opacity: 0.9; +} + +div.swal2-confirm { + border-radius: 0.5rem !important; +} + +div.swal2-cancel { + display: inline-flex; + height: 2.25rem; + align-items: center; + justify-content: center; + border-radius: 0.375rem; + padding-left: 1rem; + padding-right: 1rem; + padding-top: 0.5rem; + padding-bottom: 0.5rem; + font-size: 0.875rem; + line-height: 1.25rem; + font-weight: 500; + transition-property: all; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + transition-duration: 200ms; +} + +div.swal2-cancel:hover { + --tw-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1); + --tw-shadow-colored: 0 4px 6px -1px var(--tw-shadow-color), 0 2px 4px -2px var(--tw-shadow-color); + box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); +} + +div.swal2-cancel:focus-visible { + outline: 2px solid transparent; + outline-offset: 2px; + --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color); + --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color); + box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000); + --tw-ring-color: var(--foreground); +} + +div.swal2-cancel:active { + --tw-scale-x: .95; + --tw-scale-y: .95; + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); +} + +div.swal2-cancel:disabled { + pointer-events: none; + opacity: 0.5; +} + +div.swal2-cancel { + border-width: 1px; + border-color: var(--accents-2); + background-color: var(--background); + color: var(--foreground); +} + +div.swal2-cancel:hover { + --tw-translate-y: -0.125rem; + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); + border-color: var(--foreground); + background-color: var(--accents-1); +} + +div.swal2-cancel { + border-radius: 0.5rem !important; +} + +/* Dark Mode Specific Adjustments */ + +.dark div.swal2-popup { + border-color: #333 !important; +} + +/* Custom Icon Colors (applied to Lucide wrapper) */ + +.text-success { + color: #10b981; +} + +.text-error { + color: #ef4444; +} + +.text-warning { + color: #f59e0b; +} + +.text-info { + color: #3b82f6; +} + +.text-question { + color: #8b5cf6; +} + +/* Custom Premium Stacking Toasts */ + +#mivo-toast-container { + position: fixed; + bottom: 1.5rem; + right: 1.5rem; + z-index: 9999; + display: flex; + flex-direction: column-reverse; + gap: 0.75rem; + pointer-events: none; + max-width: 400px; + width: calc(100% - 3rem); +} + +.mivo-toast { + pointer-events: auto; + background-color: rgba(255, 255, 255, 0.6); + backdrop-filter: blur(12px); + -webkit-backdrop-filter: blur(12px); + border: 1px solid rgba(255, 255, 255, 0.4); + border-radius: 0.75rem; + box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05); + padding: 1rem; + display: flex; + align-items: center; + gap: 0.75rem; + animation: toast-slide-in 0.3s cubic-bezier(0.16, 1, 0.3, 1); + transition: all 0.3s ease; + position: relative; + overflow: hidden; +} + +.dark .mivo-toast { + background-color: rgba(0, 0, 0, 0.7); + border-color: rgba(255, 255, 255, 0.1); +} + +.mivo-toast-content { + flex: 1; + min-width: 0; +} + +.mivo-toast-title { + font-size: 0.875rem; + font-weight: 600; + color: var(--foreground); + line-height: 1.25; +} + +.mivo-toast-message { + font-size: 0.75rem; + color: var(--accents-5); + margin-top: 0.125rem; +} + +.mivo-toast-close { + padding: 0.25rem; + margin: -0.25rem; + border-radius: 0.375rem; + color: var(--accents-4); + transition: all 0.2s; + cursor: pointer; +} + +.mivo-toast-close:hover { + background-color: var(--accents-2); + color: var(--foreground); +} + +.mivo-toast-progress { + position: absolute; + bottom: 0; + left: 0; + height: 2px; + background-color: currentColor; + opacity: 0.2; + width: 0%; +} + +@keyframes toast-slide-in { + from { + transform: translateX(100%) scale(0.9); + opacity: 0; + } + + to { + transform: translateX(0) scale(1); + opacity: 1; + } +} + +.mivo-toast-fade-out { + transform: translateX(100%); + opacity: 0; +} + +/* SweetAlert Premium Fixes */ + +div:where(.swal2-icon).swal2-success { + border-color: #10B981 !important; + color: #10B981 !important; +} + +div:where(.swal2-icon).swal2-success .swal2-success-ring { + border-color: rgba(16, 185, 129, 0.4) !important; + /* Emphasize ring slightly */ +} + +div:where(.swal2-icon).swal2-success .swal2-success-circular-line-left, +div:where(.swal2-icon).swal2-success .swal2-success-circular-line-right, +div:where(.swal2-icon).swal2-success .swal2-success-fix { + background-color: transparent !important; +} + +div:where(.swal2-icon).swal2-success .swal2-success-line-tip, +div:where(.swal2-icon).swal2-success .swal2-success-line-long { + background-color: #10B981 !important; +} + +div:where(.swal2-icon).swal2-error { + border-color: #EF4444 !important; + color: #EF4444 !important; +} + +div:where(.swal2-container) div:where(.swal2-popup).swal2-premium-card { + background: rgba(255, 255, 255, 0.25) !important; + backdrop-filter: blur(40px) !important; + border: 1px solid rgba(255, 255, 255, 0.2) !important; + box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25) !important; +} + +.dark div:where(.swal2-container) div:where(.swal2-popup).swal2-premium-card { + background: rgba(0, 0, 0, 0.4) !important; + border-color: rgba(255, 255, 255, 0.1) !important; +} + +.selection\:bg-accents-2 *::-moz-selection { + background-color: var(--accents-2); +} + +.selection\:bg-accents-2 *::selection { + background-color: var(--accents-2); +} + +.selection\:bg-red-500\/30 *::-moz-selection { + background-color: rgb(239 68 68 / 0.3); +} + +.selection\:bg-red-500\/30 *::selection { + background-color: rgb(239 68 68 / 0.3); +} + +.selection\:text-foreground *::-moz-selection { + color: var(--foreground); +} + +.selection\:text-foreground *::selection { + color: var(--foreground); +} + +.selection\:bg-accents-2::-moz-selection { + background-color: var(--accents-2); +} + +.selection\:bg-accents-2::selection { + background-color: var(--accents-2); +} + +.selection\:bg-red-500\/30::-moz-selection { + background-color: rgb(239 68 68 / 0.3); +} + +.selection\:bg-red-500\/30::selection { + background-color: rgb(239 68 68 / 0.3); +} + +.selection\:text-foreground::-moz-selection { + color: var(--foreground); +} + +.selection\:text-foreground::selection { + color: var(--foreground); +} + +.placeholder\:text-accents-3::-moz-placeholder { + color: var(--accents-3); +} + +.placeholder\:text-accents-3::placeholder { + color: var(--accents-3); +} + +.after\:absolute::after { + content: var(--tw-content); + position: absolute; +} + +.after\:bottom-0::after { + content: var(--tw-content); + bottom: 0px; +} + +.after\:left-0::after { + content: var(--tw-content); + left: 0px; +} + +.after\:h-0\.5::after { + content: var(--tw-content); + height: 0.125rem; +} + +.after\:w-full::after { + content: var(--tw-content); + width: 100%; +} + +.after\:bg-foreground::after { + content: var(--tw-content); + background-color: var(--foreground); +} + +.focus-within\:z-10:focus-within { + z-index: 10; +} + +.hover\:-translate-y-1:hover { + --tw-translate-y: -0.25rem; + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); +} + +.hover\:scale-100:hover { + --tw-scale-x: 1; + --tw-scale-y: 1; + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); +} + +.hover\:scale-95:hover { + --tw-scale-x: .95; + --tw-scale-y: .95; + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); +} + +.hover\:border-foreground:hover { + border-color: var(--foreground); +} + +.hover\:border-red-500\/20:hover { + border-color: rgb(239 68 68 / 0.2); +} + +.hover\:bg-accents-1:hover { + background-color: var(--accents-1); +} + +.hover\:bg-accents-2:hover { + background-color: var(--accents-2); +} + +.hover\:bg-accents-3:hover { + background-color: var(--accents-3); +} + +.hover\:bg-amber-600:hover { + --tw-bg-opacity: 1; + background-color: rgb(217 119 6 / var(--tw-bg-opacity, 1)); +} + +.hover\:bg-background:hover { + background-color: var(--background); +} + +.hover\:bg-blue-100:hover { + --tw-bg-opacity: 1; + background-color: rgb(219 234 254 / var(--tw-bg-opacity, 1)); +} + +.hover\:bg-blue-600:hover { + --tw-bg-opacity: 1; + background-color: rgb(37 99 235 / var(--tw-bg-opacity, 1)); +} + +.hover\:bg-emerald-600:hover { + --tw-bg-opacity: 1; + background-color: rgb(5 150 105 / var(--tw-bg-opacity, 1)); +} + +.hover\:bg-red-100:hover { + --tw-bg-opacity: 1; + background-color: rgb(254 226 226 / var(--tw-bg-opacity, 1)); +} + +.hover\:bg-red-50:hover { + --tw-bg-opacity: 1; + background-color: rgb(254 242 242 / var(--tw-bg-opacity, 1)); +} + +.hover\:bg-red-500\/10:hover { + background-color: rgb(239 68 68 / 0.1); +} + +.hover\:bg-red-500\/20:hover { + background-color: rgb(239 68 68 / 0.2); +} + +.hover\:bg-red-600:hover { + --tw-bg-opacity: 1; + background-color: rgb(220 38 38 / var(--tw-bg-opacity, 1)); +} + +.hover\:bg-white\/5:hover { + background-color: rgb(255 255 255 / 0.05); +} + +.hover\:bg-white\/80:hover { + background-color: rgb(255 255 255 / 0.8); +} + +.hover\:text-accents-2:hover { + color: var(--accents-2); +} + +.hover\:text-foreground:hover { + color: var(--foreground); +} + +.hover\:text-purple-600:hover { + --tw-text-opacity: 1; + color: rgb(147 51 234 / var(--tw-text-opacity, 1)); +} + +.hover\:text-red-300:hover { + --tw-text-opacity: 1; + color: rgb(252 165 165 / var(--tw-text-opacity, 1)); +} + +.hover\:text-red-500:hover { + --tw-text-opacity: 1; + color: rgb(239 68 68 / var(--tw-text-opacity, 1)); +} + +.hover\:text-red-600:hover { + --tw-text-opacity: 1; + color: rgb(220 38 38 / var(--tw-text-opacity, 1)); +} + +.hover\:underline:hover { + text-decoration-line: underline; +} + +.hover\:shadow-md:hover { + --tw-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1); + --tw-shadow-colored: 0 4px 6px -1px var(--tw-shadow-color), 0 2px 4px -2px var(--tw-shadow-color); + box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); +} + +.hover\:shadow-sm:hover { + --tw-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05); + --tw-shadow-colored: 0 1px 2px 0 var(--tw-shadow-color); + box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); +} + +.focus\:z-10:focus { + z-index: 10; +} + +.focus\:border-red-500:focus { + --tw-border-opacity: 1; + border-color: rgb(239 68 68 / var(--tw-border-opacity, 1)); +} + +.focus\:outline-none:focus { + outline: 2px solid transparent; + outline-offset: 2px; +} + +.focus\:ring-1:focus { + --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color); + --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color); + box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000); +} + +.focus\:ring-2:focus { + --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color); + --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color); + box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000); +} + +.focus\:ring-accents-2:focus { + --tw-ring-color: var(--accents-2); +} + +.focus\:ring-foreground:focus { + --tw-ring-color: var(--foreground); +} + +.focus\:ring-red-500:focus { + --tw-ring-opacity: 1; + --tw-ring-color: rgb(239 68 68 / var(--tw-ring-opacity, 1)); +} + +.disabled\:cursor-not-allowed:disabled { + cursor: not-allowed; +} + +.disabled\:opacity-50:disabled { + opacity: 0.5; +} + +.group:hover .group-hover\:pointer-events-auto { + pointer-events: auto; +} + +.group:hover .group-hover\:-translate-x-full { + --tw-translate-x: -100%; + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); +} + +.group:hover .group-hover\:translate-x-0 { + --tw-translate-x: 0px; + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); +} + +.group\/lang-item:hover .group-hover\/lang-item\:scale-110 { + --tw-scale-x: 1.1; + --tw-scale-y: 1.1; + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); +} + +.group\/lang:hover .group-hover\/lang\:scale-110 { + --tw-scale-x: 1.1; + --tw-scale-y: 1.1; + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); +} + +.group:hover .group-hover\:scale-105 { + --tw-scale-x: 1.05; + --tw-scale-y: 1.05; + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); +} + +.group:hover .group-hover\:scale-110 { + --tw-scale-x: 1.1; + --tw-scale-y: 1.1; + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); +} + +.group:hover .group-hover\:bg-accents-2 { + background-color: var(--accents-2); +} + +.group:hover .group-hover\:bg-foreground { + background-color: var(--foreground); +} + +.group:hover .group-hover\:bg-red-500\/20 { + background-color: rgb(239 68 68 / 0.2); +} + +.group\/item:hover .group-hover\/item\:text-foreground { + color: var(--foreground); +} + +.group:hover .group-hover\:\!text-red-500 { + --tw-text-opacity: 1 !important; + color: rgb(239 68 68 / var(--tw-text-opacity, 1)) !important; +} + +.group:hover .group-hover\:text-background { + color: var(--background); +} + +.group:hover .group-hover\:text-blue-500 { + --tw-text-opacity: 1; + color: rgb(59 130 246 / var(--tw-text-opacity, 1)); +} + +.group:hover .group-hover\:text-foreground { + color: var(--foreground); +} + +.group:hover .group-hover\:text-red-400\/80 { + color: rgb(248 113 113 / 0.8); +} + +.group:hover .group-hover\:text-red-500 { + --tw-text-opacity: 1; + color: rgb(239 68 68 / var(--tw-text-opacity, 1)); +} + +.group:hover .group-hover\:text-white { + --tw-text-opacity: 1; + color: rgb(255 255 255 / var(--tw-text-opacity, 1)); +} + +.group:hover .group-hover\:opacity-0 { + opacity: 0; +} + +.group:hover .group-hover\:opacity-100 { + opacity: 1; +} + +.group:hover .group-hover\:opacity-50 { + opacity: 0.5; +} + +.group:hover .group-hover\:duration-200 { + transition-duration: 200ms; +} + +.dark\:block:is(.dark *) { + display: block; +} + +.dark\:hidden:is(.dark *) { + display: none; +} + +.dark\:border-blue-800:is(.dark *) { + --tw-border-opacity: 1; + border-color: rgb(30 64 175 / var(--tw-border-opacity, 1)); +} + +.dark\:border-emerald-800:is(.dark *) { + --tw-border-opacity: 1; + border-color: rgb(6 95 70 / var(--tw-border-opacity, 1)); +} + +.dark\:border-green-800:is(.dark *) { + --tw-border-opacity: 1; + border-color: rgb(22 101 52 / var(--tw-border-opacity, 1)); +} + +.dark\:border-orange-800:is(.dark *) { + --tw-border-opacity: 1; + border-color: rgb(154 52 18 / var(--tw-border-opacity, 1)); +} + +.dark\:border-red-800:is(.dark *) { + --tw-border-opacity: 1; + border-color: rgb(153 27 27 / var(--tw-border-opacity, 1)); +} + +.dark\:border-slate-800:is(.dark *) { + --tw-border-opacity: 1; + border-color: rgb(30 41 59 / var(--tw-border-opacity, 1)); +} + +.dark\:border-white\/10:is(.dark *) { + border-color: rgb(255 255 255 / 0.1); +} + +.dark\:\!bg-red-900\/10:is(.dark *) { + background-color: rgb(127 29 29 / 0.1) !important; +} + +.dark\:bg-black\/20:is(.dark *) { + background-color: rgb(0 0 0 / 0.2); +} + +.dark\:bg-black\/30:is(.dark *) { + background-color: rgb(0 0 0 / 0.3); +} + +.dark\:bg-black\/40:is(.dark *) { + background-color: rgb(0 0 0 / 0.4); +} + +.dark\:bg-blue-500:is(.dark *) { + --tw-bg-opacity: 1; + background-color: rgb(59 130 246 / var(--tw-bg-opacity, 1)); +} + +.dark\:bg-blue-500\/5:is(.dark *) { + background-color: rgb(59 130 246 / 0.05); +} + +.dark\:bg-blue-900\/20:is(.dark *) { + background-color: rgb(30 58 138 / 0.2); +} + +.dark\:bg-blue-900\/30:is(.dark *) { + background-color: rgb(30 58 138 / 0.3); +} + +.dark\:bg-green-900\/30:is(.dark *) { + background-color: rgb(20 83 45 / 0.3); +} + +.dark\:bg-purple-500\/5:is(.dark *) { + background-color: rgb(168 85 247 / 0.05); +} + +.dark\:bg-purple-900\/30:is(.dark *) { + background-color: rgb(88 28 135 / 0.3); +} + +.dark\:bg-red-900\/20:is(.dark *) { + background-color: rgb(127 29 29 / 0.2); +} + +.dark\:bg-red-900\/30:is(.dark *) { + background-color: rgb(127 29 29 / 0.3); +} + +.dark\:bg-red-900\/40:is(.dark *) { + background-color: rgb(127 29 29 / 0.4); +} + +.dark\:bg-white\/5:is(.dark *) { + background-color: rgb(255 255 255 / 0.05); +} + +.dark\:bg-\[url\(\'data\:image\/svg\+xml\;base64\2c PHN2ZyB3aWR0aD0iMjAiIGhlaWdodD0iMjAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI\+PGNpcmNsZSBjeD0iMSIgY3k9IjEiIHI9IjEiIGZpbGw9InJnYmEoMjU1LDI1NSwyNTUsMC4wNSkiLz48L3N2Zz4\=\'\)\]:is(.dark *) { + background-image: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAiIGhlaWdodD0iMjAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGNpcmNsZSBjeD0iMSIgY3k9IjEiIHI9IjEiIGZpbGw9InJnYmEoMjU1LDI1NSwyNTUsMC4wNSkiLz48L3N2Zz4='); +} + +.dark\:\!text-accents-6:is(.dark *) { + color: var(--accents-6) !important; +} + +.dark\:\!text-white:is(.dark *) { + --tw-text-opacity: 1 !important; + color: rgb(255 255 255 / var(--tw-text-opacity, 1)) !important; +} + +.dark\:text-blue-400:is(.dark *) { + --tw-text-opacity: 1; + color: rgb(96 165 250 / var(--tw-text-opacity, 1)); +} + +.dark\:text-emerald-400:is(.dark *) { + --tw-text-opacity: 1; + color: rgb(52 211 153 / var(--tw-text-opacity, 1)); +} + +.dark\:text-green-400:is(.dark *) { + --tw-text-opacity: 1; + color: rgb(74 222 128 / var(--tw-text-opacity, 1)); +} + +.dark\:text-orange-400:is(.dark *) { + --tw-text-opacity: 1; + color: rgb(251 146 60 / var(--tw-text-opacity, 1)); +} + +.dark\:text-purple-400:is(.dark *) { + --tw-text-opacity: 1; + color: rgb(192 132 252 / var(--tw-text-opacity, 1)); +} + +.dark\:text-red-400:is(.dark *) { + --tw-text-opacity: 1; + color: rgb(248 113 113 / var(--tw-text-opacity, 1)); +} + +.dark\:text-red-500:is(.dark *) { + --tw-text-opacity: 1; + color: rgb(239 68 68 / var(--tw-text-opacity, 1)); +} + +.dark\:text-slate-400:is(.dark *) { + --tw-text-opacity: 1; + color: rgb(148 163 184 / var(--tw-text-opacity, 1)); +} + +.dark\:text-yellow-400:is(.dark *) { + --tw-text-opacity: 1; + color: rgb(250 204 21 / var(--tw-text-opacity, 1)); +} + +.dark\:ring-red-800\/50:is(.dark *) { + --tw-ring-color: rgb(153 27 27 / 0.5); +} + +.dark\:ring-white\/5:is(.dark *) { + --tw-ring-color: rgb(255 255 255 / 0.05); +} + +.dark\:hover\:bg-blue-900\/40:hover:is(.dark *) { + background-color: rgb(30 58 138 / 0.4); +} + +.dark\:hover\:bg-red-900\/10:hover:is(.dark *) { + background-color: rgb(127 29 29 / 0.1); +} + +.dark\:hover\:bg-red-900\/20:hover:is(.dark *) { + background-color: rgb(127 29 29 / 0.2); +} + +.dark\:hover\:bg-red-900\/30:hover:is(.dark *) { + background-color: rgb(127 29 29 / 0.3); +} + +.dark\:hover\:bg-red-900\/40:hover:is(.dark *) { + background-color: rgb(127 29 29 / 0.4); +} + +.dark\:hover\:bg-white\/10:hover:is(.dark *) { + background-color: rgb(255 255 255 / 0.1); +} + +.dark\:hover\:text-purple-400:hover:is(.dark *) { + --tw-text-opacity: 1; + color: rgb(192 132 252 / var(--tw-text-opacity, 1)); +} + +@media (min-width: 640px) { + .sm\:mb-10 { + margin-bottom: 2.5rem; + } + + .sm\:mb-8 { + margin-bottom: 2rem; + } + + .sm\:mt-0 { + margin-top: 0px; + } + + .sm\:block { + display: block; + } + + .sm\:flex { + display: flex; + } + + .sm\:hidden { + display: none; + } + + .sm\:h-12 { + height: 3rem; + } + + .sm\:h-\[500px\] { + height: 500px; + } + + .sm\:w-48 { + width: 12rem; + } + + .sm\:w-64 { + width: 16rem; + } + + .sm\:w-auto { + width: auto; + } + + .sm\:max-w-md { + max-width: 28rem; + } + + .sm\:grid-cols-2 { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + .sm\:grid-cols-3 { + grid-template-columns: repeat(3, minmax(0, 1fr)); + } + + .sm\:flex-row { + flex-direction: row; + } + + .sm\:items-center { + align-items: center; + } + + .sm\:gap-0 { + gap: 0px; + } + + .sm\:self-auto { + align-self: auto; + } + + .sm\:p-6 { + padding: 1.5rem; + } + + .sm\:p-8 { + padding: 2rem; + } + + .sm\:px-6 { + padding-left: 1.5rem; + padding-right: 1.5rem; + } + + .sm\:text-3xl { + font-size: 1.875rem; + line-height: 2.25rem; + } +} + +@media (min-width: 768px) { + .md\:static { + position: static; + } + + .md\:col-span-1 { + grid-column: span 1 / span 1; + } + + .md\:col-span-2 { + grid-column: span 2 / span 2; + } + + .md\:block { + display: block; + } + + .md\:flex { + display: flex; + } + + .md\:hidden { + display: none; + } + + .md\:h-14 { + height: 3.5rem; + } + + .md\:h-8 { + height: 2rem; + } + + .md\:w-14 { + width: 3.5rem; + } + + .md\:w-64 { + width: 16rem; + } + + .md\:w-8 { + width: 2rem; + } + + .md\:w-auto { + width: auto; + } + + .md\:translate-x-0 { + --tw-translate-x: 0px; + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); + } + + .md\:grid-cols-1 { + grid-template-columns: repeat(1, minmax(0, 1fr)); + } + + .md\:grid-cols-2 { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + .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\:flex-row { + flex-direction: row; + } + + .md\:items-center { + align-items: center; + } + + .md\:justify-between { + justify-content: space-between; + } + + .md\:p-6 { + padding: 1.5rem; + } + + .md\:p-8 { + padding: 2rem; + } + + .md\:px-8 { + padding-left: 2rem; + padding-right: 2rem; + } + + .md\:py-16 { + padding-top: 4rem; + padding-bottom: 4rem; + } + + .md\:text-2xl { + font-size: 1.5rem; + line-height: 2rem; + } + + .md\:text-3xl { + font-size: 1.875rem; + line-height: 2.25rem; + } + + .md\:text-base { + font-size: 1rem; + line-height: 1.5rem; + } + + .md\:text-sm { + font-size: 0.875rem; + line-height: 1.25rem; + } +} + +@media (min-width: 1024px) { + .lg\:col-span-1 { + grid-column: span 1 / span 1; + } + + .lg\:col-span-2 { + grid-column: span 2 / span 2; + } + + .lg\:h-\[calc\(100vh-8rem\)\] { + height: calc(100vh - 8rem); + } + + .lg\:h-auto { + height: auto; + } + + .lg\:min-h-0 { + min-height: 0px; + } + + .lg\:w-64 { + width: 16rem; + } + + .lg\:w-auto { + width: auto; + } + + .lg\:grid-cols-1 { + grid-template-columns: repeat(1, minmax(0, 1fr)); + } + + .lg\:grid-cols-3 { + grid-template-columns: repeat(3, minmax(0, 1fr)); + } + + .lg\:grid-cols-5 { + grid-template-columns: repeat(5, minmax(0, 1fr)); + } + + .lg\:flex-row { + flex-direction: row; + } + + .lg\:items-center { + align-items: center; + } + + .lg\:px-8 { + padding-left: 2rem; + padding-right: 2rem; + } +} + +@media (min-width: 1280px) { + .xl\:grid-cols-2 { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + .xl\:grid-cols-4 { + grid-template-columns: repeat(4, minmax(0, 1fr)); + } +} diff --git a/public/assets/flag-icons-main.zip b/public/assets/flag-icons-main.zip new file mode 100644 index 0000000..b5b6f1f Binary files /dev/null and b/public/assets/flag-icons-main.zip differ diff --git a/public/assets/fonts/Geist-Bold.woff2 b/public/assets/fonts/Geist-Bold.woff2 new file mode 100644 index 0000000..0132bc6 Binary files /dev/null and b/public/assets/fonts/Geist-Bold.woff2 differ diff --git a/public/assets/fonts/Geist-Regular.woff2 b/public/assets/fonts/Geist-Regular.woff2 new file mode 100644 index 0000000..dda2a42 Binary files /dev/null and b/public/assets/fonts/Geist-Regular.woff2 differ diff --git a/public/assets/fonts/GeistMono-Regular.woff2 b/public/assets/fonts/GeistMono-Regular.woff2 new file mode 100644 index 0000000..2db5c17 Binary files /dev/null and b/public/assets/fonts/GeistMono-Regular.woff2 differ diff --git a/public/assets/img/favicon.png b/public/assets/img/favicon.png new file mode 100644 index 0000000..af028f6 Binary files /dev/null and b/public/assets/img/favicon.png differ diff --git a/public/assets/img/index.php b/public/assets/img/index.php new file mode 100644 index 0000000..00b593c --- /dev/null +++ b/public/assets/img/index.php @@ -0,0 +1,20 @@ +. + */ +echo ""; +?> + diff --git a/public/assets/img/logo-m-dark.svg b/public/assets/img/logo-m-dark.svg new file mode 100644 index 0000000..0df2041 --- /dev/null +++ b/public/assets/img/logo-m-dark.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/public/assets/img/logo-m.svg b/public/assets/img/logo-m.svg new file mode 100644 index 0000000..ff08b11 --- /dev/null +++ b/public/assets/img/logo-m.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/public/assets/img/logo-outlined.png b/public/assets/img/logo-outlined.png new file mode 100644 index 0000000..d95fda4 Binary files /dev/null and b/public/assets/img/logo-outlined.png differ diff --git a/public/assets/img/logo-outlined.svg b/public/assets/img/logo-outlined.svg new file mode 100644 index 0000000..c2440d6 --- /dev/null +++ b/public/assets/img/logo-outlined.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/public/assets/img/logo.png b/public/assets/img/logo.png new file mode 100644 index 0000000..1a3bdde Binary files /dev/null and b/public/assets/img/logo.png differ diff --git a/public/assets/img/logo.svg b/public/assets/img/logo.svg new file mode 100644 index 0000000..5a914a0 --- /dev/null +++ b/public/assets/img/logo.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/public/assets/img/logos/SnapInsta.to_572755515_18030421517720884_3040798698013006144_n.jpg b/public/assets/img/logos/SnapInsta.to_572755515_18030421517720884_3040798698013006144_n.jpg new file mode 100644 index 0000000..ed57874 Binary files /dev/null and b/public/assets/img/logos/SnapInsta.to_572755515_18030421517720884_3040798698013006144_n.jpg differ diff --git a/public/assets/img/logos/whc7WD.jpg b/public/assets/img/logos/whc7WD.jpg new file mode 100644 index 0000000..ed57874 Binary files /dev/null and b/public/assets/img/logos/whc7WD.jpg differ diff --git a/public/assets/js/alert-helper.js b/public/assets/js/alert-helper.js new file mode 100644 index 0000000..ff7950e --- /dev/null +++ b/public/assets/js/alert-helper.js @@ -0,0 +1,148 @@ +/** + * Global Alert Helper for Mivo + * Provides a standardized way to trigger premium SweetAlert2 dialogs. + */ +const Mivo = { + /** + * Show a simple alert dialog. + * @param {string} type - 'success', 'error', 'warning', 'info', 'question' + * @param {string} title - The title of the alert + * @param {string} message - The body text/HTML + * @returns {Promise} + */ + alert: function(type, title, message = '') { + const typeMap = { + 'success': { icon: 'check-circle-2', color: 'text-success' }, + 'error': { icon: 'x-circle', color: 'text-error' }, + 'warning': { icon: 'alert-triangle', color: 'text-warning' }, + 'info': { icon: 'info', color: 'text-info' }, + 'question':{ icon: 'help-circle', color: 'text-question' } + }; + + const config = typeMap[type] || typeMap['info']; + + return Swal.fire({ + iconHtml: ``, + title: title, + html: message, + confirmButtonText: 'OK', + customClass: { + popup: 'swal2-premium-card', + confirmButton: 'btn btn-primary', + cancelButton: 'btn btn-secondary', + }, + buttonsStyling: false, + heightAuto: false, + didOpen: () => { + if (typeof lucide !== 'undefined') lucide.createIcons(); + } + }); + }, + + /** + * Show a confirmation dialog. + * @param {string} title - The title of the confirmation + * @param {string} message - The body text/HTML + * @param {string} confirmText - Text for the confirm button + * @param {string} cancelText - Text for the cancel button + * @returns {Promise} Resolves if confirmed, rejects if cancelled + */ + confirm: function(title, message = '', confirmText = 'Yes, Proceed', cancelText = 'Cancel') { + return Swal.fire({ + iconHtml: ``, + title: title, + html: message, + showCancelButton: true, + confirmButtonText: confirmText, + cancelButtonText: cancelText, + customClass: { + popup: 'swal2-premium-card', + confirmButton: 'btn btn-primary', + cancelButton: 'btn btn-secondary', + }, + buttonsStyling: false, + reverseButtons: true, + heightAuto: false, + didOpen: () => { + if (typeof lucide !== 'undefined') lucide.createIcons(); + } + }).then(result => result.isConfirmed); + }, + + /** + * Show a premium stacking toast. + * @param {string} type - 'success', 'error', 'warning', 'info' + * @param {string} title - Title + * @param {string} message - Body text + * @param {number} duration - ms before auto-close + */ + toast: function(type, title, message = '', duration = 5000) { + let container = document.getElementById('mivo-toast-container'); + if (!container) { + container = document.createElement('div'); + container.id = 'mivo-toast-container'; + document.body.appendChild(container); + } + + const typeMap = { + 'success': { icon: 'check-circle-2', color: 'text-success' }, + 'error': { icon: 'x-circle', color: 'text-error' }, + 'warning': { icon: 'alert-triangle', color: 'text-warning' }, + 'info': { icon: 'info', color: 'text-info' } + }; + + const config = typeMap[type] || typeMap['info']; + + const toast = document.createElement('div'); + toast.className = `mivo-toast ${config.color}`; + + toast.innerHTML = ` +
+ +
+
+
${title}
+ ${message ? `
${message}
` : ''} +
+ +
+ `; + + container.appendChild(toast); + if (typeof lucide !== 'undefined') lucide.createIcons(); + + // Close logic + const closeToast = () => { + toast.classList.add('mivo-toast-fade-out'); + setTimeout(() => { + toast.remove(); + if (container.children.length === 0) container.remove(); + }, 300); + }; + + toast.querySelector('.mivo-toast-close').addEventListener('click', closeToast); + + // Auto-close with progress bar + const progress = toast.querySelector('.mivo-toast-progress'); + const start = Date.now(); + + const updateProgress = () => { + const elapsed = Date.now() - start; + const percentage = Math.min((elapsed / duration) * 100, 100); + progress.style.width = percentage + '%'; + + if (percentage < 100) { + requestAnimationFrame(updateProgress); + } else { + closeToast(); + } + }; + + requestAnimationFrame(updateProgress); + } +}; + +// Also expose as global shortcuts if needed +window.Mivo = Mivo; diff --git a/public/assets/js/chart.min.js b/public/assets/js/chart.min.js new file mode 100644 index 0000000..4cfce5c --- /dev/null +++ b/public/assets/js/chart.min.js @@ -0,0 +1,14 @@ +/*! + * Chart.js v4.5.1 + * https://www.chartjs.org + * (c) 2025 Chart.js Contributors + * Released under the MIT License + */ +!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t="undefined"!=typeof globalThis?globalThis:t||self).Chart=e()}(this,(function(){"use strict";var t=Object.freeze({__proto__:null,get Colors(){return Jo},get Decimation(){return ta},get Filler(){return ba},get Legend(){return Ma},get SubTitle(){return Pa},get Title(){return ka},get Tooltip(){return Na}});function e(){}const i=(()=>{let t=0;return()=>t++})();function s(t){return null==t}function n(t){if(Array.isArray&&Array.isArray(t))return!0;const e=Object.prototype.toString.call(t);return"[object"===e.slice(0,7)&&"Array]"===e.slice(-6)}function o(t){return null!==t&&"[object Object]"===Object.prototype.toString.call(t)}function a(t){return("number"==typeof t||t instanceof Number)&&isFinite(+t)}function r(t,e){return a(t)?t:e}function l(t,e){return void 0===t?e:t}const h=(t,e)=>"string"==typeof t&&t.endsWith("%")?parseFloat(t)/100:+t/e,c=(t,e)=>"string"==typeof t&&t.endsWith("%")?parseFloat(t)/100*e:+t;function d(t,e,i){if(t&&"function"==typeof t.call)return t.apply(i,e)}function u(t,e,i,s){let a,r,l;if(n(t))if(r=t.length,s)for(a=r-1;a>=0;a--)e.call(i,t[a],a);else for(a=0;at,x:t=>t.x,y:t=>t.y};function v(t){const e=t.split("."),i=[];let s="";for(const t of e)s+=t,s.endsWith("\\")?s=s.slice(0,-1)+".":(i.push(s),s="");return i}function M(t,e){const i=y[e]||(y[e]=function(t){const e=v(t);return t=>{for(const i of e){if(""===i)break;t=t&&t[i]}return t}}(e));return i(t)}function w(t){return t.charAt(0).toUpperCase()+t.slice(1)}const k=t=>void 0!==t,S=t=>"function"==typeof t,P=(t,e)=>{if(t.size!==e.size)return!1;for(const i of t)if(!e.has(i))return!1;return!0};function D(t){return"mouseup"===t.type||"click"===t.type||"contextmenu"===t.type}const C=Math.PI,O=2*C,A=O+C,T=Number.POSITIVE_INFINITY,L=C/180,E=C/2,R=C/4,I=2*C/3,z=Math.log10,F=Math.sign;function V(t,e,i){return Math.abs(t-e)t-e)).pop(),e}function N(t){return!function(t){return"symbol"==typeof t||"object"==typeof t&&null!==t&&!(Symbol.toPrimitive in t||"toString"in t||"valueOf"in t)}(t)&&!isNaN(parseFloat(t))&&isFinite(t)}function H(t,e){const i=Math.round(t);return i-e<=t&&i+e>=t}function j(t,e,i){let s,n,o;for(s=0,n=t.length;sl&&h=Math.min(e,i)-s&&t<=Math.max(e,i)+s}function et(t,e,i){i=i||(i=>t[i]1;)s=o+n>>1,i(s)?o=s:n=s;return{lo:o,hi:n}}const it=(t,e,i,s)=>et(t,i,s?s=>{const n=t[s][e];return nt[s][e]et(t,i,(s=>t[s][e]>=i));function nt(t,e,i){let s=0,n=t.length;for(;ss&&t[n-1]>i;)n--;return s>0||n{const i="_onData"+w(e),s=t[e];Object.defineProperty(t,e,{configurable:!0,enumerable:!1,value(...e){const n=s.apply(this,e);return t._chartjs.listeners.forEach((t=>{"function"==typeof t[i]&&t[i](...e)})),n}})})))}function rt(t,e){const i=t._chartjs;if(!i)return;const s=i.listeners,n=s.indexOf(e);-1!==n&&s.splice(n,1),s.length>0||(ot.forEach((e=>{delete t[e]})),delete t._chartjs)}function lt(t){const e=new Set(t);return e.size===t.length?t:Array.from(e)}const ht="undefined"==typeof window?function(t){return t()}:window.requestAnimationFrame;function ct(t,e){let i=[],s=!1;return function(...n){i=n,s||(s=!0,ht.call(window,(()=>{s=!1,t.apply(e,i)})))}}function dt(t,e){let i;return function(...s){return e?(clearTimeout(i),i=setTimeout(t,e,s)):t.apply(this,s),e}}const ut=t=>"start"===t?"left":"end"===t?"right":"center",ft=(t,e,i)=>"start"===t?e:"end"===t?i:(e+i)/2,gt=(t,e,i,s)=>t===(s?"left":"right")?i:"center"===t?(e+i)/2:e;function pt(t,e,i){const n=e.length;let o=0,a=n;if(t._sorted){const{iScale:r,vScale:l,_parsed:h}=t,c=t.dataset&&t.dataset.options?t.dataset.options.spanGaps:null,d=r.axis,{min:u,max:f,minDefined:g,maxDefined:p}=r.getUserBounds();if(g){if(o=Math.min(it(h,d,u).lo,i?n:it(e,d,r.getPixelForValue(u)).lo),c){const t=h.slice(0,o+1).reverse().findIndex((t=>!s(t[l.axis])));o-=Math.max(0,t)}o=Z(o,0,n-1)}if(p){let t=Math.max(it(h,r.axis,f,!0).hi+1,i?0:it(e,d,r.getPixelForValue(f),!0).hi+1);if(c){const e=h.slice(t-1).findIndex((t=>!s(t[l.axis])));t+=Math.max(0,e)}a=Z(t,o,n)-o}else a=n-o}return{start:o,count:a}}function mt(t){const{xScale:e,yScale:i,_scaleRanges:s}=t,n={xmin:e.min,xmax:e.max,ymin:i.min,ymax:i.max};if(!s)return t._scaleRanges=n,!0;const o=s.xmin!==e.min||s.xmax!==e.max||s.ymin!==i.min||s.ymax!==i.max;return Object.assign(s,n),o}class xt{constructor(){this._request=null,this._charts=new Map,this._running=!1,this._lastDate=void 0}_notify(t,e,i,s){const n=e.listeners[s],o=e.duration;n.forEach((s=>s({chart:t,initial:e.initial,numSteps:o,currentStep:Math.min(i-e.start,o)})))}_refresh(){this._request||(this._running=!0,this._request=ht.call(window,(()=>{this._update(),this._request=null,this._running&&this._refresh()})))}_update(t=Date.now()){let e=0;this._charts.forEach(((i,s)=>{if(!i.running||!i.items.length)return;const n=i.items;let o,a=n.length-1,r=!1;for(;a>=0;--a)o=n[a],o._active?(o._total>i.duration&&(i.duration=o._total),o.tick(t),r=!0):(n[a]=n[n.length-1],n.pop());r&&(s.draw(),this._notify(s,i,t,"progress")),n.length||(i.running=!1,this._notify(s,i,t,"complete"),i.initial=!1),e+=n.length})),this._lastDate=t,0===e&&(this._running=!1)}_getAnims(t){const e=this._charts;let i=e.get(t);return i||(i={running:!1,initial:!0,items:[],listeners:{complete:[],progress:[]}},e.set(t,i)),i}listen(t,e,i){this._getAnims(t).listeners[e].push(i)}add(t,e){e&&e.length&&this._getAnims(t).items.push(...e)}has(t){return this._getAnims(t).items.length>0}start(t){const e=this._charts.get(t);e&&(e.running=!0,e.start=Date.now(),e.duration=e.items.reduce(((t,e)=>Math.max(t,e._duration)),0),this._refresh())}running(t){if(!this._running)return!1;const e=this._charts.get(t);return!!(e&&e.running&&e.items.length)}stop(t){const e=this._charts.get(t);if(!e||!e.items.length)return;const i=e.items;let s=i.length-1;for(;s>=0;--s)i[s].cancel();e.items=[],this._notify(t,e,Date.now(),"complete")}remove(t){return this._charts.delete(t)}}var bt=new xt; +/*! + * @kurkle/color v0.3.2 + * https://github.com/kurkle/color#readme + * (c) 2023 Jukka Kurkela + * Released under the MIT License + */function _t(t){return t+.5|0}const yt=(t,e,i)=>Math.max(Math.min(t,i),e);function vt(t){return yt(_t(2.55*t),0,255)}function Mt(t){return yt(_t(255*t),0,255)}function wt(t){return yt(_t(t/2.55)/100,0,1)}function kt(t){return yt(_t(100*t),0,100)}const St={0:0,1:1,2:2,3:3,4:4,5:5,6:6,7:7,8:8,9:9,A:10,B:11,C:12,D:13,E:14,F:15,a:10,b:11,c:12,d:13,e:14,f:15},Pt=[..."0123456789ABCDEF"],Dt=t=>Pt[15&t],Ct=t=>Pt[(240&t)>>4]+Pt[15&t],Ot=t=>(240&t)>>4==(15&t);function At(t){var e=(t=>Ot(t.r)&&Ot(t.g)&&Ot(t.b)&&Ot(t.a))(t)?Dt:Ct;return t?"#"+e(t.r)+e(t.g)+e(t.b)+((t,e)=>t<255?e(t):"")(t.a,e):void 0}const Tt=/^(hsla?|hwb|hsv)\(\s*([-+.e\d]+)(?:deg)?[\s,]+([-+.e\d]+)%[\s,]+([-+.e\d]+)%(?:[\s,]+([-+.e\d]+)(%)?)?\s*\)$/;function Lt(t,e,i){const s=e*Math.min(i,1-i),n=(e,n=(e+t/30)%12)=>i-s*Math.max(Math.min(n-3,9-n,1),-1);return[n(0),n(8),n(4)]}function Et(t,e,i){const s=(s,n=(s+t/60)%6)=>i-i*e*Math.max(Math.min(n,4-n,1),0);return[s(5),s(3),s(1)]}function Rt(t,e,i){const s=Lt(t,1,.5);let n;for(e+i>1&&(n=1/(e+i),e*=n,i*=n),n=0;n<3;n++)s[n]*=1-e-i,s[n]+=e;return s}function It(t){const e=t.r/255,i=t.g/255,s=t.b/255,n=Math.max(e,i,s),o=Math.min(e,i,s),a=(n+o)/2;let r,l,h;return n!==o&&(h=n-o,l=a>.5?h/(2-n-o):h/(n+o),r=function(t,e,i,s,n){return t===n?(e-i)/s+(e>16&255,o>>8&255,255&o]}return t}(),Ht.transparent=[0,0,0,0]);const e=Ht[t.toLowerCase()];return e&&{r:e[0],g:e[1],b:e[2],a:4===e.length?e[3]:255}}const $t=/^rgba?\(\s*([-+.\d]+)(%)?[\s,]+([-+.e\d]+)(%)?[\s,]+([-+.e\d]+)(%)?(?:[\s,/]+([-+.e\d]+)(%)?)?\s*\)$/;const Yt=t=>t<=.0031308?12.92*t:1.055*Math.pow(t,1/2.4)-.055,Ut=t=>t<=.04045?t/12.92:Math.pow((t+.055)/1.055,2.4);function Xt(t,e,i){if(t){let s=It(t);s[e]=Math.max(0,Math.min(s[e]+s[e]*i,0===e?360:1)),s=Ft(s),t.r=s[0],t.g=s[1],t.b=s[2]}}function qt(t,e){return t?Object.assign(e||{},t):t}function Kt(t){var e={r:0,g:0,b:0,a:255};return Array.isArray(t)?t.length>=3&&(e={r:t[0],g:t[1],b:t[2],a:255},t.length>3&&(e.a=Mt(t[3]))):(e=qt(t,{r:0,g:0,b:0,a:1})).a=Mt(e.a),e}function Gt(t){return"r"===t.charAt(0)?function(t){const e=$t.exec(t);let i,s,n,o=255;if(e){if(e[7]!==i){const t=+e[7];o=e[8]?vt(t):yt(255*t,0,255)}return i=+e[1],s=+e[3],n=+e[5],i=255&(e[2]?vt(i):yt(i,0,255)),s=255&(e[4]?vt(s):yt(s,0,255)),n=255&(e[6]?vt(n):yt(n,0,255)),{r:i,g:s,b:n,a:o}}}(t):Bt(t)}class Jt{constructor(t){if(t instanceof Jt)return t;const e=typeof t;let i;var s,n,o;"object"===e?i=Kt(t):"string"===e&&(o=(s=t).length,"#"===s[0]&&(4===o||5===o?n={r:255&17*St[s[1]],g:255&17*St[s[2]],b:255&17*St[s[3]],a:5===o?17*St[s[4]]:255}:7!==o&&9!==o||(n={r:St[s[1]]<<4|St[s[2]],g:St[s[3]]<<4|St[s[4]],b:St[s[5]]<<4|St[s[6]],a:9===o?St[s[7]]<<4|St[s[8]]:255})),i=n||jt(t)||Gt(t)),this._rgb=i,this._valid=!!i}get valid(){return this._valid}get rgb(){var t=qt(this._rgb);return t&&(t.a=wt(t.a)),t}set rgb(t){this._rgb=Kt(t)}rgbString(){return this._valid?(t=this._rgb)&&(t.a<255?`rgba(${t.r}, ${t.g}, ${t.b}, ${wt(t.a)})`:`rgb(${t.r}, ${t.g}, ${t.b})`):void 0;var t}hexString(){return this._valid?At(this._rgb):void 0}hslString(){return this._valid?function(t){if(!t)return;const e=It(t),i=e[0],s=kt(e[1]),n=kt(e[2]);return t.a<255?`hsla(${i}, ${s}%, ${n}%, ${wt(t.a)})`:`hsl(${i}, ${s}%, ${n}%)`}(this._rgb):void 0}mix(t,e){if(t){const i=this.rgb,s=t.rgb;let n;const o=e===n?.5:e,a=2*o-1,r=i.a-s.a,l=((a*r==-1?a:(a+r)/(1+a*r))+1)/2;n=1-l,i.r=255&l*i.r+n*s.r+.5,i.g=255&l*i.g+n*s.g+.5,i.b=255&l*i.b+n*s.b+.5,i.a=o*i.a+(1-o)*s.a,this.rgb=i}return this}interpolate(t,e){return t&&(this._rgb=function(t,e,i){const s=Ut(wt(t.r)),n=Ut(wt(t.g)),o=Ut(wt(t.b));return{r:Mt(Yt(s+i*(Ut(wt(e.r))-s))),g:Mt(Yt(n+i*(Ut(wt(e.g))-n))),b:Mt(Yt(o+i*(Ut(wt(e.b))-o))),a:t.a+i*(e.a-t.a)}}(this._rgb,t._rgb,e)),this}clone(){return new Jt(this.rgb)}alpha(t){return this._rgb.a=Mt(t),this}clearer(t){return this._rgb.a*=1-t,this}greyscale(){const t=this._rgb,e=_t(.3*t.r+.59*t.g+.11*t.b);return t.r=t.g=t.b=e,this}opaquer(t){return this._rgb.a*=1+t,this}negate(){const t=this._rgb;return t.r=255-t.r,t.g=255-t.g,t.b=255-t.b,this}lighten(t){return Xt(this._rgb,2,t),this}darken(t){return Xt(this._rgb,2,-t),this}saturate(t){return Xt(this._rgb,1,t),this}desaturate(t){return Xt(this._rgb,1,-t),this}rotate(t){return function(t,e){var i=It(t);i[0]=Vt(i[0]+e),i=Ft(i),t.r=i[0],t.g=i[1],t.b=i[2]}(this._rgb,t),this}}function Zt(t){if(t&&"object"==typeof t){const e=t.toString();return"[object CanvasPattern]"===e||"[object CanvasGradient]"===e}return!1}function Qt(t){return Zt(t)?t:new Jt(t)}function te(t){return Zt(t)?t:new Jt(t).saturate(.5).darken(.1).hexString()}const ee=["x","y","borderWidth","radius","tension"],ie=["color","borderColor","backgroundColor"];const se=new Map;function ne(t,e,i){return function(t,e){e=e||{};const i=t+JSON.stringify(e);let s=se.get(i);return s||(s=new Intl.NumberFormat(t,e),se.set(i,s)),s}(e,i).format(t)}const oe={values:t=>n(t)?t:""+t,numeric(t,e,i){if(0===t)return"0";const s=this.chart.options.locale;let n,o=t;if(i.length>1){const e=Math.max(Math.abs(i[0].value),Math.abs(i[i.length-1].value));(e<1e-4||e>1e15)&&(n="scientific"),o=function(t,e){let i=e.length>3?e[2].value-e[1].value:e[1].value-e[0].value;Math.abs(i)>=1&&t!==Math.floor(t)&&(i=t-Math.floor(t));return i}(t,i)}const a=z(Math.abs(o)),r=isNaN(a)?1:Math.max(Math.min(-1*Math.floor(a),20),0),l={notation:n,minimumFractionDigits:r,maximumFractionDigits:r};return Object.assign(l,this.options.ticks.format),ne(t,s,l)},logarithmic(t,e,i){if(0===t)return"0";const s=i[e].significand||t/Math.pow(10,Math.floor(z(t)));return[1,2,3,5,10,15].includes(s)||e>.8*i.length?oe.numeric.call(this,t,e,i):""}};var ae={formatters:oe};const re=Object.create(null),le=Object.create(null);function he(t,e){if(!e)return t;const i=e.split(".");for(let e=0,s=i.length;et.chart.platform.getDevicePixelRatio(),this.elements={},this.events=["mousemove","mouseout","click","touchstart","touchmove"],this.font={family:"'Helvetica Neue', 'Helvetica', 'Arial', sans-serif",size:12,style:"normal",lineHeight:1.2,weight:null},this.hover={},this.hoverBackgroundColor=(t,e)=>te(e.backgroundColor),this.hoverBorderColor=(t,e)=>te(e.borderColor),this.hoverColor=(t,e)=>te(e.color),this.indexAxis="x",this.interaction={mode:"nearest",intersect:!0,includeInvisible:!1},this.maintainAspectRatio=!0,this.onHover=null,this.onClick=null,this.parsing=!0,this.plugins={},this.responsive=!0,this.scale=void 0,this.scales={},this.showLine=!0,this.drawActiveElementsOnTop=!0,this.describe(t),this.apply(e)}set(t,e){return ce(this,t,e)}get(t){return he(this,t)}describe(t,e){return ce(le,t,e)}override(t,e){return ce(re,t,e)}route(t,e,i,s){const n=he(this,t),a=he(this,i),r="_"+e;Object.defineProperties(n,{[r]:{value:n[e],writable:!0},[e]:{enumerable:!0,get(){const t=this[r],e=a[s];return o(t)?Object.assign({},e,t):l(t,e)},set(t){this[r]=t}}})}apply(t){t.forEach((t=>t(this)))}}var ue=new de({_scriptable:t=>!t.startsWith("on"),_indexable:t=>"events"!==t,hover:{_fallback:"interaction"},interaction:{_scriptable:!1,_indexable:!1}},[function(t){t.set("animation",{delay:void 0,duration:1e3,easing:"easeOutQuart",fn:void 0,from:void 0,loop:void 0,to:void 0,type:void 0}),t.describe("animation",{_fallback:!1,_indexable:!1,_scriptable:t=>"onProgress"!==t&&"onComplete"!==t&&"fn"!==t}),t.set("animations",{colors:{type:"color",properties:ie},numbers:{type:"number",properties:ee}}),t.describe("animations",{_fallback:"animation"}),t.set("transitions",{active:{animation:{duration:400}},resize:{animation:{duration:0}},show:{animations:{colors:{from:"transparent"},visible:{type:"boolean",duration:0}}},hide:{animations:{colors:{to:"transparent"},visible:{type:"boolean",easing:"linear",fn:t=>0|t}}}})},function(t){t.set("layout",{autoPadding:!0,padding:{top:0,right:0,bottom:0,left:0}})},function(t){t.set("scale",{display:!0,offset:!1,reverse:!1,beginAtZero:!1,bounds:"ticks",clip:!0,grace:0,grid:{display:!0,lineWidth:1,drawOnChartArea:!0,drawTicks:!0,tickLength:8,tickWidth:(t,e)=>e.lineWidth,tickColor:(t,e)=>e.color,offset:!1},border:{display:!0,dash:[],dashOffset:0,width:1},title:{display:!1,text:"",padding:{top:4,bottom:4}},ticks:{minRotation:0,maxRotation:50,mirror:!1,textStrokeWidth:0,textStrokeColor:"",padding:3,display:!0,autoSkip:!0,autoSkipPadding:3,labelOffset:0,callback:ae.formatters.values,minor:{},major:{},align:"center",crossAlign:"near",showLabelBackdrop:!1,backdropColor:"rgba(255, 255, 255, 0.75)",backdropPadding:2}}),t.route("scale.ticks","color","","color"),t.route("scale.grid","color","","borderColor"),t.route("scale.border","color","","borderColor"),t.route("scale.title","color","","color"),t.describe("scale",{_fallback:!1,_scriptable:t=>!t.startsWith("before")&&!t.startsWith("after")&&"callback"!==t&&"parser"!==t,_indexable:t=>"borderDash"!==t&&"tickBorderDash"!==t&&"dash"!==t}),t.describe("scales",{_fallback:"scale"}),t.describe("scale.ticks",{_scriptable:t=>"backdropPadding"!==t&&"callback"!==t,_indexable:t=>"backdropPadding"!==t})}]);function fe(){return"undefined"!=typeof window&&"undefined"!=typeof document}function ge(t){let e=t.parentNode;return e&&"[object ShadowRoot]"===e.toString()&&(e=e.host),e}function pe(t,e,i){let s;return"string"==typeof t?(s=parseInt(t,10),-1!==t.indexOf("%")&&(s=s/100*e.parentNode[i])):s=t,s}const me=t=>t.ownerDocument.defaultView.getComputedStyle(t,null);function xe(t,e){return me(t).getPropertyValue(e)}const be=["top","right","bottom","left"];function _e(t,e,i){const s={};i=i?"-"+i:"";for(let n=0;n<4;n++){const o=be[n];s[o]=parseFloat(t[e+"-"+o+i])||0}return s.width=s.left+s.right,s.height=s.top+s.bottom,s}const ye=(t,e,i)=>(t>0||e>0)&&(!i||!i.shadowRoot);function ve(t,e){if("native"in t)return t;const{canvas:i,currentDevicePixelRatio:s}=e,n=me(i),o="border-box"===n.boxSizing,a=_e(n,"padding"),r=_e(n,"border","width"),{x:l,y:h,box:c}=function(t,e){const i=t.touches,s=i&&i.length?i[0]:t,{offsetX:n,offsetY:o}=s;let a,r,l=!1;if(ye(n,o,t.target))a=n,r=o;else{const t=e.getBoundingClientRect();a=s.clientX-t.left,r=s.clientY-t.top,l=!0}return{x:a,y:r,box:l}}(t,i),d=a.left+(c&&r.left),u=a.top+(c&&r.top);let{width:f,height:g}=e;return o&&(f-=a.width+r.width,g-=a.height+r.height),{x:Math.round((l-d)/f*i.width/s),y:Math.round((h-u)/g*i.height/s)}}const Me=t=>Math.round(10*t)/10;function we(t,e,i,s){const n=me(t),o=_e(n,"margin"),a=pe(n.maxWidth,t,"clientWidth")||T,r=pe(n.maxHeight,t,"clientHeight")||T,l=function(t,e,i){let s,n;if(void 0===e||void 0===i){const o=t&&ge(t);if(o){const t=o.getBoundingClientRect(),a=me(o),r=_e(a,"border","width"),l=_e(a,"padding");e=t.width-l.width-r.width,i=t.height-l.height-r.height,s=pe(a.maxWidth,o,"clientWidth"),n=pe(a.maxHeight,o,"clientHeight")}else e=t.clientWidth,i=t.clientHeight}return{width:e,height:i,maxWidth:s||T,maxHeight:n||T}}(t,e,i);let{width:h,height:c}=l;if("content-box"===n.boxSizing){const t=_e(n,"border","width"),e=_e(n,"padding");h-=e.width+t.width,c-=e.height+t.height}h=Math.max(0,h-o.width),c=Math.max(0,s?h/s:c-o.height),h=Me(Math.min(h,a,l.maxWidth)),c=Me(Math.min(c,r,l.maxHeight)),h&&!c&&(c=Me(h/2));return(void 0!==e||void 0!==i)&&s&&l.height&&c>l.height&&(c=l.height,h=Me(Math.floor(c*s))),{width:h,height:c}}function ke(t,e,i){const s=e||1,n=Me(t.height*s),o=Me(t.width*s);t.height=Me(t.height),t.width=Me(t.width);const a=t.canvas;return a.style&&(i||!a.style.height&&!a.style.width)&&(a.style.height=`${t.height}px`,a.style.width=`${t.width}px`),(t.currentDevicePixelRatio!==s||a.height!==n||a.width!==o)&&(t.currentDevicePixelRatio=s,a.height=n,a.width=o,t.ctx.setTransform(s,0,0,s,0,0),!0)}const Se=function(){let t=!1;try{const e={get passive(){return t=!0,!1}};fe()&&(window.addEventListener("test",null,e),window.removeEventListener("test",null,e))}catch(t){}return t}();function Pe(t,e){const i=xe(t,e),s=i&&i.match(/^(\d+)(\.\d+)?px$/);return s?+s[1]:void 0}function De(t){return!t||s(t.size)||s(t.family)?null:(t.style?t.style+" ":"")+(t.weight?t.weight+" ":"")+t.size+"px "+t.family}function Ce(t,e,i,s,n){let o=e[n];return o||(o=e[n]=t.measureText(n).width,i.push(n)),o>s&&(s=o),s}function Oe(t,e,i,s){let o=(s=s||{}).data=s.data||{},a=s.garbageCollect=s.garbageCollect||[];s.font!==e&&(o=s.data={},a=s.garbageCollect=[],s.font=e),t.save(),t.font=e;let r=0;const l=i.length;let h,c,d,u,f;for(h=0;hi.length){for(h=0;h0&&t.stroke()}}function Re(t,e,i){return i=i||.5,!e||t&&t.x>e.left-i&&t.xe.top-i&&t.y0&&""!==r.strokeColor;let c,d;for(t.save(),t.font=a.string,function(t,e){e.translation&&t.translate(e.translation[0],e.translation[1]),s(e.rotation)||t.rotate(e.rotation),e.color&&(t.fillStyle=e.color),e.textAlign&&(t.textAlign=e.textAlign),e.textBaseline&&(t.textBaseline=e.textBaseline)}(t,r),c=0;ct[0])){const o=i||t;void 0===s&&(s=ti("_fallback",t));const a={[Symbol.toStringTag]:"Object",_cacheable:!0,_scopes:t,_rootScopes:o,_fallback:s,_getTarget:n,override:i=>je([i,...t],e,o,s)};return new Proxy(a,{deleteProperty:(e,i)=>(delete e[i],delete e._keys,delete t[0][i],!0),get:(i,s)=>qe(i,s,(()=>function(t,e,i,s){let n;for(const o of e)if(n=ti(Ue(o,t),i),void 0!==n)return Xe(t,n)?Ze(i,s,t,n):n}(s,e,t,i))),getOwnPropertyDescriptor:(t,e)=>Reflect.getOwnPropertyDescriptor(t._scopes[0],e),getPrototypeOf:()=>Reflect.getPrototypeOf(t[0]),has:(t,e)=>ei(t).includes(e),ownKeys:t=>ei(t),set(t,e,i){const s=t._storage||(t._storage=n());return t[e]=s[e]=i,delete t._keys,!0}})}function $e(t,e,i,s){const a={_cacheable:!1,_proxy:t,_context:e,_subProxy:i,_stack:new Set,_descriptors:Ye(t,s),setContext:e=>$e(t,e,i,s),override:n=>$e(t.override(n),e,i,s)};return new Proxy(a,{deleteProperty:(e,i)=>(delete e[i],delete t[i],!0),get:(t,e,i)=>qe(t,e,(()=>function(t,e,i){const{_proxy:s,_context:a,_subProxy:r,_descriptors:l}=t;let h=s[e];S(h)&&l.isScriptable(e)&&(h=function(t,e,i,s){const{_proxy:n,_context:o,_subProxy:a,_stack:r}=i;if(r.has(t))throw new Error("Recursion detected: "+Array.from(r).join("->")+"->"+t);r.add(t);let l=e(o,a||s);r.delete(t),Xe(t,l)&&(l=Ze(n._scopes,n,t,l));return l}(e,h,t,i));n(h)&&h.length&&(h=function(t,e,i,s){const{_proxy:n,_context:a,_subProxy:r,_descriptors:l}=i;if(void 0!==a.index&&s(t))return e[a.index%e.length];if(o(e[0])){const i=e,s=n._scopes.filter((t=>t!==i));e=[];for(const o of i){const i=Ze(s,n,t,o);e.push($e(i,a,r&&r[t],l))}}return e}(e,h,t,l.isIndexable));Xe(e,h)&&(h=$e(h,a,r&&r[e],l));return h}(t,e,i))),getOwnPropertyDescriptor:(e,i)=>e._descriptors.allKeys?Reflect.has(t,i)?{enumerable:!0,configurable:!0}:void 0:Reflect.getOwnPropertyDescriptor(t,i),getPrototypeOf:()=>Reflect.getPrototypeOf(t),has:(e,i)=>Reflect.has(t,i),ownKeys:()=>Reflect.ownKeys(t),set:(e,i,s)=>(t[i]=s,delete e[i],!0)})}function Ye(t,e={scriptable:!0,indexable:!0}){const{_scriptable:i=e.scriptable,_indexable:s=e.indexable,_allKeys:n=e.allKeys}=t;return{allKeys:n,scriptable:i,indexable:s,isScriptable:S(i)?i:()=>i,isIndexable:S(s)?s:()=>s}}const Ue=(t,e)=>t?t+w(e):e,Xe=(t,e)=>o(e)&&"adapters"!==t&&(null===Object.getPrototypeOf(e)||e.constructor===Object);function qe(t,e,i){if(Object.prototype.hasOwnProperty.call(t,e)||"constructor"===e)return t[e];const s=i();return t[e]=s,s}function Ke(t,e,i){return S(t)?t(e,i):t}const Ge=(t,e)=>!0===t?e:"string"==typeof t?M(e,t):void 0;function Je(t,e,i,s,n){for(const o of e){const e=Ge(i,o);if(e){t.add(e);const o=Ke(e._fallback,i,n);if(void 0!==o&&o!==i&&o!==s)return o}else if(!1===e&&void 0!==s&&i!==s)return null}return!1}function Ze(t,e,i,s){const a=e._rootScopes,r=Ke(e._fallback,i,s),l=[...t,...a],h=new Set;h.add(s);let c=Qe(h,l,i,r||i,s);return null!==c&&((void 0===r||r===i||(c=Qe(h,l,r,c,s),null!==c))&&je(Array.from(h),[""],a,r,(()=>function(t,e,i){const s=t._getTarget();e in s||(s[e]={});const a=s[e];if(n(a)&&o(i))return i;return a||{}}(e,i,s))))}function Qe(t,e,i,s,n){for(;i;)i=Je(t,e,i,s,n);return i}function ti(t,e){for(const i of e){if(!i)continue;const e=i[t];if(void 0!==e)return e}}function ei(t){let e=t._keys;return e||(e=t._keys=function(t){const e=new Set;for(const i of t)for(const t of Object.keys(i).filter((t=>!t.startsWith("_"))))e.add(t);return Array.from(e)}(t._scopes)),e}function ii(t,e,i,s){const{iScale:n}=t,{key:o="r"}=this._parsing,a=new Array(s);let r,l,h,c;for(r=0,l=s;re"x"===t?"y":"x";function ai(t,e,i,s){const n=t.skip?e:t,o=e,a=i.skip?e:i,r=q(o,n),l=q(a,o);let h=r/(r+l),c=l/(r+l);h=isNaN(h)?0:h,c=isNaN(c)?0:c;const d=s*h,u=s*c;return{previous:{x:o.x-d*(a.x-n.x),y:o.y-d*(a.y-n.y)},next:{x:o.x+u*(a.x-n.x),y:o.y+u*(a.y-n.y)}}}function ri(t,e="x"){const i=oi(e),s=t.length,n=Array(s).fill(0),o=Array(s);let a,r,l,h=ni(t,0);for(a=0;a!t.skip))),"monotone"===e.cubicInterpolationMode)ri(t,n);else{let i=s?t[t.length-1]:t[0];for(o=0,a=t.length;o0===t||1===t,di=(t,e,i)=>-Math.pow(2,10*(t-=1))*Math.sin((t-e)*O/i),ui=(t,e,i)=>Math.pow(2,-10*t)*Math.sin((t-e)*O/i)+1,fi={linear:t=>t,easeInQuad:t=>t*t,easeOutQuad:t=>-t*(t-2),easeInOutQuad:t=>(t/=.5)<1?.5*t*t:-.5*(--t*(t-2)-1),easeInCubic:t=>t*t*t,easeOutCubic:t=>(t-=1)*t*t+1,easeInOutCubic:t=>(t/=.5)<1?.5*t*t*t:.5*((t-=2)*t*t+2),easeInQuart:t=>t*t*t*t,easeOutQuart:t=>-((t-=1)*t*t*t-1),easeInOutQuart:t=>(t/=.5)<1?.5*t*t*t*t:-.5*((t-=2)*t*t*t-2),easeInQuint:t=>t*t*t*t*t,easeOutQuint:t=>(t-=1)*t*t*t*t+1,easeInOutQuint:t=>(t/=.5)<1?.5*t*t*t*t*t:.5*((t-=2)*t*t*t*t+2),easeInSine:t=>1-Math.cos(t*E),easeOutSine:t=>Math.sin(t*E),easeInOutSine:t=>-.5*(Math.cos(C*t)-1),easeInExpo:t=>0===t?0:Math.pow(2,10*(t-1)),easeOutExpo:t=>1===t?1:1-Math.pow(2,-10*t),easeInOutExpo:t=>ci(t)?t:t<.5?.5*Math.pow(2,10*(2*t-1)):.5*(2-Math.pow(2,-10*(2*t-1))),easeInCirc:t=>t>=1?t:-(Math.sqrt(1-t*t)-1),easeOutCirc:t=>Math.sqrt(1-(t-=1)*t),easeInOutCirc:t=>(t/=.5)<1?-.5*(Math.sqrt(1-t*t)-1):.5*(Math.sqrt(1-(t-=2)*t)+1),easeInElastic:t=>ci(t)?t:di(t,.075,.3),easeOutElastic:t=>ci(t)?t:ui(t,.075,.3),easeInOutElastic(t){const e=.1125;return ci(t)?t:t<.5?.5*di(2*t,e,.45):.5+.5*ui(2*t-1,e,.45)},easeInBack(t){const e=1.70158;return t*t*((e+1)*t-e)},easeOutBack(t){const e=1.70158;return(t-=1)*t*((e+1)*t+e)+1},easeInOutBack(t){let e=1.70158;return(t/=.5)<1?t*t*((1+(e*=1.525))*t-e)*.5:.5*((t-=2)*t*((1+(e*=1.525))*t+e)+2)},easeInBounce:t=>1-fi.easeOutBounce(1-t),easeOutBounce(t){const e=7.5625,i=2.75;return t<1/i?e*t*t:t<2/i?e*(t-=1.5/i)*t+.75:t<2.5/i?e*(t-=2.25/i)*t+.9375:e*(t-=2.625/i)*t+.984375},easeInOutBounce:t=>t<.5?.5*fi.easeInBounce(2*t):.5*fi.easeOutBounce(2*t-1)+.5};function gi(t,e,i,s){return{x:t.x+i*(e.x-t.x),y:t.y+i*(e.y-t.y)}}function pi(t,e,i,s){return{x:t.x+i*(e.x-t.x),y:"middle"===s?i<.5?t.y:e.y:"after"===s?i<1?t.y:e.y:i>0?e.y:t.y}}function mi(t,e,i,s){const n={x:t.cp2x,y:t.cp2y},o={x:e.cp1x,y:e.cp1y},a=gi(t,n,i),r=gi(n,o,i),l=gi(o,e,i),h=gi(a,r,i),c=gi(r,l,i);return gi(h,c,i)}const xi=/^(normal|(\d+(?:\.\d+)?)(px|em|%)?)$/,bi=/^(normal|italic|initial|inherit|unset|(oblique( -?[0-9]?[0-9]deg)?))$/;function _i(t,e){const i=(""+t).match(xi);if(!i||"normal"===i[1])return 1.2*e;switch(t=+i[2],i[3]){case"px":return t;case"%":t/=100}return e*t}const yi=t=>+t||0;function vi(t,e){const i={},s=o(e),n=s?Object.keys(e):e,a=o(t)?s?i=>l(t[i],t[e[i]]):e=>t[e]:()=>t;for(const t of n)i[t]=yi(a(t));return i}function Mi(t){return vi(t,{top:"y",right:"x",bottom:"y",left:"x"})}function wi(t){return vi(t,["topLeft","topRight","bottomLeft","bottomRight"])}function ki(t){const e=Mi(t);return e.width=e.left+e.right,e.height=e.top+e.bottom,e}function Si(t,e){t=t||{},e=e||ue.font;let i=l(t.size,e.size);"string"==typeof i&&(i=parseInt(i,10));let s=l(t.style,e.style);s&&!(""+s).match(bi)&&(console.warn('Invalid font style specified: "'+s+'"'),s=void 0);const n={family:l(t.family,e.family),lineHeight:_i(l(t.lineHeight,e.lineHeight),i),size:i,style:s,weight:l(t.weight,e.weight),string:""};return n.string=De(n),n}function Pi(t,e,i,s){let o,a,r,l=!0;for(o=0,a=t.length;oi&&0===t?0:t+e;return{min:a(s,-Math.abs(o)),max:a(n,o)}}function Ci(t,e){return Object.assign(Object.create(t),e)}function Oi(t,e,i){return t?function(t,e){return{x:i=>t+t+e-i,setWidth(t){e=t},textAlign:t=>"center"===t?t:"right"===t?"left":"right",xPlus:(t,e)=>t-e,leftForLtr:(t,e)=>t-e}}(e,i):{x:t=>t,setWidth(t){},textAlign:t=>t,xPlus:(t,e)=>t+e,leftForLtr:(t,e)=>t}}function Ai(t,e){let i,s;"ltr"!==e&&"rtl"!==e||(i=t.canvas.style,s=[i.getPropertyValue("direction"),i.getPropertyPriority("direction")],i.setProperty("direction",e,"important"),t.prevTextDirection=s)}function Ti(t,e){void 0!==e&&(delete t.prevTextDirection,t.canvas.style.setProperty("direction",e[0],e[1]))}function Li(t){return"angle"===t?{between:J,compare:K,normalize:G}:{between:tt,compare:(t,e)=>t-e,normalize:t=>t}}function Ei({start:t,end:e,count:i,loop:s,style:n}){return{start:t%i,end:e%i,loop:s&&(e-t+1)%i==0,style:n}}function Ri(t,e,i){if(!i)return[t];const{property:s,start:n,end:o}=i,a=e.length,{compare:r,between:l,normalize:h}=Li(s),{start:c,end:d,loop:u,style:f}=function(t,e,i){const{property:s,start:n,end:o}=i,{between:a,normalize:r}=Li(s),l=e.length;let h,c,{start:d,end:u,loop:f}=t;if(f){for(d+=l,u+=l,h=0,c=l;hb||l(n,x,p)&&0!==r(n,x),v=()=>!b||0===r(o,p)||l(o,x,p);for(let t=c,i=c;t<=d;++t)m=e[t%a],m.skip||(p=h(m[s]),p!==x&&(b=l(p,n,o),null===_&&y()&&(_=0===r(p,n)?t:i),null!==_&&v()&&(g.push(Ei({start:_,end:t,loop:u,count:a,style:f})),_=null),i=t,x=p));return null!==_&&g.push(Ei({start:_,end:d,loop:u,count:a,style:f})),g}function Ii(t,e){const i=[],s=t.segments;for(let n=0;nn&&t[o%e].skip;)o--;return o%=e,{start:n,end:o}}(i,n,o,s);if(!0===s)return Fi(t,[{start:a,end:r,loop:o}],i,e);return Fi(t,function(t,e,i,s){const n=t.length,o=[];let a,r=e,l=t[e];for(a=e+1;a<=i;++a){const i=t[a%n];i.skip||i.stop?l.skip||(s=!1,o.push({start:e%n,end:(a-1)%n,loop:s}),e=r=i.stop?a:null):(r=a,l.skip&&(e=a)),l=i}return null!==r&&o.push({start:e%n,end:r%n,loop:s}),o}(i,a,r!s(t[e.axis])));n.lo-=Math.max(0,a);const r=i.slice(n.hi).findIndex((t=>!s(t[e.axis])));n.hi+=Math.max(0,r)}return n}if(o._sharedOptions){const t=a[0],s="function"==typeof t.getRange&&t.getRange(e);if(s){const t=r(a,e,i-s),n=r(a,e,i+s);return{lo:t.lo,hi:n.hi}}}}return{lo:0,hi:a.length-1}}function $i(t,e,i,s,n){const o=t.getSortedVisibleDatasetMetas(),a=i[e];for(let t=0,i=o.length;t{t[a]&&t[a](e[i],n)&&(o.push({element:t,datasetIndex:s,index:l}),r=r||t.inRange(e.x,e.y,n))})),s&&!r?[]:o}var Ki={evaluateInteractionItems:$i,modes:{index(t,e,i,s){const n=ve(e,t),o=i.axis||"x",a=i.includeInvisible||!1,r=i.intersect?Yi(t,n,o,s,a):Xi(t,n,o,!1,s,a),l=[];return r.length?(t.getSortedVisibleDatasetMetas().forEach((t=>{const e=r[0].index,i=t.data[e];i&&!i.skip&&l.push({element:i,datasetIndex:t.index,index:e})})),l):[]},dataset(t,e,i,s){const n=ve(e,t),o=i.axis||"xy",a=i.includeInvisible||!1;let r=i.intersect?Yi(t,n,o,s,a):Xi(t,n,o,!1,s,a);if(r.length>0){const e=r[0].datasetIndex,i=t.getDatasetMeta(e).data;r=[];for(let t=0;tYi(t,ve(e,t),i.axis||"xy",s,i.includeInvisible||!1),nearest(t,e,i,s){const n=ve(e,t),o=i.axis||"xy",a=i.includeInvisible||!1;return Xi(t,n,o,i.intersect,s,a)},x:(t,e,i,s)=>qi(t,ve(e,t),"x",i.intersect,s),y:(t,e,i,s)=>qi(t,ve(e,t),"y",i.intersect,s)}};const Gi=["left","top","right","bottom"];function Ji(t,e){return t.filter((t=>t.pos===e))}function Zi(t,e){return t.filter((t=>-1===Gi.indexOf(t.pos)&&t.box.axis===e))}function Qi(t,e){return t.sort(((t,i)=>{const s=e?i:t,n=e?t:i;return s.weight===n.weight?s.index-n.index:s.weight-n.weight}))}function ts(t,e){const i=function(t){const e={};for(const i of t){const{stack:t,pos:s,stackWeight:n}=i;if(!t||!Gi.includes(s))continue;const o=e[t]||(e[t]={count:0,placed:0,weight:0,size:0});o.count++,o.weight+=n}return e}(t),{vBoxMaxWidth:s,hBoxMaxHeight:n}=e;let o,a,r;for(o=0,a=t.length;o{s[t]=Math.max(e[t],i[t])})),s}return s(t?["left","right"]:["top","bottom"])}function os(t,e,i,s){const n=[];let o,a,r,l,h,c;for(o=0,a=t.length,h=0;ot.box.fullSize)),!0),s=Qi(Ji(e,"left"),!0),n=Qi(Ji(e,"right")),o=Qi(Ji(e,"top"),!0),a=Qi(Ji(e,"bottom")),r=Zi(e,"x"),l=Zi(e,"y");return{fullSize:i,leftAndTop:s.concat(o),rightAndBottom:n.concat(l).concat(a).concat(r),chartArea:Ji(e,"chartArea"),vertical:s.concat(n).concat(l),horizontal:o.concat(a).concat(r)}}(t.boxes),l=r.vertical,h=r.horizontal;u(t.boxes,(t=>{"function"==typeof t.beforeLayout&&t.beforeLayout()}));const c=l.reduce(((t,e)=>e.box.options&&!1===e.box.options.display?t:t+1),0)||1,d=Object.freeze({outerWidth:e,outerHeight:i,padding:n,availableWidth:o,availableHeight:a,vBoxMaxWidth:o/2/c,hBoxMaxHeight:a/2}),f=Object.assign({},n);is(f,ki(s));const g=Object.assign({maxPadding:f,w:o,h:a,x:n.left,y:n.top},n),p=ts(l.concat(h),d);os(r.fullSize,g,d,p),os(l,g,d,p),os(h,g,d,p)&&os(l,g,d,p),function(t){const e=t.maxPadding;function i(i){const s=Math.max(e[i]-t[i],0);return t[i]+=s,s}t.y+=i("top"),t.x+=i("left"),i("right"),i("bottom")}(g),rs(r.leftAndTop,g,d,p),g.x+=g.w,g.y+=g.h,rs(r.rightAndBottom,g,d,p),t.chartArea={left:g.left,top:g.top,right:g.left+g.w,bottom:g.top+g.h,height:g.h,width:g.w},u(r.chartArea,(e=>{const i=e.box;Object.assign(i,t.chartArea),i.update(g.w,g.h,{left:0,top:0,right:0,bottom:0})}))}};class hs{acquireContext(t,e){}releaseContext(t){return!1}addEventListener(t,e,i){}removeEventListener(t,e,i){}getDevicePixelRatio(){return 1}getMaximumSize(t,e,i,s){return e=Math.max(0,e||t.width),i=i||t.height,{width:e,height:Math.max(0,s?Math.floor(e/s):i)}}isAttached(t){return!0}updateConfig(t){}}class cs extends hs{acquireContext(t){return t&&t.getContext&&t.getContext("2d")||null}updateConfig(t){t.options.animation=!1}}const ds="$chartjs",us={touchstart:"mousedown",touchmove:"mousemove",touchend:"mouseup",pointerenter:"mouseenter",pointerdown:"mousedown",pointermove:"mousemove",pointerup:"mouseup",pointerleave:"mouseout",pointerout:"mouseout"},fs=t=>null===t||""===t;const gs=!!Se&&{passive:!0};function ps(t,e,i){t&&t.canvas&&t.canvas.removeEventListener(e,i,gs)}function ms(t,e){for(const i of t)if(i===e||i.contains(e))return!0}function xs(t,e,i){const s=t.canvas,n=new MutationObserver((t=>{let e=!1;for(const i of t)e=e||ms(i.addedNodes,s),e=e&&!ms(i.removedNodes,s);e&&i()}));return n.observe(document,{childList:!0,subtree:!0}),n}function bs(t,e,i){const s=t.canvas,n=new MutationObserver((t=>{let e=!1;for(const i of t)e=e||ms(i.removedNodes,s),e=e&&!ms(i.addedNodes,s);e&&i()}));return n.observe(document,{childList:!0,subtree:!0}),n}const _s=new Map;let ys=0;function vs(){const t=window.devicePixelRatio;t!==ys&&(ys=t,_s.forEach(((e,i)=>{i.currentDevicePixelRatio!==t&&e()})))}function Ms(t,e,i){const s=t.canvas,n=s&&ge(s);if(!n)return;const o=ct(((t,e)=>{const s=n.clientWidth;i(t,e),s{const e=t[0],i=e.contentRect.width,s=e.contentRect.height;0===i&&0===s||o(i,s)}));return a.observe(n),function(t,e){_s.size||window.addEventListener("resize",vs),_s.set(t,e)}(t,o),a}function ws(t,e,i){i&&i.disconnect(),"resize"===e&&function(t){_s.delete(t),_s.size||window.removeEventListener("resize",vs)}(t)}function ks(t,e,i){const s=t.canvas,n=ct((e=>{null!==t.ctx&&i(function(t,e){const i=us[t.type]||t.type,{x:s,y:n}=ve(t,e);return{type:i,chart:e,native:t,x:void 0!==s?s:null,y:void 0!==n?n:null}}(e,t))}),t);return function(t,e,i){t&&t.addEventListener(e,i,gs)}(s,e,n),n}class Ss extends hs{acquireContext(t,e){const i=t&&t.getContext&&t.getContext("2d");return i&&i.canvas===t?(function(t,e){const i=t.style,s=t.getAttribute("height"),n=t.getAttribute("width");if(t[ds]={initial:{height:s,width:n,style:{display:i.display,height:i.height,width:i.width}}},i.display=i.display||"block",i.boxSizing=i.boxSizing||"border-box",fs(n)){const e=Pe(t,"width");void 0!==e&&(t.width=e)}if(fs(s))if(""===t.style.height)t.height=t.width/(e||2);else{const e=Pe(t,"height");void 0!==e&&(t.height=e)}}(t,e),i):null}releaseContext(t){const e=t.canvas;if(!e[ds])return!1;const i=e[ds].initial;["height","width"].forEach((t=>{const n=i[t];s(n)?e.removeAttribute(t):e.setAttribute(t,n)}));const n=i.style||{};return Object.keys(n).forEach((t=>{e.style[t]=n[t]})),e.width=e.width,delete e[ds],!0}addEventListener(t,e,i){this.removeEventListener(t,e);const s=t.$proxies||(t.$proxies={}),n={attach:xs,detach:bs,resize:Ms}[e]||ks;s[e]=n(t,e,i)}removeEventListener(t,e){const i=t.$proxies||(t.$proxies={}),s=i[e];if(!s)return;({attach:ws,detach:ws,resize:ws}[e]||ps)(t,e,s),i[e]=void 0}getDevicePixelRatio(){return window.devicePixelRatio}getMaximumSize(t,e,i,s){return we(t,e,i,s)}isAttached(t){const e=t&&ge(t);return!(!e||!e.isConnected)}}function Ps(t){return!fe()||"undefined"!=typeof OffscreenCanvas&&t instanceof OffscreenCanvas?cs:Ss}var Ds=Object.freeze({__proto__:null,BasePlatform:hs,BasicPlatform:cs,DomPlatform:Ss,_detectPlatform:Ps});const Cs="transparent",Os={boolean:(t,e,i)=>i>.5?e:t,color(t,e,i){const s=Qt(t||Cs),n=s.valid&&Qt(e||Cs);return n&&n.valid?n.mix(s,i).hexString():e},number:(t,e,i)=>t+(e-t)*i};class As{constructor(t,e,i,s){const n=e[i];s=Pi([t.to,s,n,t.from]);const o=Pi([t.from,n,s]);this._active=!0,this._fn=t.fn||Os[t.type||typeof o],this._easing=fi[t.easing]||fi.linear,this._start=Math.floor(Date.now()+(t.delay||0)),this._duration=this._total=Math.floor(t.duration),this._loop=!!t.loop,this._target=e,this._prop=i,this._from=o,this._to=s,this._promises=void 0}active(){return this._active}update(t,e,i){if(this._active){this._notify(!1);const s=this._target[this._prop],n=i-this._start,o=this._duration-n;this._start=i,this._duration=Math.floor(Math.max(o,t.duration)),this._total+=n,this._loop=!!t.loop,this._to=Pi([t.to,e,s,t.from]),this._from=Pi([t.from,s,e])}}cancel(){this._active&&(this.tick(Date.now()),this._active=!1,this._notify(!1))}tick(t){const e=t-this._start,i=this._duration,s=this._prop,n=this._from,o=this._loop,a=this._to;let r;if(this._active=n!==a&&(o||e1?2-r:r,r=this._easing(Math.min(1,Math.max(0,r))),this._target[s]=this._fn(n,a,r))}wait(){const t=this._promises||(this._promises=[]);return new Promise(((e,i)=>{t.push({res:e,rej:i})}))}_notify(t){const e=t?"res":"rej",i=this._promises||[];for(let t=0;t{const a=t[s];if(!o(a))return;const r={};for(const t of e)r[t]=a[t];(n(a.properties)&&a.properties||[s]).forEach((t=>{t!==s&&i.has(t)||i.set(t,r)}))}))}_animateOptions(t,e){const i=e.options,s=function(t,e){if(!e)return;let i=t.options;if(!i)return void(t.options=e);i.$shared&&(t.options=i=Object.assign({},i,{$shared:!1,$animations:{}}));return i}(t,i);if(!s)return[];const n=this._createAnimations(s,i);return i.$shared&&function(t,e){const i=[],s=Object.keys(e);for(let e=0;e{t.options=i}),(()=>{})),n}_createAnimations(t,e){const i=this._properties,s=[],n=t.$animations||(t.$animations={}),o=Object.keys(e),a=Date.now();let r;for(r=o.length-1;r>=0;--r){const l=o[r];if("$"===l.charAt(0))continue;if("options"===l){s.push(...this._animateOptions(t,e));continue}const h=e[l];let c=n[l];const d=i.get(l);if(c){if(d&&c.active()){c.update(d,h,a);continue}c.cancel()}d&&d.duration?(n[l]=c=new As(d,t,l,h),s.push(c)):t[l]=h}return s}update(t,e){if(0===this._properties.size)return void Object.assign(t,e);const i=this._createAnimations(t,e);return i.length?(bt.add(this._chart,i),!0):void 0}}function Ls(t,e){const i=t&&t.options||{},s=i.reverse,n=void 0===i.min?e:0,o=void 0===i.max?e:0;return{start:s?o:n,end:s?n:o}}function Es(t,e){const i=[],s=t._getSortedDatasetMetas(e);let n,o;for(n=0,o=s.length;n0||!i&&e<0)return n.index}return null}function Vs(t,e){const{chart:i,_cachedMeta:s}=t,n=i._stacks||(i._stacks={}),{iScale:o,vScale:a,index:r}=s,l=o.axis,h=a.axis,c=function(t,e,i){return`${t.id}.${e.id}.${i.stack||i.type}`}(o,a,s),d=e.length;let u;for(let t=0;ti[t].axis===e)).shift()}function Ws(t,e){const i=t.controller.index,s=t.vScale&&t.vScale.axis;if(s){e=e||t._parsed;for(const t of e){const e=t._stacks;if(!e||void 0===e[s]||void 0===e[s][i])return;delete e[s][i],void 0!==e[s]._visualValues&&void 0!==e[s]._visualValues[i]&&delete e[s]._visualValues[i]}}}const Ns=t=>"reset"===t||"none"===t,Hs=(t,e)=>e?t:Object.assign({},t);class js{static defaults={};static datasetElementType=null;static dataElementType=null;constructor(t,e){this.chart=t,this._ctx=t.ctx,this.index=e,this._cachedDataOpts={},this._cachedMeta=this.getMeta(),this._type=this._cachedMeta.type,this.options=void 0,this._parsing=!1,this._data=void 0,this._objectData=void 0,this._sharedOptions=void 0,this._drawStart=void 0,this._drawCount=void 0,this.enableOptionSharing=!1,this.supportsDecimation=!1,this.$context=void 0,this._syncList=[],this.datasetElementType=new.target.datasetElementType,this.dataElementType=new.target.dataElementType,this.initialize()}initialize(){const t=this._cachedMeta;this.configure(),this.linkScales(),t._stacked=Is(t.vScale,t),this.addElements(),this.options.fill&&!this.chart.isPluginEnabled("filler")&&console.warn("Tried to use the 'fill' option without the 'Filler' plugin enabled. Please import and register the 'Filler' plugin and make sure it is not disabled in the options")}updateIndex(t){this.index!==t&&Ws(this._cachedMeta),this.index=t}linkScales(){const t=this.chart,e=this._cachedMeta,i=this.getDataset(),s=(t,e,i,s)=>"x"===t?e:"r"===t?s:i,n=e.xAxisID=l(i.xAxisID,Bs(t,"x")),o=e.yAxisID=l(i.yAxisID,Bs(t,"y")),a=e.rAxisID=l(i.rAxisID,Bs(t,"r")),r=e.indexAxis,h=e.iAxisID=s(r,n,o,a),c=e.vAxisID=s(r,o,n,a);e.xScale=this.getScaleForId(n),e.yScale=this.getScaleForId(o),e.rScale=this.getScaleForId(a),e.iScale=this.getScaleForId(h),e.vScale=this.getScaleForId(c)}getDataset(){return this.chart.data.datasets[this.index]}getMeta(){return this.chart.getDatasetMeta(this.index)}getScaleForId(t){return this.chart.scales[t]}_getOtherScale(t){const e=this._cachedMeta;return t===e.iScale?e.vScale:e.iScale}reset(){this._update("reset")}_destroy(){const t=this._cachedMeta;this._data&&rt(this._data,this),t._stacked&&Ws(t)}_dataCheck(){const t=this.getDataset(),e=t.data||(t.data=[]),i=this._data;if(o(e)){const t=this._cachedMeta;this._data=function(t,e){const{iScale:i,vScale:s}=e,n="x"===i.axis?"x":"y",o="x"===s.axis?"x":"y",a=Object.keys(t),r=new Array(a.length);let l,h,c;for(l=0,h=a.length;l0&&i._parsed[t-1];if(!1===this._parsing)i._parsed=s,i._sorted=!0,d=s;else{d=n(s[t])?this.parseArrayData(i,s,t,e):o(s[t])?this.parseObjectData(i,s,t,e):this.parsePrimitiveData(i,s,t,e);const a=()=>null===c[l]||f&&c[l]t&&!e.hidden&&e._stacked&&{keys:Es(i,!0),values:null})(e,i,this.chart),h={min:Number.POSITIVE_INFINITY,max:Number.NEGATIVE_INFINITY},{min:c,max:d}=function(t){const{min:e,max:i,minDefined:s,maxDefined:n}=t.getUserBounds();return{min:s?e:Number.NEGATIVE_INFINITY,max:n?i:Number.POSITIVE_INFINITY}}(r);let u,f;function g(){f=s[u];const e=f[r.axis];return!a(f[t.axis])||c>e||d=0;--u)if(!g()){this.updateRangeFromParsed(h,t,f,l);break}return h}getAllParsedValues(t){const e=this._cachedMeta._parsed,i=[];let s,n,o;for(s=0,n=e.length;s=0&&tthis.getContext(i,s,e)),c);return f.$shared&&(f.$shared=r,n[o]=Object.freeze(Hs(f,r))),f}_resolveAnimations(t,e,i){const s=this.chart,n=this._cachedDataOpts,o=`animation-${e}`,a=n[o];if(a)return a;let r;if(!1!==s.options.animation){const s=this.chart.config,n=s.datasetAnimationScopeKeys(this._type,e),o=s.getOptionScopes(this.getDataset(),n);r=s.createResolver(o,this.getContext(t,i,e))}const l=new Ts(s,r&&r.animations);return r&&r._cacheable&&(n[o]=Object.freeze(l)),l}getSharedOptions(t){if(t.$shared)return this._sharedOptions||(this._sharedOptions=Object.assign({},t))}includeOptions(t,e){return!e||Ns(t)||this.chart._animationsDisabled}_getSharedOptions(t,e){const i=this.resolveDataElementOptions(t,e),s=this._sharedOptions,n=this.getSharedOptions(i),o=this.includeOptions(e,n)||n!==s;return this.updateSharedOptions(n,e,i),{sharedOptions:n,includeOptions:o}}updateElement(t,e,i,s){Ns(s)?Object.assign(t,i):this._resolveAnimations(e,s).update(t,i)}updateSharedOptions(t,e,i){t&&!Ns(e)&&this._resolveAnimations(void 0,e).update(t,i)}_setStyle(t,e,i,s){t.active=s;const n=this.getStyle(e,s);this._resolveAnimations(e,i,s).update(t,{options:!s&&this.getSharedOptions(n)||n})}removeHoverStyle(t,e,i){this._setStyle(t,i,"active",!1)}setHoverStyle(t,e,i){this._setStyle(t,i,"active",!0)}_removeDatasetHoverStyle(){const t=this._cachedMeta.dataset;t&&this._setStyle(t,void 0,"active",!1)}_setDatasetHoverStyle(){const t=this._cachedMeta.dataset;t&&this._setStyle(t,void 0,"active",!0)}_resyncElements(t){const e=this._data,i=this._cachedMeta.data;for(const[t,e,i]of this._syncList)this[t](e,i);this._syncList=[];const s=i.length,n=e.length,o=Math.min(n,s);o&&this.parse(0,o),n>s?this._insertElements(s,n-s,t):n{for(t.length+=e,a=t.length-1;a>=o;a--)t[a]=t[a-e]};for(r(n),a=t;a{s[t]=i[t]&&i[t].active()?i[t]._to:this[t]})),s}}function Ys(t,e){const i=t.options.ticks,n=function(t){const e=t.options.offset,i=t._tickSize(),s=t._length/i+(e?0:1),n=t._maxLength/i;return Math.floor(Math.min(s,n))}(t),o=Math.min(i.maxTicksLimit||n,n),a=i.major.enabled?function(t){const e=[];let i,s;for(i=0,s=t.length;io)return function(t,e,i,s){let n,o=0,a=i[0];for(s=Math.ceil(s),n=0;nn)return e}return Math.max(n,1)}(a,e,o);if(r>0){let t,i;const n=r>1?Math.round((h-l)/(r-1)):null;for(Us(e,c,d,s(n)?0:l-n,l),t=0,i=r-1;t"top"===e||"left"===e?t[e]+i:t[e]-i,qs=(t,e)=>Math.min(e||t,t);function Ks(t,e){const i=[],s=t.length/e,n=t.length;let o=0;for(;oa+r)))return h}function Js(t){return t.drawTicks?t.tickLength:0}function Zs(t,e){if(!t.display)return 0;const i=Si(t.font,e),s=ki(t.padding);return(n(t.text)?t.text.length:1)*i.lineHeight+s.height}function Qs(t,e,i){let s=ut(t);return(i&&"right"!==e||!i&&"right"===e)&&(s=(t=>"left"===t?"right":"right"===t?"left":t)(s)),s}class tn extends $s{constructor(t){super(),this.id=t.id,this.type=t.type,this.options=void 0,this.ctx=t.ctx,this.chart=t.chart,this.top=void 0,this.bottom=void 0,this.left=void 0,this.right=void 0,this.width=void 0,this.height=void 0,this._margins={left:0,right:0,top:0,bottom:0},this.maxWidth=void 0,this.maxHeight=void 0,this.paddingTop=void 0,this.paddingBottom=void 0,this.paddingLeft=void 0,this.paddingRight=void 0,this.axis=void 0,this.labelRotation=void 0,this.min=void 0,this.max=void 0,this._range=void 0,this.ticks=[],this._gridLineItems=null,this._labelItems=null,this._labelSizes=null,this._length=0,this._maxLength=0,this._longestTextCache={},this._startPixel=void 0,this._endPixel=void 0,this._reversePixels=!1,this._userMax=void 0,this._userMin=void 0,this._suggestedMax=void 0,this._suggestedMin=void 0,this._ticksLength=0,this._borderValue=0,this._cache={},this._dataLimitsCached=!1,this.$context=void 0}init(t){this.options=t.setContext(this.getContext()),this.axis=t.axis,this._userMin=this.parse(t.min),this._userMax=this.parse(t.max),this._suggestedMin=this.parse(t.suggestedMin),this._suggestedMax=this.parse(t.suggestedMax)}parse(t,e){return t}getUserBounds(){let{_userMin:t,_userMax:e,_suggestedMin:i,_suggestedMax:s}=this;return t=r(t,Number.POSITIVE_INFINITY),e=r(e,Number.NEGATIVE_INFINITY),i=r(i,Number.POSITIVE_INFINITY),s=r(s,Number.NEGATIVE_INFINITY),{min:r(t,i),max:r(e,s),minDefined:a(t),maxDefined:a(e)}}getMinMax(t){let e,{min:i,max:s,minDefined:n,maxDefined:o}=this.getUserBounds();if(n&&o)return{min:i,max:s};const a=this.getMatchingVisibleMetas();for(let r=0,l=a.length;rs?s:i,s=n&&i>s?i:s,{min:r(i,r(s,i)),max:r(s,r(i,s))}}getPadding(){return{left:this.paddingLeft||0,top:this.paddingTop||0,right:this.paddingRight||0,bottom:this.paddingBottom||0}}getTicks(){return this.ticks}getLabels(){const t=this.chart.data;return this.options.labels||(this.isHorizontal()?t.xLabels:t.yLabels)||t.labels||[]}getLabelItems(t=this.chart.chartArea){return this._labelItems||(this._labelItems=this._computeLabelItems(t))}beforeLayout(){this._cache={},this._dataLimitsCached=!1}beforeUpdate(){d(this.options.beforeUpdate,[this])}update(t,e,i){const{beginAtZero:s,grace:n,ticks:o}=this.options,a=o.sampleSize;this.beforeUpdate(),this.maxWidth=t,this.maxHeight=e,this._margins=i=Object.assign({left:0,right:0,top:0,bottom:0},i),this.ticks=null,this._labelSizes=null,this._gridLineItems=null,this._labelItems=null,this.beforeSetDimensions(),this.setDimensions(),this.afterSetDimensions(),this._maxLength=this.isHorizontal()?this.width+i.left+i.right:this.height+i.top+i.bottom,this._dataLimitsCached||(this.beforeDataLimits(),this.determineDataLimits(),this.afterDataLimits(),this._range=Di(this,n,s),this._dataLimitsCached=!0),this.beforeBuildTicks(),this.ticks=this.buildTicks()||[],this.afterBuildTicks();const r=a=n||i<=1||!this.isHorizontal())return void(this.labelRotation=s);const h=this._getLabelSizes(),c=h.widest.width,d=h.highest.height,u=Z(this.chart.width-c,0,this.maxWidth);o=t.offset?this.maxWidth/i:u/(i-1),c+6>o&&(o=u/(i-(t.offset?.5:1)),a=this.maxHeight-Js(t.grid)-e.padding-Zs(t.title,this.chart.options.font),r=Math.sqrt(c*c+d*d),l=Y(Math.min(Math.asin(Z((h.highest.height+6)/o,-1,1)),Math.asin(Z(a/r,-1,1))-Math.asin(Z(d/r,-1,1)))),l=Math.max(s,Math.min(n,l))),this.labelRotation=l}afterCalculateLabelRotation(){d(this.options.afterCalculateLabelRotation,[this])}afterAutoSkip(){}beforeFit(){d(this.options.beforeFit,[this])}fit(){const t={width:0,height:0},{chart:e,options:{ticks:i,title:s,grid:n}}=this,o=this._isVisible(),a=this.isHorizontal();if(o){const o=Zs(s,e.options.font);if(a?(t.width=this.maxWidth,t.height=Js(n)+o):(t.height=this.maxHeight,t.width=Js(n)+o),i.display&&this.ticks.length){const{first:e,last:s,widest:n,highest:o}=this._getLabelSizes(),r=2*i.padding,l=$(this.labelRotation),h=Math.cos(l),c=Math.sin(l);if(a){const e=i.mirror?0:c*n.width+h*o.height;t.height=Math.min(this.maxHeight,t.height+e+r)}else{const e=i.mirror?0:h*n.width+c*o.height;t.width=Math.min(this.maxWidth,t.width+e+r)}this._calculatePadding(e,s,c,h)}}this._handleMargins(),a?(this.width=this._length=e.width-this._margins.left-this._margins.right,this.height=t.height):(this.width=t.width,this.height=this._length=e.height-this._margins.top-this._margins.bottom)}_calculatePadding(t,e,i,s){const{ticks:{align:n,padding:o},position:a}=this.options,r=0!==this.labelRotation,l="top"!==a&&"x"===this.axis;if(this.isHorizontal()){const a=this.getPixelForTick(0)-this.left,h=this.right-this.getPixelForTick(this.ticks.length-1);let c=0,d=0;r?l?(c=s*t.width,d=i*e.height):(c=i*t.height,d=s*e.width):"start"===n?d=e.width:"end"===n?c=t.width:"inner"!==n&&(c=t.width/2,d=e.width/2),this.paddingLeft=Math.max((c-a+o)*this.width/(this.width-a),0),this.paddingRight=Math.max((d-h+o)*this.width/(this.width-h),0)}else{let i=e.height/2,s=t.height/2;"start"===n?(i=0,s=t.height):"end"===n&&(i=e.height,s=0),this.paddingTop=i+o,this.paddingBottom=s+o}}_handleMargins(){this._margins&&(this._margins.left=Math.max(this.paddingLeft,this._margins.left),this._margins.top=Math.max(this.paddingTop,this._margins.top),this._margins.right=Math.max(this.paddingRight,this._margins.right),this._margins.bottom=Math.max(this.paddingBottom,this._margins.bottom))}afterFit(){d(this.options.afterFit,[this])}isHorizontal(){const{axis:t,position:e}=this.options;return"top"===e||"bottom"===e||"x"===t}isFullSize(){return this.options.fullSize}_convertTicksToLabels(t){let e,i;for(this.beforeTickToLabelConversion(),this.generateTickLabels(t),e=0,i=t.length;e{const i=t.gc,s=i.length/2;let n;if(s>e){for(n=0;n({width:r[t]||0,height:l[t]||0});return{first:P(0),last:P(e-1),widest:P(k),highest:P(S),widths:r,heights:l}}getLabelForValue(t){return t}getPixelForValue(t,e){return NaN}getValueForPixel(t){}getPixelForTick(t){const e=this.ticks;return t<0||t>e.length-1?null:this.getPixelForValue(e[t].value)}getPixelForDecimal(t){this._reversePixels&&(t=1-t);const e=this._startPixel+t*this._length;return Q(this._alignToPixels?Ae(this.chart,e,0):e)}getDecimalForPixel(t){const e=(t-this._startPixel)/this._length;return this._reversePixels?1-e:e}getBasePixel(){return this.getPixelForValue(this.getBaseValue())}getBaseValue(){const{min:t,max:e}=this;return t<0&&e<0?e:t>0&&e>0?t:0}getContext(t){const e=this.ticks||[];if(t>=0&&ta*s?a/i:r/s:r*s0}_computeGridLineItems(t){const e=this.axis,i=this.chart,s=this.options,{grid:n,position:a,border:r}=s,h=n.offset,c=this.isHorizontal(),d=this.ticks.length+(h?1:0),u=Js(n),f=[],g=r.setContext(this.getContext()),p=g.display?g.width:0,m=p/2,x=function(t){return Ae(i,t,p)};let b,_,y,v,M,w,k,S,P,D,C,O;if("top"===a)b=x(this.bottom),w=this.bottom-u,S=b-m,D=x(t.top)+m,O=t.bottom;else if("bottom"===a)b=x(this.top),D=t.top,O=x(t.bottom)-m,w=b+m,S=this.top+u;else if("left"===a)b=x(this.right),M=this.right-u,k=b-m,P=x(t.left)+m,C=t.right;else if("right"===a)b=x(this.left),P=t.left,C=x(t.right)-m,M=b+m,k=this.left+u;else if("x"===e){if("center"===a)b=x((t.top+t.bottom)/2+.5);else if(o(a)){const t=Object.keys(a)[0],e=a[t];b=x(this.chart.scales[t].getPixelForValue(e))}D=t.top,O=t.bottom,w=b+m,S=w+u}else if("y"===e){if("center"===a)b=x((t.left+t.right)/2);else if(o(a)){const t=Object.keys(a)[0],e=a[t];b=x(this.chart.scales[t].getPixelForValue(e))}M=b-m,k=M-u,P=t.left,C=t.right}const A=l(s.ticks.maxTicksLimit,d),T=Math.max(1,Math.ceil(d/A));for(_=0;_0&&(o-=s/2)}d={left:o,top:n,width:s+e.width,height:i+e.height,color:t.backdropColor}}x.push({label:v,font:P,textOffset:O,options:{rotation:m,color:i,strokeColor:o,strokeWidth:h,textAlign:f,textBaseline:A,translation:[M,w],backdrop:d}})}return x}_getXAxisLabelAlignment(){const{position:t,ticks:e}=this.options;if(-$(this.labelRotation))return"top"===t?"left":"right";let i="center";return"start"===e.align?i="left":"end"===e.align?i="right":"inner"===e.align&&(i="inner"),i}_getYAxisLabelAlignment(t){const{position:e,ticks:{crossAlign:i,mirror:s,padding:n}}=this.options,o=t+n,a=this._getLabelSizes().widest.width;let r,l;return"left"===e?s?(l=this.right+n,"near"===i?r="left":"center"===i?(r="center",l+=a/2):(r="right",l+=a)):(l=this.right-o,"near"===i?r="right":"center"===i?(r="center",l-=a/2):(r="left",l=this.left)):"right"===e?s?(l=this.left+n,"near"===i?r="right":"center"===i?(r="center",l-=a/2):(r="left",l-=a)):(l=this.left+o,"near"===i?r="left":"center"===i?(r="center",l+=a/2):(r="right",l=this.right)):r="right",{textAlign:r,x:l}}_computeLabelArea(){if(this.options.ticks.mirror)return;const t=this.chart,e=this.options.position;return"left"===e||"right"===e?{top:0,left:this.left,bottom:t.height,right:this.right}:"top"===e||"bottom"===e?{top:this.top,left:0,bottom:this.bottom,right:t.width}:void 0}drawBackground(){const{ctx:t,options:{backgroundColor:e},left:i,top:s,width:n,height:o}=this;e&&(t.save(),t.fillStyle=e,t.fillRect(i,s,n,o),t.restore())}getLineWidthForValue(t){const e=this.options.grid;if(!this._isVisible()||!e.display)return 0;const i=this.ticks.findIndex((e=>e.value===t));if(i>=0){return e.setContext(this.getContext(i)).lineWidth}return 0}drawGrid(t){const e=this.options.grid,i=this.ctx,s=this._gridLineItems||(this._gridLineItems=this._computeGridLineItems(t));let n,o;const a=(t,e,s)=>{s.width&&s.color&&(i.save(),i.lineWidth=s.width,i.strokeStyle=s.color,i.setLineDash(s.borderDash||[]),i.lineDashOffset=s.borderDashOffset,i.beginPath(),i.moveTo(t.x,t.y),i.lineTo(e.x,e.y),i.stroke(),i.restore())};if(e.display)for(n=0,o=s.length;n{this.drawBackground(),this.drawGrid(t),this.drawTitle()}},{z:s,draw:()=>{this.drawBorder()}},{z:e,draw:t=>{this.drawLabels(t)}}]:[{z:e,draw:t=>{this.draw(t)}}]}getMatchingVisibleMetas(t){const e=this.chart.getSortedVisibleDatasetMetas(),i=this.axis+"AxisID",s=[];let n,o;for(n=0,o=e.length;n{const s=i.split("."),n=s.pop(),o=[t].concat(s).join("."),a=e[i].split("."),r=a.pop(),l=a.join(".");ue.route(o,n,l,r)}))}(e,t.defaultRoutes);t.descriptors&&ue.describe(e,t.descriptors)}(t,o,i),this.override&&ue.override(t.id,t.overrides)),o}get(t){return this.items[t]}unregister(t){const e=this.items,i=t.id,s=this.scope;i in e&&delete e[i],s&&i in ue[s]&&(delete ue[s][i],this.override&&delete re[i])}}class sn{constructor(){this.controllers=new en(js,"datasets",!0),this.elements=new en($s,"elements"),this.plugins=new en(Object,"plugins"),this.scales=new en(tn,"scales"),this._typedRegistries=[this.controllers,this.scales,this.elements]}add(...t){this._each("register",t)}remove(...t){this._each("unregister",t)}addControllers(...t){this._each("register",t,this.controllers)}addElements(...t){this._each("register",t,this.elements)}addPlugins(...t){this._each("register",t,this.plugins)}addScales(...t){this._each("register",t,this.scales)}getController(t){return this._get(t,this.controllers,"controller")}getElement(t){return this._get(t,this.elements,"element")}getPlugin(t){return this._get(t,this.plugins,"plugin")}getScale(t){return this._get(t,this.scales,"scale")}removeControllers(...t){this._each("unregister",t,this.controllers)}removeElements(...t){this._each("unregister",t,this.elements)}removePlugins(...t){this._each("unregister",t,this.plugins)}removeScales(...t){this._each("unregister",t,this.scales)}_each(t,e,i){[...e].forEach((e=>{const s=i||this._getRegistryForType(e);i||s.isForType(e)||s===this.plugins&&e.id?this._exec(t,s,e):u(e,(e=>{const s=i||this._getRegistryForType(e);this._exec(t,s,e)}))}))}_exec(t,e,i){const s=w(t);d(i["before"+s],[],i),e[t](i),d(i["after"+s],[],i)}_getRegistryForType(t){for(let e=0;et.filter((t=>!e.some((e=>t.plugin.id===e.plugin.id))));this._notify(s(e,i),t,"stop"),this._notify(s(i,e),t,"start")}}function an(t,e){return e||!1!==t?!0===t?{}:t:null}function rn(t,{plugin:e,local:i},s,n){const o=t.pluginScopeKeys(e),a=t.getOptionScopes(s,o);return i&&e.defaults&&a.push(e.defaults),t.createResolver(a,n,[""],{scriptable:!1,indexable:!1,allKeys:!0})}function ln(t,e){const i=ue.datasets[t]||{};return((e.datasets||{})[t]||{}).indexAxis||e.indexAxis||i.indexAxis||"x"}function hn(t){if("x"===t||"y"===t||"r"===t)return t}function cn(t,...e){if(hn(t))return t;for(const s of e){const e=s.axis||("top"===(i=s.position)||"bottom"===i?"x":"left"===i||"right"===i?"y":void 0)||t.length>1&&hn(t[0].toLowerCase());if(e)return e}var i;throw new Error(`Cannot determine type of '${t}' axis. Please provide 'axis' or 'position' option.`)}function dn(t,e,i){if(i[e+"AxisID"]===t)return{axis:e}}function un(t,e){const i=re[t.type]||{scales:{}},s=e.scales||{},n=ln(t.type,e),a=Object.create(null);return Object.keys(s).forEach((e=>{const r=s[e];if(!o(r))return console.error(`Invalid scale configuration for scale: ${e}`);if(r._proxy)return console.warn(`Ignoring resolver passed as options for scale: ${e}`);const l=cn(e,r,function(t,e){if(e.data&&e.data.datasets){const i=e.data.datasets.filter((e=>e.xAxisID===t||e.yAxisID===t));if(i.length)return dn(t,"x",i[0])||dn(t,"y",i[0])}return{}}(e,t),ue.scales[r.type]),h=function(t,e){return t===e?"_index_":"_value_"}(l,n),c=i.scales||{};a[e]=b(Object.create(null),[{axis:l},r,c[l],c[h]])})),t.data.datasets.forEach((i=>{const n=i.type||t.type,o=i.indexAxis||ln(n,e),r=(re[n]||{}).scales||{};Object.keys(r).forEach((t=>{const e=function(t,e){let i=t;return"_index_"===t?i=e:"_value_"===t&&(i="x"===e?"y":"x"),i}(t,o),n=i[e+"AxisID"]||e;a[n]=a[n]||Object.create(null),b(a[n],[{axis:e},s[n],r[t]])}))})),Object.keys(a).forEach((t=>{const e=a[t];b(e,[ue.scales[e.type],ue.scale])})),a}function fn(t){const e=t.options||(t.options={});e.plugins=l(e.plugins,{}),e.scales=un(t,e)}function gn(t){return(t=t||{}).datasets=t.datasets||[],t.labels=t.labels||[],t}const pn=new Map,mn=new Set;function xn(t,e){let i=pn.get(t);return i||(i=e(),pn.set(t,i),mn.add(i)),i}const bn=(t,e,i)=>{const s=M(e,i);void 0!==s&&t.add(s)};class _n{constructor(t){this._config=function(t){return(t=t||{}).data=gn(t.data),fn(t),t}(t),this._scopeCache=new Map,this._resolverCache=new Map}get platform(){return this._config.platform}get type(){return this._config.type}set type(t){this._config.type=t}get data(){return this._config.data}set data(t){this._config.data=gn(t)}get options(){return this._config.options}set options(t){this._config.options=t}get plugins(){return this._config.plugins}update(){const t=this._config;this.clearCache(),fn(t)}clearCache(){this._scopeCache.clear(),this._resolverCache.clear()}datasetScopeKeys(t){return xn(t,(()=>[[`datasets.${t}`,""]]))}datasetAnimationScopeKeys(t,e){return xn(`${t}.transition.${e}`,(()=>[[`datasets.${t}.transitions.${e}`,`transitions.${e}`],[`datasets.${t}`,""]]))}datasetElementScopeKeys(t,e){return xn(`${t}-${e}`,(()=>[[`datasets.${t}.elements.${e}`,`datasets.${t}`,`elements.${e}`,""]]))}pluginScopeKeys(t){const e=t.id;return xn(`${this.type}-plugin-${e}`,(()=>[[`plugins.${e}`,...t.additionalOptionScopes||[]]]))}_cachedScopes(t,e){const i=this._scopeCache;let s=i.get(t);return s&&!e||(s=new Map,i.set(t,s)),s}getOptionScopes(t,e,i){const{options:s,type:n}=this,o=this._cachedScopes(t,i),a=o.get(e);if(a)return a;const r=new Set;e.forEach((e=>{t&&(r.add(t),e.forEach((e=>bn(r,t,e)))),e.forEach((t=>bn(r,s,t))),e.forEach((t=>bn(r,re[n]||{},t))),e.forEach((t=>bn(r,ue,t))),e.forEach((t=>bn(r,le,t)))}));const l=Array.from(r);return 0===l.length&&l.push(Object.create(null)),mn.has(e)&&o.set(e,l),l}chartOptionScopes(){const{options:t,type:e}=this;return[t,re[e]||{},ue.datasets[e]||{},{type:e},ue,le]}resolveNamedOptions(t,e,i,s=[""]){const o={$shared:!0},{resolver:a,subPrefixes:r}=yn(this._resolverCache,t,s);let l=a;if(function(t,e){const{isScriptable:i,isIndexable:s}=Ye(t);for(const o of e){const e=i(o),a=s(o),r=(a||e)&&t[o];if(e&&(S(r)||vn(r))||a&&n(r))return!0}return!1}(a,e)){o.$shared=!1;l=$e(a,i=S(i)?i():i,this.createResolver(t,i,r))}for(const t of e)o[t]=l[t];return o}createResolver(t,e,i=[""],s){const{resolver:n}=yn(this._resolverCache,t,i);return o(e)?$e(n,e,void 0,s):n}}function yn(t,e,i){let s=t.get(e);s||(s=new Map,t.set(e,s));const n=i.join();let o=s.get(n);if(!o){o={resolver:je(e,i),subPrefixes:i.filter((t=>!t.toLowerCase().includes("hover")))},s.set(n,o)}return o}const vn=t=>o(t)&&Object.getOwnPropertyNames(t).some((e=>S(t[e])));const Mn=["top","bottom","left","right","chartArea"];function wn(t,e){return"top"===t||"bottom"===t||-1===Mn.indexOf(t)&&"x"===e}function kn(t,e){return function(i,s){return i[t]===s[t]?i[e]-s[e]:i[t]-s[t]}}function Sn(t){const e=t.chart,i=e.options.animation;e.notifyPlugins("afterRender"),d(i&&i.onComplete,[t],e)}function Pn(t){const e=t.chart,i=e.options.animation;d(i&&i.onProgress,[t],e)}function Dn(t){return fe()&&"string"==typeof t?t=document.getElementById(t):t&&t.length&&(t=t[0]),t&&t.canvas&&(t=t.canvas),t}const Cn={},On=t=>{const e=Dn(t);return Object.values(Cn).filter((t=>t.canvas===e)).pop()};function An(t,e,i){const s=Object.keys(t);for(const n of s){const s=+n;if(s>=e){const o=t[n];delete t[n],(i>0||s>e)&&(t[s+i]=o)}}}class Tn{static defaults=ue;static instances=Cn;static overrides=re;static registry=nn;static version="4.5.1";static getChart=On;static register(...t){nn.add(...t),Ln()}static unregister(...t){nn.remove(...t),Ln()}constructor(t,e){const s=this.config=new _n(e),n=Dn(t),o=On(n);if(o)throw new Error("Canvas is already in use. Chart with ID '"+o.id+"' must be destroyed before the canvas with ID '"+o.canvas.id+"' can be reused.");const a=s.createResolver(s.chartOptionScopes(),this.getContext());this.platform=new(s.platform||Ps(n)),this.platform.updateConfig(s);const r=this.platform.acquireContext(n,a.aspectRatio),l=r&&r.canvas,h=l&&l.height,c=l&&l.width;this.id=i(),this.ctx=r,this.canvas=l,this.width=c,this.height=h,this._options=a,this._aspectRatio=this.aspectRatio,this._layers=[],this._metasets=[],this._stacks=void 0,this.boxes=[],this.currentDevicePixelRatio=void 0,this.chartArea=void 0,this._active=[],this._lastEvent=void 0,this._listeners={},this._responsiveListeners=void 0,this._sortedMetasets=[],this.scales={},this._plugins=new on,this.$proxies={},this._hiddenIndices={},this.attached=!1,this._animationsDisabled=void 0,this.$context=void 0,this._doResize=dt((t=>this.update(t)),a.resizeDelay||0),this._dataChanges=[],Cn[this.id]=this,r&&l?(bt.listen(this,"complete",Sn),bt.listen(this,"progress",Pn),this._initialize(),this.attached&&this.update()):console.error("Failed to create chart: can't acquire context from the given item")}get aspectRatio(){const{options:{aspectRatio:t,maintainAspectRatio:e},width:i,height:n,_aspectRatio:o}=this;return s(t)?e&&o?o:n?i/n:null:t}get data(){return this.config.data}set data(t){this.config.data=t}get options(){return this._options}set options(t){this.config.options=t}get registry(){return nn}_initialize(){return this.notifyPlugins("beforeInit"),this.options.responsive?this.resize():ke(this,this.options.devicePixelRatio),this.bindEvents(),this.notifyPlugins("afterInit"),this}clear(){return Te(this.canvas,this.ctx),this}stop(){return bt.stop(this),this}resize(t,e){bt.running(this)?this._resizeBeforeDraw={width:t,height:e}:this._resize(t,e)}_resize(t,e){const i=this.options,s=this.canvas,n=i.maintainAspectRatio&&this.aspectRatio,o=this.platform.getMaximumSize(s,t,e,n),a=i.devicePixelRatio||this.platform.getDevicePixelRatio(),r=this.width?"resize":"attach";this.width=o.width,this.height=o.height,this._aspectRatio=this.aspectRatio,ke(this,a,!0)&&(this.notifyPlugins("resize",{size:o}),d(i.onResize,[this,o],this),this.attached&&this._doResize(r)&&this.render())}ensureScalesHaveIDs(){u(this.options.scales||{},((t,e)=>{t.id=e}))}buildOrUpdateScales(){const t=this.options,e=t.scales,i=this.scales,s=Object.keys(i).reduce(((t,e)=>(t[e]=!1,t)),{});let n=[];e&&(n=n.concat(Object.keys(e).map((t=>{const i=e[t],s=cn(t,i),n="r"===s,o="x"===s;return{options:i,dposition:n?"chartArea":o?"bottom":"left",dtype:n?"radialLinear":o?"category":"linear"}})))),u(n,(e=>{const n=e.options,o=n.id,a=cn(o,n),r=l(n.type,e.dtype);void 0!==n.position&&wn(n.position,a)===wn(e.dposition)||(n.position=e.dposition),s[o]=!0;let h=null;if(o in i&&i[o].type===r)h=i[o];else{h=new(nn.getScale(r))({id:o,type:r,ctx:this.ctx,chart:this}),i[h.id]=h}h.init(n,t)})),u(s,((t,e)=>{t||delete i[e]})),u(i,(t=>{ls.configure(this,t,t.options),ls.addBox(this,t)}))}_updateMetasets(){const t=this._metasets,e=this.data.datasets.length,i=t.length;if(t.sort(((t,e)=>t.index-e.index)),i>e){for(let t=e;te.length&&delete this._stacks,t.forEach(((t,i)=>{0===e.filter((e=>e===t._dataset)).length&&this._destroyDatasetMeta(i)}))}buildOrUpdateControllers(){const t=[],e=this.data.datasets;let i,s;for(this._removeUnreferencedMetasets(),i=0,s=e.length;i{this.getDatasetMeta(e).controller.reset()}),this)}reset(){this._resetElements(),this.notifyPlugins("reset")}update(t){const e=this.config;e.update();const i=this._options=e.createResolver(e.chartOptionScopes(),this.getContext()),s=this._animationsDisabled=!i.animation;if(this._updateScales(),this._checkEventBindings(),this._updateHiddenIndices(),this._plugins.invalidate(),!1===this.notifyPlugins("beforeUpdate",{mode:t,cancelable:!0}))return;const n=this.buildOrUpdateControllers();this.notifyPlugins("beforeElementsUpdate");let o=0;for(let t=0,e=this.data.datasets.length;t{t.reset()})),this._updateDatasets(t),this.notifyPlugins("afterUpdate",{mode:t}),this._layers.sort(kn("z","_idx"));const{_active:a,_lastEvent:r}=this;r?this._eventHandler(r,!0):a.length&&this._updateHoverStyles(a,a,!0),this.render()}_updateScales(){u(this.scales,(t=>{ls.removeBox(this,t)})),this.ensureScalesHaveIDs(),this.buildOrUpdateScales()}_checkEventBindings(){const t=this.options,e=new Set(Object.keys(this._listeners)),i=new Set(t.events);P(e,i)&&!!this._responsiveListeners===t.responsive||(this.unbindEvents(),this.bindEvents())}_updateHiddenIndices(){const{_hiddenIndices:t}=this,e=this._getUniformDataChanges()||[];for(const{method:i,start:s,count:n}of e){An(t,s,"_removeElements"===i?-n:n)}}_getUniformDataChanges(){const t=this._dataChanges;if(!t||!t.length)return;this._dataChanges=[];const e=this.data.datasets.length,i=e=>new Set(t.filter((t=>t[0]===e)).map(((t,e)=>e+","+t.splice(1).join(",")))),s=i(0);for(let t=1;tt.split(","))).map((t=>({method:t[1],start:+t[2],count:+t[3]})))}_updateLayout(t){if(!1===this.notifyPlugins("beforeLayout",{cancelable:!0}))return;ls.update(this,this.width,this.height,t);const e=this.chartArea,i=e.width<=0||e.height<=0;this._layers=[],u(this.boxes,(t=>{i&&"chartArea"===t.position||(t.configure&&t.configure(),this._layers.push(...t._layers()))}),this),this._layers.forEach(((t,e)=>{t._idx=e})),this.notifyPlugins("afterLayout")}_updateDatasets(t){if(!1!==this.notifyPlugins("beforeDatasetsUpdate",{mode:t,cancelable:!0})){for(let t=0,e=this.data.datasets.length;t=0;--e)this._drawDataset(t[e]);this.notifyPlugins("afterDatasetsDraw")}_drawDataset(t){const e=this.ctx,i={meta:t,index:t.index,cancelable:!0},s=Ni(this,t);!1!==this.notifyPlugins("beforeDatasetDraw",i)&&(s&&Ie(e,s),t.controller.draw(),s&&ze(e),i.cancelable=!1,this.notifyPlugins("afterDatasetDraw",i))}isPointInArea(t){return Re(t,this.chartArea,this._minPadding)}getElementsAtEventForMode(t,e,i,s){const n=Ki.modes[e];return"function"==typeof n?n(this,t,i,s):[]}getDatasetMeta(t){const e=this.data.datasets[t],i=this._metasets;let s=i.filter((t=>t&&t._dataset===e)).pop();return s||(s={type:null,data:[],dataset:null,controller:null,hidden:null,xAxisID:null,yAxisID:null,order:e&&e.order||0,index:t,_dataset:e,_parsed:[],_sorted:!1},i.push(s)),s}getContext(){return this.$context||(this.$context=Ci(null,{chart:this,type:"chart"}))}getVisibleDatasetCount(){return this.getSortedVisibleDatasetMetas().length}isDatasetVisible(t){const e=this.data.datasets[t];if(!e)return!1;const i=this.getDatasetMeta(t);return"boolean"==typeof i.hidden?!i.hidden:!e.hidden}setDatasetVisibility(t,e){this.getDatasetMeta(t).hidden=!e}toggleDataVisibility(t){this._hiddenIndices[t]=!this._hiddenIndices[t]}getDataVisibility(t){return!this._hiddenIndices[t]}_updateVisibility(t,e,i){const s=i?"show":"hide",n=this.getDatasetMeta(t),o=n.controller._resolveAnimations(void 0,s);k(e)?(n.data[e].hidden=!i,this.update()):(this.setDatasetVisibility(t,i),o.update(n,{visible:i}),this.update((e=>e.datasetIndex===t?s:void 0)))}hide(t,e){this._updateVisibility(t,e,!1)}show(t,e){this._updateVisibility(t,e,!0)}_destroyDatasetMeta(t){const e=this._metasets[t];e&&e.controller&&e.controller._destroy(),delete this._metasets[t]}_stop(){let t,e;for(this.stop(),bt.remove(this),t=0,e=this.data.datasets.length;t{e.addEventListener(this,i,s),t[i]=s},s=(t,e,i)=>{t.offsetX=e,t.offsetY=i,this._eventHandler(t)};u(this.options.events,(t=>i(t,s)))}bindResponsiveEvents(){this._responsiveListeners||(this._responsiveListeners={});const t=this._responsiveListeners,e=this.platform,i=(i,s)=>{e.addEventListener(this,i,s),t[i]=s},s=(i,s)=>{t[i]&&(e.removeEventListener(this,i,s),delete t[i])},n=(t,e)=>{this.canvas&&this.resize(t,e)};let o;const a=()=>{s("attach",a),this.attached=!0,this.resize(),i("resize",n),i("detach",o)};o=()=>{this.attached=!1,s("resize",n),this._stop(),this._resize(0,0),i("attach",a)},e.isAttached(this.canvas)?a():o()}unbindEvents(){u(this._listeners,((t,e)=>{this.platform.removeEventListener(this,e,t)})),this._listeners={},u(this._responsiveListeners,((t,e)=>{this.platform.removeEventListener(this,e,t)})),this._responsiveListeners=void 0}updateHoverStyle(t,e,i){const s=i?"set":"remove";let n,o,a,r;for("dataset"===e&&(n=this.getDatasetMeta(t[0].datasetIndex),n.controller["_"+s+"DatasetHoverStyle"]()),a=0,r=t.length;a{const i=this.getDatasetMeta(t);if(!i)throw new Error("No dataset found at index "+t);return{datasetIndex:t,element:i.data[e],index:e}}));!f(i,e)&&(this._active=i,this._lastEvent=null,this._updateHoverStyles(i,e))}notifyPlugins(t,e,i){return this._plugins.notify(this,t,e,i)}isPluginEnabled(t){return 1===this._plugins._cache.filter((e=>e.plugin.id===t)).length}_updateHoverStyles(t,e,i){const s=this.options.hover,n=(t,e)=>t.filter((t=>!e.some((e=>t.datasetIndex===e.datasetIndex&&t.index===e.index)))),o=n(e,t),a=i?t:n(t,e);o.length&&this.updateHoverStyle(o,s.mode,!1),a.length&&s.mode&&this.updateHoverStyle(a,s.mode,!0)}_eventHandler(t,e){const i={event:t,replay:e,cancelable:!0,inChartArea:this.isPointInArea(t)},s=e=>(e.options.events||this.options.events).includes(t.native.type);if(!1===this.notifyPlugins("beforeEvent",i,s))return;const n=this._handleEvent(t,e,i.inChartArea);return i.cancelable=!1,this.notifyPlugins("afterEvent",i,s),(n||i.changed)&&this.render(),this}_handleEvent(t,e,i){const{_active:s=[],options:n}=this,o=e,a=this._getActiveElements(t,s,i,o),r=D(t),l=function(t,e,i,s){return i&&"mouseout"!==t.type?s?e:t:null}(t,this._lastEvent,i,r);i&&(this._lastEvent=null,d(n.onHover,[t,a,this],this),r&&d(n.onClick,[t,a,this],this));const h=!f(a,s);return(h||e)&&(this._active=a,this._updateHoverStyles(a,s,e)),this._lastEvent=l,h}_getActiveElements(t,e,i,s){if("mouseout"===t.type)return[];if(!i)return e;const n=this.options.hover;return this.getElementsAtEventForMode(t,n.mode,n,s)}}function Ln(){return u(Tn.instances,(t=>t._plugins.invalidate()))}function En(){throw new Error("This method is not implemented: Check that a complete date adapter is provided.")}class Rn{static override(t){Object.assign(Rn.prototype,t)}options;constructor(t){this.options=t||{}}init(){}formats(){return En()}parse(){return En()}format(){return En()}add(){return En()}diff(){return En()}startOf(){return En()}endOf(){return En()}}var In={_date:Rn};function zn(t){const e=t.iScale,i=function(t,e){if(!t._cache.$bar){const i=t.getMatchingVisibleMetas(e);let s=[];for(let e=0,n=i.length;et-e)))}return t._cache.$bar}(e,t.type);let s,n,o,a,r=e._length;const l=()=>{32767!==o&&-32768!==o&&(k(a)&&(r=Math.min(r,Math.abs(o-a)||r)),a=o)};for(s=0,n=i.length;sMath.abs(r)&&(l=r,h=a),e[i.axis]=h,e._custom={barStart:l,barEnd:h,start:n,end:o,min:a,max:r}}(t,e,i,s):e[i.axis]=i.parse(t,s),e}function Vn(t,e,i,s){const n=t.iScale,o=t.vScale,a=n.getLabels(),r=n===o,l=[];let h,c,d,u;for(h=i,c=i+s;ht.x,i="left",s="right"):(e=t.base"spacing"!==t,_indexable:t=>"spacing"!==t&&!t.startsWith("borderDash")&&!t.startsWith("hoverBorderDash")};static overrides={aspectRatio:1,plugins:{legend:{labels:{generateLabels(t){const e=t.data,{labels:{pointStyle:i,textAlign:s,color:n,useBorderRadius:o,borderRadius:a}}=t.legend.options;return e.labels.length&&e.datasets.length?e.labels.map(((e,r)=>{const l=t.getDatasetMeta(0).controller.getStyle(r);return{text:e,fillStyle:l.backgroundColor,fontColor:n,hidden:!t.getDataVisibility(r),lineDash:l.borderDash,lineDashOffset:l.borderDashOffset,lineJoin:l.borderJoinStyle,lineWidth:l.borderWidth,strokeStyle:l.borderColor,textAlign:s,pointStyle:i,borderRadius:o&&(a||l.borderRadius),index:r}})):[]}},onClick(t,e,i){i.chart.toggleDataVisibility(e.index),i.chart.update()}}}};constructor(t,e){super(t,e),this.enableOptionSharing=!0,this.innerRadius=void 0,this.outerRadius=void 0,this.offsetX=void 0,this.offsetY=void 0}linkScales(){}parse(t,e){const i=this.getDataset().data,s=this._cachedMeta;if(!1===this._parsing)s._parsed=i;else{let n,a,r=t=>+i[t];if(o(i[t])){const{key:t="value"}=this._parsing;r=e=>+M(i[e],t)}for(n=t,a=t+e;nJ(t,r,l,!0)?1:Math.max(e,e*i,s,s*i),g=(t,e,s)=>J(t,r,l,!0)?-1:Math.min(e,e*i,s,s*i),p=f(0,h,d),m=f(E,c,u),x=g(C,h,d),b=g(C+E,c,u);s=(p-x)/2,n=(m-b)/2,o=-(p+x)/2,a=-(m+b)/2}return{ratioX:s,ratioY:n,offsetX:o,offsetY:a}}(u,d,r),x=(i.width-o)/f,b=(i.height-o)/g,_=Math.max(Math.min(x,b)/2,0),y=c(this.options.radius,_),v=(y-Math.max(y*r,0))/this._getVisibleDatasetWeightTotal();this.offsetX=p*y,this.offsetY=m*y,s.total=this.calculateTotal(),this.outerRadius=y-v*this._getRingWeightOffset(this.index),this.innerRadius=Math.max(this.outerRadius-v*l,0),this.updateElements(n,0,n.length,t)}_circumference(t,e){const i=this.options,s=this._cachedMeta,n=this._getCircumference();return e&&i.animation.animateRotate||!this.chart.getDataVisibility(t)||null===s._parsed[t]||s.data[t].hidden?0:this.calculateCircumference(s._parsed[t]*n/O)}updateElements(t,e,i,s){const n="reset"===s,o=this.chart,a=o.chartArea,r=o.options.animation,l=(a.left+a.right)/2,h=(a.top+a.bottom)/2,c=n&&r.animateScale,d=c?0:this.innerRadius,u=c?0:this.outerRadius,{sharedOptions:f,includeOptions:g}=this._getSharedOptions(e,s);let p,m=this._getRotation();for(p=0;p0&&!isNaN(t)?O*(Math.abs(t)/e):0}getLabelAndValue(t){const e=this._cachedMeta,i=this.chart,s=i.data.labels||[],n=ne(e._parsed[t],i.options.locale);return{label:s[t]||"",value:n}}getMaxBorderWidth(t){let e=0;const i=this.chart;let s,n,o,a,r;if(!t)for(s=0,n=i.data.datasets.length;s{const o=t.getDatasetMeta(0).controller.getStyle(n);return{text:e,fillStyle:o.backgroundColor,strokeStyle:o.borderColor,fontColor:s,lineWidth:o.borderWidth,pointStyle:i,hidden:!t.getDataVisibility(n),index:n}}))}return[]}},onClick(t,e,i){i.chart.toggleDataVisibility(e.index),i.chart.update()}}},scales:{r:{type:"radialLinear",angleLines:{display:!1},beginAtZero:!0,grid:{circular:!0},pointLabels:{display:!1},startAngle:0}}};constructor(t,e){super(t,e),this.innerRadius=void 0,this.outerRadius=void 0}getLabelAndValue(t){const e=this._cachedMeta,i=this.chart,s=i.data.labels||[],n=ne(e._parsed[t].r,i.options.locale);return{label:s[t]||"",value:n}}parseObjectData(t,e,i,s){return ii.bind(this)(t,e,i,s)}update(t){const e=this._cachedMeta.data;this._updateRadius(),this.updateElements(e,0,e.length,t)}getMinMax(){const t=this._cachedMeta,e={min:Number.POSITIVE_INFINITY,max:Number.NEGATIVE_INFINITY};return t.data.forEach(((t,i)=>{const s=this.getParsed(i).r;!isNaN(s)&&this.chart.getDataVisibility(i)&&(se.max&&(e.max=s))})),e}_updateRadius(){const t=this.chart,e=t.chartArea,i=t.options,s=Math.min(e.right-e.left,e.bottom-e.top),n=Math.max(s/2,0),o=(n-Math.max(i.cutoutPercentage?n/100*i.cutoutPercentage:1,0))/t.getVisibleDatasetCount();this.outerRadius=n-o*this.index,this.innerRadius=this.outerRadius-o}updateElements(t,e,i,s){const n="reset"===s,o=this.chart,a=o.options.animation,r=this._cachedMeta.rScale,l=r.xCenter,h=r.yCenter,c=r.getIndexAngle(0)-.5*C;let d,u=c;const f=360/this.countVisibleElements();for(d=0;d{!isNaN(this.getParsed(i).r)&&this.chart.getDataVisibility(i)&&e++})),e}_computeAngle(t,e,i){return this.chart.getDataVisibility(t)?$(this.resolveDataElementOptions(t,e).angle||i):0}}var Un=Object.freeze({__proto__:null,BarController:class extends js{static id="bar";static defaults={datasetElementType:!1,dataElementType:"bar",categoryPercentage:.8,barPercentage:.9,grouped:!0,animations:{numbers:{type:"number",properties:["x","y","base","width","height"]}}};static overrides={scales:{_index_:{type:"category",offset:!0,grid:{offset:!0}},_value_:{type:"linear",beginAtZero:!0}}};parsePrimitiveData(t,e,i,s){return Vn(t,e,i,s)}parseArrayData(t,e,i,s){return Vn(t,e,i,s)}parseObjectData(t,e,i,s){const{iScale:n,vScale:o}=t,{xAxisKey:a="x",yAxisKey:r="y"}=this._parsing,l="x"===n.axis?a:r,h="x"===o.axis?a:r,c=[];let d,u,f,g;for(d=i,u=i+s;dt.controller.options.grouped)),o=i.options.stacked,a=[],r=this._cachedMeta.controller.getParsed(e),l=r&&r[i.axis],h=t=>{const e=t._parsed.find((t=>t[i.axis]===l)),n=e&&e[t.vScale.axis];if(s(n)||isNaN(n))return!0};for(const i of n)if((void 0===e||!h(i))&&((!1===o||-1===a.indexOf(i.stack)||void 0===o&&void 0===i.stack)&&a.push(i.stack),i.index===t))break;return a.length||a.push(void 0),a}_getStackCount(t){return this._getStacks(void 0,t).length}_getAxisCount(){return this._getAxis().length}getFirstScaleIdForIndexAxis(){const t=this.chart.scales,e=this.chart.options.indexAxis;return Object.keys(t).filter((i=>t[i].axis===e)).shift()}_getAxis(){const t={},e=this.getFirstScaleIdForIndexAxis();for(const i of this.chart.data.datasets)t[l("x"===this.chart.options.indexAxis?i.xAxisID:i.yAxisID,e)]=!0;return Object.keys(t)}_getStackIndex(t,e,i){const s=this._getStacks(t,i),n=void 0!==e?s.indexOf(e):-1;return-1===n?s.length-1:n}_getRuler(){const t=this.options,e=this._cachedMeta,i=e.iScale,s=[];let n,o;for(n=0,o=e.data.length;n=i?1:-1)}(u,e,r)*a,f===r&&(x-=u/2);const t=e.getPixelForDecimal(0),s=e.getPixelForDecimal(1),o=Math.min(t,s),h=Math.max(t,s);x=Math.max(Math.min(x,h),o),d=x+u,i&&!c&&(l._stacks[e.axis]._visualValues[n]=e.getValueForPixel(d)-e.getValueForPixel(x))}if(x===e.getPixelForValue(r)){const t=F(u)*e.getLineWidthForValue(r)/2;x+=t,u-=t}return{size:u,base:x,head:d,center:d+u/2}}_calculateBarIndexPixels(t,e){const i=e.scale,n=this.options,o=n.skipNull,a=l(n.maxBarThickness,1/0);let r,h;const c=this._getAxisCount();if(e.grouped){const i=o?this._getStackCount(t):e.stackCount,d="flex"===n.barThickness?function(t,e,i,s){const n=e.pixels,o=n[t];let a=t>0?n[t-1]:null,r=t=0;--i)e=Math.max(e,t[i].size(this.resolveDataElementOptions(i))/2);return e>0&&e}getLabelAndValue(t){const e=this._cachedMeta,i=this.chart.data.labels||[],{xScale:s,yScale:n}=e,o=this.getParsed(t),a=s.getLabelForValue(o.x),r=n.getLabelForValue(o.y),l=o._custom;return{label:i[t]||"",value:"("+a+", "+r+(l?", "+l:"")+")"}}update(t){const e=this._cachedMeta.data;this.updateElements(e,0,e.length,t)}updateElements(t,e,i,s){const n="reset"===s,{iScale:o,vScale:a}=this._cachedMeta,{sharedOptions:r,includeOptions:l}=this._getSharedOptions(e,s),h=o.axis,c=a.axis;for(let d=e;d0&&this.getParsed(e-1);for(let i=0;i<_;++i){const g=t[i],_=x?g:{};if(i=b){_.skip=!0;continue}const v=this.getParsed(i),M=s(v[f]),w=_[u]=a.getPixelForValue(v[u],i),k=_[f]=o||M?r.getBasePixel():r.getPixelForValue(l?this.applyStack(r,v,l):v[f],i);_.skip=isNaN(w)||isNaN(k)||M,_.stop=i>0&&Math.abs(v[u]-y[u])>m,p&&(_.parsed=v,_.raw=h.data[i]),d&&(_.options=c||this.resolveDataElementOptions(i,g.active?"active":n)),x||this.updateElement(g,i,_,n),y=v}}getMaxOverflow(){const t=this._cachedMeta,e=t.dataset,i=e.options&&e.options.borderWidth||0,s=t.data||[];if(!s.length)return i;const n=s[0].size(this.resolveDataElementOptions(0)),o=s[s.length-1].size(this.resolveDataElementOptions(s.length-1));return Math.max(i,n,o)/2}draw(){const t=this._cachedMeta;t.dataset.updateControlPoints(this.chart.chartArea,t.iScale.axis),super.draw()}},PieController:class extends $n{static id="pie";static defaults={cutout:0,rotation:0,circumference:360,radius:"100%"}},PolarAreaController:Yn,RadarController:class extends js{static id="radar";static defaults={datasetElementType:"line",dataElementType:"point",indexAxis:"r",showLine:!0,elements:{line:{fill:"start"}}};static overrides={aspectRatio:1,scales:{r:{type:"radialLinear"}}};getLabelAndValue(t){const e=this._cachedMeta.vScale,i=this.getParsed(t);return{label:e.getLabels()[t],value:""+e.getLabelForValue(i[e.axis])}}parseObjectData(t,e,i,s){return ii.bind(this)(t,e,i,s)}update(t){const e=this._cachedMeta,i=e.dataset,s=e.data||[],n=e.iScale.getLabels();if(i.points=s,"resize"!==t){const e=this.resolveDatasetElementOptions(t);this.options.showLine||(e.borderWidth=0);const o={_loop:!0,_fullLoop:n.length===s.length,options:e};this.updateElement(i,void 0,o,t)}this.updateElements(s,0,s.length,t)}updateElements(t,e,i,s){const n=this._cachedMeta.rScale,o="reset"===s;for(let a=e;a0&&this.getParsed(e-1);for(let c=e;c0&&Math.abs(i[f]-_[f])>x,m&&(p.parsed=i,p.raw=h.data[c]),u&&(p.options=d||this.resolveDataElementOptions(c,e.active?"active":n)),b||this.updateElement(e,c,p,n),_=i}this.updateSharedOptions(d,n,c)}getMaxOverflow(){const t=this._cachedMeta,e=t.data||[];if(!this.options.showLine){let t=0;for(let i=e.length-1;i>=0;--i)t=Math.max(t,e[i].size(this.resolveDataElementOptions(i))/2);return t>0&&t}const i=t.dataset,s=i.options&&i.options.borderWidth||0;if(!e.length)return s;const n=e[0].size(this.resolveDataElementOptions(0)),o=e[e.length-1].size(this.resolveDataElementOptions(e.length-1));return Math.max(s,n,o)/2}}});function Xn(t,e,i,s){const n=vi(t.options.borderRadius,["outerStart","outerEnd","innerStart","innerEnd"]);const o=(i-e)/2,a=Math.min(o,s*e/2),r=t=>{const e=(i-Math.min(o,t))*s/2;return Z(t,0,Math.min(o,e))};return{outerStart:r(n.outerStart),outerEnd:r(n.outerEnd),innerStart:Z(n.innerStart,0,a),innerEnd:Z(n.innerEnd,0,a)}}function qn(t,e,i,s){return{x:i+t*Math.cos(e),y:s+t*Math.sin(e)}}function Kn(t,e,i,s,n,o){const{x:a,y:r,startAngle:l,pixelMargin:h,innerRadius:c}=e,d=Math.max(e.outerRadius+s+i-h,0),u=c>0?c+s+i+h:0;let f=0;const g=n-l;if(s){const t=((c>0?c-s:0)+(d>0?d-s:0))/2;f=(g-(0!==t?g*t/(t+s):g))/2}const p=(g-Math.max(.001,g*d-i/C)/d)/2,m=l+p+f,x=n-p-f,{outerStart:b,outerEnd:_,innerStart:y,innerEnd:v}=Xn(e,u,d,x-m),M=d-b,w=d-_,k=m+b/M,S=x-_/w,P=u+y,D=u+v,O=m+y/P,A=x-v/D;if(t.beginPath(),o){const e=(k+S)/2;if(t.arc(a,r,d,k,e),t.arc(a,r,d,e,S),_>0){const e=qn(w,S,a,r);t.arc(e.x,e.y,_,S,x+E)}const i=qn(D,x,a,r);if(t.lineTo(i.x,i.y),v>0){const e=qn(D,A,a,r);t.arc(e.x,e.y,v,x+E,A+Math.PI)}const s=(x-v/u+(m+y/u))/2;if(t.arc(a,r,u,x-v/u,s,!0),t.arc(a,r,u,s,m+y/u,!0),y>0){const e=qn(P,O,a,r);t.arc(e.x,e.y,y,O+Math.PI,m-E)}const n=qn(M,m,a,r);if(t.lineTo(n.x,n.y),b>0){const e=qn(M,k,a,r);t.arc(e.x,e.y,b,m-E,k)}}else{t.moveTo(a,r);const e=Math.cos(k)*d+a,i=Math.sin(k)*d+r;t.lineTo(e,i);const s=Math.cos(S)*d+a,n=Math.sin(S)*d+r;t.lineTo(s,n)}t.closePath()}function Gn(t,e,i,s,n){const{fullCircles:o,startAngle:a,circumference:r,options:l}=e,{borderWidth:h,borderJoinStyle:c,borderDash:d,borderDashOffset:u,borderRadius:f}=l,g="inner"===l.borderAlign;if(!h)return;t.setLineDash(d||[]),t.lineDashOffset=u,g?(t.lineWidth=2*h,t.lineJoin=c||"round"):(t.lineWidth=h,t.lineJoin=c||"bevel");let p=e.endAngle;if(o){Kn(t,e,i,s,p,n);for(let e=0;en?(h=n/l,t.arc(o,a,l,i+h,s-h,!0)):t.arc(o,a,n,i+E,s-E),t.closePath(),t.clip()}(t,e,p),l.selfJoin&&p-a>=C&&0===f&&"miter"!==c&&function(t,e,i){const{startAngle:s,x:n,y:o,outerRadius:a,innerRadius:r,options:l}=e,{borderWidth:h,borderJoinStyle:c}=l,d=Math.min(h/a,G(s-i));if(t.beginPath(),t.arc(n,o,a-h/2,s+d/2,i-d/2),r>0){const e=Math.min(h/r,G(s-i));t.arc(n,o,r+h/2,i-e/2,s+e/2,!0)}else{const e=Math.min(h/2,a*G(s-i));if("round"===c)t.arc(n,o,e,i-C/2,s+C/2,!0);else if("bevel"===c){const a=2*e*e,r=-a*Math.cos(i+C/2)+n,l=-a*Math.sin(i+C/2)+o,h=a*Math.cos(s+C/2)+n,c=a*Math.sin(s+C/2)+o;t.lineTo(r,l),t.lineTo(h,c)}}t.closePath(),t.moveTo(0,0),t.rect(0,0,t.canvas.width,t.canvas.height),t.clip("evenodd")}(t,e,p),o||(Kn(t,e,i,s,p,n),t.stroke())}function Jn(t,e,i=e){t.lineCap=l(i.borderCapStyle,e.borderCapStyle),t.setLineDash(l(i.borderDash,e.borderDash)),t.lineDashOffset=l(i.borderDashOffset,e.borderDashOffset),t.lineJoin=l(i.borderJoinStyle,e.borderJoinStyle),t.lineWidth=l(i.borderWidth,e.borderWidth),t.strokeStyle=l(i.borderColor,e.borderColor)}function Zn(t,e,i){t.lineTo(i.x,i.y)}function Qn(t,e,i={}){const s=t.length,{start:n=0,end:o=s-1}=i,{start:a,end:r}=e,l=Math.max(n,a),h=Math.min(o,r),c=nr&&o>r;return{count:s,start:l,loop:e.loop,ilen:h(a+(h?r-t:t))%o,_=()=>{f!==g&&(t.lineTo(m,g),t.lineTo(m,f),t.lineTo(m,p))};for(l&&(d=n[b(0)],t.moveTo(d.x,d.y)),c=0;c<=r;++c){if(d=n[b(c)],d.skip)continue;const e=d.x,i=d.y,s=0|e;s===u?(ig&&(g=i),m=(x*m+e)/++x):(_(),t.lineTo(e,i),u=s,x=0,f=g=i),p=i}_()}function io(t){const e=t.options,i=e.borderDash&&e.borderDash.length;return!(t._decimated||t._loop||e.tension||"monotone"===e.cubicInterpolationMode||e.stepped||i)?eo:to}const so="function"==typeof Path2D;function no(t,e,i,s){so&&!e.options.segment?function(t,e,i,s){let n=e._path;n||(n=e._path=new Path2D,e.path(n,i,s)&&n.closePath()),Jn(t,e.options),t.stroke(n)}(t,e,i,s):function(t,e,i,s){const{segments:n,options:o}=e,a=io(e);for(const r of n)Jn(t,o,r.style),t.beginPath(),a(t,e,r,{start:i,end:i+s-1})&&t.closePath(),t.stroke()}(t,e,i,s)}class oo extends $s{static id="line";static defaults={borderCapStyle:"butt",borderDash:[],borderDashOffset:0,borderJoinStyle:"miter",borderWidth:3,capBezierPoints:!0,cubicInterpolationMode:"default",fill:!1,spanGaps:!1,stepped:!1,tension:0};static defaultRoutes={backgroundColor:"backgroundColor",borderColor:"borderColor"};static descriptors={_scriptable:!0,_indexable:t=>"borderDash"!==t&&"fill"!==t};constructor(t){super(),this.animated=!0,this.options=void 0,this._chart=void 0,this._loop=void 0,this._fullLoop=void 0,this._path=void 0,this._points=void 0,this._segments=void 0,this._decimated=!1,this._pointsUpdated=!1,this._datasetIndex=void 0,t&&Object.assign(this,t)}updateControlPoints(t,e){const i=this.options;if((i.tension||"monotone"===i.cubicInterpolationMode)&&!i.stepped&&!this._pointsUpdated){const s=i.spanGaps?this._loop:this._fullLoop;hi(this._points,i,t,s,e),this._pointsUpdated=!0}}set points(t){this._points=t,delete this._segments,delete this._path,this._pointsUpdated=!1}get points(){return this._points}get segments(){return this._segments||(this._segments=zi(this,this.options.segment))}first(){const t=this.segments,e=this.points;return t.length&&e[t[0].start]}last(){const t=this.segments,e=this.points,i=t.length;return i&&e[t[i-1].end]}interpolate(t,e){const i=this.options,s=t[e],n=this.points,o=Ii(this,{property:e,start:s,end:s});if(!o.length)return;const a=[],r=function(t){return t.stepped?pi:t.tension||"monotone"===t.cubicInterpolationMode?mi:gi}(i);let l,h;for(l=0,h=o.length;l"borderDash"!==t};circumference;endAngle;fullCircles;innerRadius;outerRadius;pixelMargin;startAngle;constructor(t){super(),this.options=void 0,this.circumference=void 0,this.startAngle=void 0,this.endAngle=void 0,this.innerRadius=void 0,this.outerRadius=void 0,this.pixelMargin=0,this.fullCircles=0,t&&Object.assign(this,t)}inRange(t,e,i){const s=this.getProps(["x","y"],i),{angle:n,distance:o}=X(s,{x:t,y:e}),{startAngle:a,endAngle:r,innerRadius:h,outerRadius:c,circumference:d}=this.getProps(["startAngle","endAngle","innerRadius","outerRadius","circumference"],i),u=(this.options.spacing+this.options.borderWidth)/2,f=l(d,r-a),g=J(n,a,r)&&a!==r,p=f>=O||g,m=tt(o,h+u,c+u);return p&&m}getCenterPoint(t){const{x:e,y:i,startAngle:s,endAngle:n,innerRadius:o,outerRadius:a}=this.getProps(["x","y","startAngle","endAngle","innerRadius","outerRadius"],t),{offset:r,spacing:l}=this.options,h=(s+n)/2,c=(o+a+l+r)/2;return{x:e+Math.cos(h)*c,y:i+Math.sin(h)*c}}tooltipPosition(t){return this.getCenterPoint(t)}draw(t){const{options:e,circumference:i}=this,s=(e.offset||0)/4,n=(e.spacing||0)/2,o=e.circular;if(this.pixelMargin="inner"===e.borderAlign?.33:0,this.fullCircles=i>O?Math.floor(i/O):0,0===i||this.innerRadius<0||this.outerRadius<0)return;t.save();const a=(this.startAngle+this.endAngle)/2;t.translate(Math.cos(a)*s,Math.sin(a)*s);const r=s*(1-Math.sin(Math.min(C,i||0)));t.fillStyle=e.backgroundColor,t.strokeStyle=e.borderColor,function(t,e,i,s,n){const{fullCircles:o,startAngle:a,circumference:r}=e;let l=e.endAngle;if(o){Kn(t,e,i,s,l,n);for(let e=0;e("string"==typeof e?(i=t.push(e)-1,s.unshift({index:i,label:e})):isNaN(e)&&(i=null),i))(t,e,i,s);return n!==t.lastIndexOf(e)?i:n}function mo(t){const e=this.getLabels();return t>=0&&ts=e?s:t,a=t=>n=i?n:t;if(t){const t=F(s),e=F(n);t<0&&e<0?a(0):t>0&&e>0&&o(0)}if(s===n){let e=0===n?1:Math.abs(.05*n);a(n+e),t||o(s-e)}this.min=s,this.max=n}getTickLimit(){const t=this.options.ticks;let e,{maxTicksLimit:i,stepSize:s}=t;return s?(e=Math.ceil(this.max/s)-Math.floor(this.min/s)+1,e>1e3&&(console.warn(`scales.${this.id}.ticks.stepSize: ${s} would result generating up to ${e} ticks. Limiting to 1000.`),e=1e3)):(e=this.computeTickLimit(),i=i||11),i&&(e=Math.min(i,e)),e}computeTickLimit(){return Number.POSITIVE_INFINITY}buildTicks(){const t=this.options,e=t.ticks;let i=this.getTickLimit();i=Math.max(2,i);const n=function(t,e){const i=[],{bounds:n,step:o,min:a,max:r,precision:l,count:h,maxTicks:c,maxDigits:d,includeBounds:u}=t,f=o||1,g=c-1,{min:p,max:m}=e,x=!s(a),b=!s(r),_=!s(h),y=(m-p)/(d+1);let v,M,w,k,S=B((m-p)/g/f)*f;if(S<1e-14&&!x&&!b)return[{value:p},{value:m}];k=Math.ceil(m/S)-Math.floor(p/S),k>g&&(S=B(k*S/g/f)*f),s(l)||(v=Math.pow(10,l),S=Math.ceil(S*v)/v),"ticks"===n?(M=Math.floor(p/S)*S,w=Math.ceil(m/S)*S):(M=p,w=m),x&&b&&o&&H((r-a)/o,S/1e3)?(k=Math.round(Math.min((r-a)/S,c)),S=(r-a)/k,M=a,w=r):_?(M=x?a:M,w=b?r:w,k=h-1,S=(w-M)/k):(k=(w-M)/S,k=V(k,Math.round(k),S/1e3)?Math.round(k):Math.ceil(k));const P=Math.max(U(S),U(M));v=Math.pow(10,s(l)?P:l),M=Math.round(M*v)/v,w=Math.round(w*v)/v;let D=0;for(x&&(u&&M!==a?(i.push({value:a}),Mr)break;i.push({value:t})}return b&&u&&w!==r?i.length&&V(i[i.length-1].value,r,xo(r,y,t))?i[i.length-1].value=r:i.push({value:r}):b&&w!==r||i.push({value:w}),i}({maxTicks:i,bounds:t.bounds,min:t.min,max:t.max,precision:e.precision,step:e.stepSize,count:e.count,maxDigits:this._maxDigits(),horizontal:this.isHorizontal(),minRotation:e.minRotation||0,includeBounds:!1!==e.includeBounds},this._range||this);return"ticks"===t.bounds&&j(n,this,"value"),t.reverse?(n.reverse(),this.start=this.max,this.end=this.min):(this.start=this.min,this.end=this.max),n}configure(){const t=this.ticks;let e=this.min,i=this.max;if(super.configure(),this.options.offset&&t.length){const s=(i-e)/Math.max(t.length-1,1)/2;e-=s,i+=s}this._startValue=e,this._endValue=i,this._valueRange=i-e}getLabelForValue(t){return ne(t,this.chart.options.locale,this.options.ticks.format)}}class _o extends bo{static id="linear";static defaults={ticks:{callback:ae.formatters.numeric}};determineDataLimits(){const{min:t,max:e}=this.getMinMax(!0);this.min=a(t)?t:0,this.max=a(e)?e:1,this.handleTickRangeOptions()}computeTickLimit(){const t=this.isHorizontal(),e=t?this.width:this.height,i=$(this.options.ticks.minRotation),s=(t?Math.sin(i):Math.cos(i))||.001,n=this._resolveTickFontOptions(0);return Math.ceil(e/Math.min(40,n.lineHeight/s))}getPixelForValue(t){return null===t?NaN:this.getPixelForDecimal((t-this._startValue)/this._valueRange)}getValueForPixel(t){return this._startValue+this.getDecimalForPixel(t)*this._valueRange}}const yo=t=>Math.floor(z(t)),vo=(t,e)=>Math.pow(10,yo(t)+e);function Mo(t){return 1===t/Math.pow(10,yo(t))}function wo(t,e,i){const s=Math.pow(10,i),n=Math.floor(t/s);return Math.ceil(e/s)-n}function ko(t,{min:e,max:i}){e=r(t.min,e);const s=[],n=yo(e);let o=function(t,e){let i=yo(e-t);for(;wo(t,e,i)>10;)i++;for(;wo(t,e,i)<10;)i--;return Math.min(i,yo(t))}(e,i),a=o<0?Math.pow(10,Math.abs(o)):1;const l=Math.pow(10,o),h=n>o?Math.pow(10,n):0,c=Math.round((e-h)*a)/a,d=Math.floor((e-h)/l/10)*l*10;let u=Math.floor((c-d)/Math.pow(10,o)),f=r(t.min,Math.round((h+d+u*Math.pow(10,o))*a)/a);for(;f=10?u=u<15?15:20:u++,u>=20&&(o++,u=2,a=o>=0?1:a),f=Math.round((h+d+u*Math.pow(10,o))*a)/a;const g=r(t.max,f);return s.push({value:g,major:Mo(g),significand:u}),s}class So extends tn{static id="logarithmic";static defaults={ticks:{callback:ae.formatters.logarithmic,major:{enabled:!0}}};constructor(t){super(t),this.start=void 0,this.end=void 0,this._startValue=void 0,this._valueRange=0}parse(t,e){const i=bo.prototype.parse.apply(this,[t,e]);if(0!==i)return a(i)&&i>0?i:null;this._zero=!0}determineDataLimits(){const{min:t,max:e}=this.getMinMax(!0);this.min=a(t)?Math.max(0,t):null,this.max=a(e)?Math.max(0,e):null,this.options.beginAtZero&&(this._zero=!0),this._zero&&this.min!==this._suggestedMin&&!a(this._userMin)&&(this.min=t===vo(this.min,0)?vo(this.min,-1):vo(this.min,0)),this.handleTickRangeOptions()}handleTickRangeOptions(){const{minDefined:t,maxDefined:e}=this.getUserBounds();let i=this.min,s=this.max;const n=e=>i=t?i:e,o=t=>s=e?s:t;i===s&&(i<=0?(n(1),o(10)):(n(vo(i,-1)),o(vo(s,1)))),i<=0&&n(vo(s,-1)),s<=0&&o(vo(i,1)),this.min=i,this.max=s}buildTicks(){const t=this.options,e=ko({min:this._userMin,max:this._userMax},this);return"ticks"===t.bounds&&j(e,this,"value"),t.reverse?(e.reverse(),this.start=this.max,this.end=this.min):(this.start=this.min,this.end=this.max),e}getLabelForValue(t){return void 0===t?"0":ne(t,this.chart.options.locale,this.options.ticks.format)}configure(){const t=this.min;super.configure(),this._startValue=z(t),this._valueRange=z(this.max)-z(t)}getPixelForValue(t){return void 0!==t&&0!==t||(t=this.min),null===t||isNaN(t)?NaN:this.getPixelForDecimal(t===this.min?0:(z(t)-this._startValue)/this._valueRange)}getValueForPixel(t){const e=this.getDecimalForPixel(t);return Math.pow(10,this._startValue+e*this._valueRange)}}function Po(t){const e=t.ticks;if(e.display&&t.display){const t=ki(e.backdropPadding);return l(e.font&&e.font.size,ue.font.size)+t.height}return 0}function Do(t,e,i,s,n){return t===s||t===n?{start:e-i/2,end:e+i/2}:tn?{start:e-i,end:e}:{start:e,end:e+i}}function Co(t){const e={l:t.left+t._padding.left,r:t.right-t._padding.right,t:t.top+t._padding.top,b:t.bottom-t._padding.bottom},i=Object.assign({},e),s=[],o=[],a=t._pointLabels.length,r=t.options.pointLabels,l=r.centerPointLabels?C/a:0;for(let u=0;ue.r&&(r=(s.end-e.r)/o,t.r=Math.max(t.r,e.r+r)),n.starte.b&&(l=(n.end-e.b)/a,t.b=Math.max(t.b,e.b+l))}function Ao(t,e,i){const s=t.drawingArea,{extra:n,additionalAngle:o,padding:a,size:r}=i,l=t.getPointPosition(e,s+n+a,o),h=Math.round(Y(G(l.angle+E))),c=function(t,e,i){90===i||270===i?t-=e/2:(i>270||i<90)&&(t-=e);return t}(l.y,r.h,h),d=function(t){if(0===t||180===t)return"center";if(t<180)return"left";return"right"}(h),u=function(t,e,i){"right"===i?t-=e:"center"===i&&(t-=e/2);return t}(l.x,r.w,d);return{visible:!0,x:l.x,y:c,textAlign:d,left:u,top:c,right:u+r.w,bottom:c+r.h}}function To(t,e){if(!e)return!0;const{left:i,top:s,right:n,bottom:o}=t;return!(Re({x:i,y:s},e)||Re({x:i,y:o},e)||Re({x:n,y:s},e)||Re({x:n,y:o},e))}function Lo(t,e,i){const{left:n,top:o,right:a,bottom:r}=i,{backdropColor:l}=e;if(!s(l)){const i=wi(e.borderRadius),s=ki(e.backdropPadding);t.fillStyle=l;const h=n-s.left,c=o-s.top,d=a-n+s.width,u=r-o+s.height;Object.values(i).some((t=>0!==t))?(t.beginPath(),He(t,{x:h,y:c,w:d,h:u,radius:i}),t.fill()):t.fillRect(h,c,d,u)}}function Eo(t,e,i,s){const{ctx:n}=t;if(i)n.arc(t.xCenter,t.yCenter,e,0,O);else{let i=t.getPointPosition(0,e);n.moveTo(i.x,i.y);for(let o=1;ot,padding:5,centerPointLabels:!1}};static defaultRoutes={"angleLines.color":"borderColor","pointLabels.color":"color","ticks.color":"color"};static descriptors={angleLines:{_fallback:"grid"}};constructor(t){super(t),this.xCenter=void 0,this.yCenter=void 0,this.drawingArea=void 0,this._pointLabels=[],this._pointLabelItems=[]}setDimensions(){const t=this._padding=ki(Po(this.options)/2),e=this.width=this.maxWidth-t.width,i=this.height=this.maxHeight-t.height;this.xCenter=Math.floor(this.left+e/2+t.left),this.yCenter=Math.floor(this.top+i/2+t.top),this.drawingArea=Math.floor(Math.min(e,i)/2)}determineDataLimits(){const{min:t,max:e}=this.getMinMax(!1);this.min=a(t)&&!isNaN(t)?t:0,this.max=a(e)&&!isNaN(e)?e:0,this.handleTickRangeOptions()}computeTickLimit(){return Math.ceil(this.drawingArea/Po(this.options))}generateTickLabels(t){bo.prototype.generateTickLabels.call(this,t),this._pointLabels=this.getLabels().map(((t,e)=>{const i=d(this.options.pointLabels.callback,[t,e],this);return i||0===i?i:""})).filter(((t,e)=>this.chart.getDataVisibility(e)))}fit(){const t=this.options;t.display&&t.pointLabels.display?Co(this):this.setCenterPoint(0,0,0,0)}setCenterPoint(t,e,i,s){this.xCenter+=Math.floor((t-e)/2),this.yCenter+=Math.floor((i-s)/2),this.drawingArea-=Math.min(this.drawingArea/2,Math.max(t,e,i,s))}getIndexAngle(t){return G(t*(O/(this._pointLabels.length||1))+$(this.options.startAngle||0))}getDistanceFromCenterForValue(t){if(s(t))return NaN;const e=this.drawingArea/(this.max-this.min);return this.options.reverse?(this.max-t)*e:(t-this.min)*e}getValueForDistanceFromCenter(t){if(s(t))return NaN;const e=t/(this.drawingArea/(this.max-this.min));return this.options.reverse?this.max-e:this.min+e}getPointLabelContext(t){const e=this._pointLabels||[];if(t>=0&&t=0;n--){const e=t._pointLabelItems[n];if(!e.visible)continue;const o=s.setContext(t.getPointLabelContext(n));Lo(i,o,e);const a=Si(o.font),{x:r,y:l,textAlign:h}=e;Ne(i,t._pointLabels[n],r,l+a.lineHeight/2,a,{color:o.color,textAlign:h,textBaseline:"middle"})}}(this,o),s.display&&this.ticks.forEach(((t,e)=>{if(0!==e||0===e&&this.min<0){r=this.getDistanceFromCenterForValue(t.value);const i=this.getContext(e),a=s.setContext(i),l=n.setContext(i);!function(t,e,i,s,n){const o=t.ctx,a=e.circular,{color:r,lineWidth:l}=e;!a&&!s||!r||!l||i<0||(o.save(),o.strokeStyle=r,o.lineWidth=l,o.setLineDash(n.dash||[]),o.lineDashOffset=n.dashOffset,o.beginPath(),Eo(t,i,a,s),o.closePath(),o.stroke(),o.restore())}(this,a,r,o,l)}})),i.display){for(t.save(),a=o-1;a>=0;a--){const s=i.setContext(this.getPointLabelContext(a)),{color:n,lineWidth:o}=s;o&&n&&(t.lineWidth=o,t.strokeStyle=n,t.setLineDash(s.borderDash),t.lineDashOffset=s.borderDashOffset,r=this.getDistanceFromCenterForValue(e.reverse?this.min:this.max),l=this.getPointPosition(a,r),t.beginPath(),t.moveTo(this.xCenter,this.yCenter),t.lineTo(l.x,l.y),t.stroke())}t.restore()}}drawBorder(){}drawLabels(){const t=this.ctx,e=this.options,i=e.ticks;if(!i.display)return;const s=this.getIndexAngle(0);let n,o;t.save(),t.translate(this.xCenter,this.yCenter),t.rotate(s),t.textAlign="center",t.textBaseline="middle",this.ticks.forEach(((s,a)=>{if(0===a&&this.min>=0&&!e.reverse)return;const r=i.setContext(this.getContext(a)),l=Si(r.font);if(n=this.getDistanceFromCenterForValue(this.ticks[a].value),r.showLabelBackdrop){t.font=l.string,o=t.measureText(s.label).width,t.fillStyle=r.backdropColor;const e=ki(r.backdropPadding);t.fillRect(-o/2-e.left,-n-l.size/2-e.top,o+e.width,l.size+e.height)}Ne(t,s.label,0,-n,l,{color:r.color,strokeColor:r.textStrokeColor,strokeWidth:r.textStrokeWidth})})),t.restore()}drawTitle(){}}const Io={millisecond:{common:!0,size:1,steps:1e3},second:{common:!0,size:1e3,steps:60},minute:{common:!0,size:6e4,steps:60},hour:{common:!0,size:36e5,steps:24},day:{common:!0,size:864e5,steps:30},week:{common:!1,size:6048e5,steps:4},month:{common:!0,size:2628e6,steps:12},quarter:{common:!1,size:7884e6,steps:4},year:{common:!0,size:3154e7}},zo=Object.keys(Io);function Fo(t,e){return t-e}function Vo(t,e){if(s(e))return null;const i=t._adapter,{parser:n,round:o,isoWeekday:r}=t._parseOpts;let l=e;return"function"==typeof n&&(l=n(l)),a(l)||(l="string"==typeof n?i.parse(l,n):i.parse(l)),null===l?null:(o&&(l="week"!==o||!N(r)&&!0!==r?i.startOf(l,o):i.startOf(l,"isoWeek",r)),+l)}function Bo(t,e,i,s){const n=zo.length;for(let o=zo.indexOf(t);o=e?i[s]:i[n]]=!0}}else t[e]=!0}function No(t,e,i){const s=[],n={},o=e.length;let a,r;for(a=0;a=0&&(e[l].major=!0);return e}(t,s,n,i):s}class Ho extends tn{static id="time";static defaults={bounds:"data",adapters:{},time:{parser:!1,unit:!1,round:!1,isoWeekday:!1,minUnit:"millisecond",displayFormats:{}},ticks:{source:"auto",callback:!1,major:{enabled:!1}}};constructor(t){super(t),this._cache={data:[],labels:[],all:[]},this._unit="day",this._majorUnit=void 0,this._offsets={},this._normalized=!1,this._parseOpts=void 0}init(t,e={}){const i=t.time||(t.time={}),s=this._adapter=new In._date(t.adapters.date);s.init(e),b(i.displayFormats,s.formats()),this._parseOpts={parser:i.parser,round:i.round,isoWeekday:i.isoWeekday},super.init(t),this._normalized=e.normalized}parse(t,e){return void 0===t?null:Vo(this,t)}beforeLayout(){super.beforeLayout(),this._cache={data:[],labels:[],all:[]}}determineDataLimits(){const t=this.options,e=this._adapter,i=t.time.unit||"day";let{min:s,max:n,minDefined:o,maxDefined:r}=this.getUserBounds();function l(t){o||isNaN(t.min)||(s=Math.min(s,t.min)),r||isNaN(t.max)||(n=Math.max(n,t.max))}o&&r||(l(this._getLabelBounds()),"ticks"===t.bounds&&"labels"===t.ticks.source||l(this.getMinMax(!1))),s=a(s)&&!isNaN(s)?s:+e.startOf(Date.now(),i),n=a(n)&&!isNaN(n)?n:+e.endOf(Date.now(),i)+1,this.min=Math.min(s,n-1),this.max=Math.max(s+1,n)}_getLabelBounds(){const t=this.getLabelTimestamps();let e=Number.POSITIVE_INFINITY,i=Number.NEGATIVE_INFINITY;return t.length&&(e=t[0],i=t[t.length-1]),{min:e,max:i}}buildTicks(){const t=this.options,e=t.time,i=t.ticks,s="labels"===i.source?this.getLabelTimestamps():this._generate();"ticks"===t.bounds&&s.length&&(this.min=this._userMin||s[0],this.max=this._userMax||s[s.length-1]);const n=this.min,o=nt(s,n,this.max);return this._unit=e.unit||(i.autoSkip?Bo(e.minUnit,this.min,this.max,this._getLabelCapacity(n)):function(t,e,i,s,n){for(let o=zo.length-1;o>=zo.indexOf(i);o--){const i=zo[o];if(Io[i].common&&t._adapter.diff(n,s,i)>=e-1)return i}return zo[i?zo.indexOf(i):0]}(this,o.length,e.minUnit,this.min,this.max)),this._majorUnit=i.major.enabled&&"year"!==this._unit?function(t){for(let e=zo.indexOf(t)+1,i=zo.length;e+t.value)))}initOffsets(t=[]){let e,i,s=0,n=0;this.options.offset&&t.length&&(e=this.getDecimalForValue(t[0]),s=1===t.length?1-e:(this.getDecimalForValue(t[1])-e)/2,i=this.getDecimalForValue(t[t.length-1]),n=1===t.length?i:(i-this.getDecimalForValue(t[t.length-2]))/2);const o=t.length<3?.5:.25;s=Z(s,0,o),n=Z(n,0,o),this._offsets={start:s,end:n,factor:1/(s+1+n)}}_generate(){const t=this._adapter,e=this.min,i=this.max,s=this.options,n=s.time,o=n.unit||Bo(n.minUnit,e,i,this._getLabelCapacity(e)),a=l(s.ticks.stepSize,1),r="week"===o&&n.isoWeekday,h=N(r)||!0===r,c={};let d,u,f=e;if(h&&(f=+t.startOf(f,"isoWeek",r)),f=+t.startOf(f,h?"day":o),t.diff(i,e,o)>1e5*a)throw new Error(e+" and "+i+" are too far apart with stepSize of "+a+" "+o);const g="data"===s.ticks.source&&this.getDataTimestamps();for(d=f,u=0;d+t))}getLabelForValue(t){const e=this._adapter,i=this.options.time;return i.tooltipFormat?e.format(t,i.tooltipFormat):e.format(t,i.displayFormats.datetime)}format(t,e){const i=this.options.time.displayFormats,s=this._unit,n=e||i[s];return this._adapter.format(t,n)}_tickFormatFunction(t,e,i,s){const n=this.options,o=n.ticks.callback;if(o)return d(o,[t,e,i],this);const a=n.time.displayFormats,r=this._unit,l=this._majorUnit,h=r&&a[r],c=l&&a[l],u=i[e],f=l&&c&&u&&u.major;return this._adapter.format(t,s||(f?c:h))}generateTickLabels(t){let e,i,s;for(e=0,i=t.length;e0?a:1}getDataTimestamps(){let t,e,i=this._cache.data||[];if(i.length)return i;const s=this.getMatchingVisibleMetas();if(this._normalized&&s.length)return this._cache.data=s[0].controller.getAllParsedValues(this);for(t=0,e=s.length;t=t[r].pos&&e<=t[l].pos&&({lo:r,hi:l}=it(t,"pos",e)),({pos:s,time:o}=t[r]),({pos:n,time:a}=t[l])):(e>=t[r].time&&e<=t[l].time&&({lo:r,hi:l}=it(t,"time",e)),({time:s,pos:o}=t[r]),({time:n,pos:a}=t[l]));const h=n-s;return h?o+(a-o)*(e-s)/h:o}var $o=Object.freeze({__proto__:null,CategoryScale:class extends tn{static id="category";static defaults={ticks:{callback:mo}};constructor(t){super(t),this._startValue=void 0,this._valueRange=0,this._addedLabels=[]}init(t){const e=this._addedLabels;if(e.length){const t=this.getLabels();for(const{index:i,label:s}of e)t[i]===s&&t.splice(i,1);this._addedLabels=[]}super.init(t)}parse(t,e){if(s(t))return null;const i=this.getLabels();return((t,e)=>null===t?null:Z(Math.round(t),0,e))(e=isFinite(e)&&i[e]===t?e:po(i,t,l(e,t),this._addedLabels),i.length-1)}determineDataLimits(){const{minDefined:t,maxDefined:e}=this.getUserBounds();let{min:i,max:s}=this.getMinMax(!0);"ticks"===this.options.bounds&&(t||(i=0),e||(s=this.getLabels().length-1)),this.min=i,this.max=s}buildTicks(){const t=this.min,e=this.max,i=this.options.offset,s=[];let n=this.getLabels();n=0===t&&e===n.length-1?n:n.slice(t,e+1),this._valueRange=Math.max(n.length-(i?0:1),1),this._startValue=this.min-(i?.5:0);for(let i=t;i<=e;i++)s.push({value:i});return s}getLabelForValue(t){return mo.call(this,t)}configure(){super.configure(),this.isHorizontal()||(this._reversePixels=!this._reversePixels)}getPixelForValue(t){return"number"!=typeof t&&(t=this.parse(t)),null===t?NaN:this.getPixelForDecimal((t-this._startValue)/this._valueRange)}getPixelForTick(t){const e=this.ticks;return t<0||t>e.length-1?null:this.getPixelForValue(e[t].value)}getValueForPixel(t){return Math.round(this._startValue+this.getDecimalForPixel(t)*this._valueRange)}getBasePixel(){return this.bottom}},LinearScale:_o,LogarithmicScale:So,RadialLinearScale:Ro,TimeScale:Ho,TimeSeriesScale:class extends Ho{static id="timeseries";static defaults=Ho.defaults;constructor(t){super(t),this._table=[],this._minPos=void 0,this._tableRange=void 0}initOffsets(){const t=this._getTimestampsForTable(),e=this._table=this.buildLookupTable(t);this._minPos=jo(e,this.min),this._tableRange=jo(e,this.max)-this._minPos,super.initOffsets(t)}buildLookupTable(t){const{min:e,max:i}=this,s=[],n=[];let o,a,r,l,h;for(o=0,a=t.length;o=e&&l<=i&&s.push(l);if(s.length<2)return[{time:e,pos:0},{time:i,pos:1}];for(o=0,a=s.length;ot-e))}_getTimestampsForTable(){let t=this._cache.all||[];if(t.length)return t;const e=this.getDataTimestamps(),i=this.getLabelTimestamps();return t=e.length&&i.length?this.normalize(e.concat(i)):e.length?e:i,t=this._cache.all=t,t}getDecimalForValue(t){return(jo(this._table,t)-this._minPos)/this._tableRange}getValueForPixel(t){const e=this._offsets,i=this.getDecimalForPixel(t)/e.factor-e.end;return jo(this._table,i*this._tableRange+this._minPos,!0)}}});const Yo=["rgb(54, 162, 235)","rgb(255, 99, 132)","rgb(255, 159, 64)","rgb(255, 205, 86)","rgb(75, 192, 192)","rgb(153, 102, 255)","rgb(201, 203, 207)"],Uo=Yo.map((t=>t.replace("rgb(","rgba(").replace(")",", 0.5)")));function Xo(t){return Yo[t%Yo.length]}function qo(t){return Uo[t%Uo.length]}function Ko(t){let e=0;return(i,s)=>{const n=t.getDatasetMeta(s).controller;n instanceof $n?e=function(t,e){return t.backgroundColor=t.data.map((()=>Xo(e++))),e}(i,e):n instanceof Yn?e=function(t,e){return t.backgroundColor=t.data.map((()=>qo(e++))),e}(i,e):n&&(e=function(t,e){return t.borderColor=Xo(e),t.backgroundColor=qo(e),++e}(i,e))}}function Go(t){let e;for(e in t)if(t[e].borderColor||t[e].backgroundColor)return!0;return!1}var Jo={id:"colors",defaults:{enabled:!0,forceOverride:!1},beforeLayout(t,e,i){if(!i.enabled)return;const{data:{datasets:s},options:n}=t.config,{elements:o}=n,a=Go(s)||(r=n)&&(r.borderColor||r.backgroundColor)||o&&Go(o)||"rgba(0,0,0,0.1)"!==ue.borderColor||"rgba(0,0,0,0.1)"!==ue.backgroundColor;var r;if(!i.forceOverride&&a)return;const l=Ko(t);s.forEach(l)}};function Zo(t){if(t._decimated){const e=t._data;delete t._decimated,delete t._data,Object.defineProperty(t,"data",{configurable:!0,enumerable:!0,writable:!0,value:e})}}function Qo(t){t.data.datasets.forEach((t=>{Zo(t)}))}var ta={id:"decimation",defaults:{algorithm:"min-max",enabled:!1},beforeElementsUpdate:(t,e,i)=>{if(!i.enabled)return void Qo(t);const n=t.width;t.data.datasets.forEach(((e,o)=>{const{_data:a,indexAxis:r}=e,l=t.getDatasetMeta(o),h=a||e.data;if("y"===Pi([r,t.options.indexAxis]))return;if(!l.controller.supportsDecimation)return;const c=t.scales[l.xAxisID];if("linear"!==c.type&&"time"!==c.type)return;if(t.options.parsing)return;let{start:d,count:u}=function(t,e){const i=e.length;let s,n=0;const{iScale:o}=t,{min:a,max:r,minDefined:l,maxDefined:h}=o.getUserBounds();return l&&(n=Z(it(e,o.axis,a).lo,0,i-1)),s=h?Z(it(e,o.axis,r).hi+1,n,i)-n:i-n,{start:n,count:s}}(l,h);if(u<=(i.threshold||4*n))return void Zo(e);let f;switch(s(a)&&(e._data=h,delete e.data,Object.defineProperty(e,"data",{configurable:!0,enumerable:!0,get:function(){return this._decimated},set:function(t){this._data=t}})),i.algorithm){case"lttb":f=function(t,e,i,s,n){const o=n.samples||s;if(o>=i)return t.slice(e,e+i);const a=[],r=(i-2)/(o-2);let l=0;const h=e+i-1;let c,d,u,f,g,p=e;for(a[l++]=t[p],c=0;cu&&(u=f,d=t[s],g=s);a[l++]=d,p=g}return a[l++]=t[h],a}(h,d,u,n,i);break;case"min-max":f=function(t,e,i,n){let o,a,r,l,h,c,d,u,f,g,p=0,m=0;const x=[],b=e+i-1,_=t[e].x,y=t[b].x-_;for(o=e;og&&(g=l,d=o),p=(m*p+a.x)/++m;else{const i=o-1;if(!s(c)&&!s(d)){const e=Math.min(c,d),s=Math.max(c,d);e!==u&&e!==i&&x.push({...t[e],x:p}),s!==u&&s!==i&&x.push({...t[s],x:p})}o>0&&i!==u&&x.push(t[i]),x.push(a),h=e,m=0,f=g=l,c=d=u=o}}return x}(h,d,u,n);break;default:throw new Error(`Unsupported decimation algorithm '${i.algorithm}'`)}e._decimated=f}))},destroy(t){Qo(t)}};function ea(t,e,i,s){if(s)return;let n=e[t],o=i[t];return"angle"===t&&(n=G(n),o=G(o)),{property:t,start:n,end:o}}function ia(t,e,i){for(;e>t;e--){const t=i[e];if(!isNaN(t.x)&&!isNaN(t.y))break}return e}function sa(t,e,i,s){return t&&e?s(t[i],e[i]):t?t[i]:e?e[i]:0}function na(t,e){let i=[],s=!1;return n(t)?(s=!0,i=t):i=function(t,e){const{x:i=null,y:s=null}=t||{},n=e.points,o=[];return e.segments.forEach((({start:t,end:e})=>{e=ia(t,e,n);const a=n[t],r=n[e];null!==s?(o.push({x:a.x,y:s}),o.push({x:r.x,y:s})):null!==i&&(o.push({x:i,y:a.y}),o.push({x:i,y:r.y}))})),o}(t,e),i.length?new oo({points:i,options:{tension:0},_loop:s,_fullLoop:s}):null}function oa(t){return t&&!1!==t.fill}function aa(t,e,i){let s=t[e].fill;const n=[e];let o;if(!i)return s;for(;!1!==s&&-1===n.indexOf(s);){if(!a(s))return s;if(o=t[s],!o)return!1;if(o.visible)return s;n.push(s),s=o.fill}return!1}function ra(t,e,i){const s=function(t){const e=t.options,i=e.fill;let s=l(i&&i.target,i);void 0===s&&(s=!!e.backgroundColor);if(!1===s||null===s)return!1;if(!0===s)return"origin";return s}(t);if(o(s))return!isNaN(s.value)&&s;let n=parseFloat(s);return a(n)&&Math.floor(n)===n?function(t,e,i,s){"-"!==t&&"+"!==t||(i=e+i);if(i===e||i<0||i>=s)return!1;return i}(s[0],e,n,i):["origin","start","end","stack","shape"].indexOf(s)>=0&&s}function la(t,e,i){const s=[];for(let n=0;n=0;--e){const i=n[e].$filler;i&&(i.line.updateControlPoints(o,i.axis),s&&i.fill&&ua(t.ctx,i,o))}},beforeDatasetsDraw(t,e,i){if("beforeDatasetsDraw"!==i.drawTime)return;const s=t.getSortedVisibleDatasetMetas();for(let e=s.length-1;e>=0;--e){const i=s[e].$filler;oa(i)&&ua(t.ctx,i,t.chartArea)}},beforeDatasetDraw(t,e,i){const s=e.meta.$filler;oa(s)&&"beforeDatasetDraw"===i.drawTime&&ua(t.ctx,s,t.chartArea)},defaults:{propagate:!0,drawTime:"beforeDatasetDraw"}};const _a=(t,e)=>{let{boxHeight:i=e,boxWidth:s=e}=t;return t.usePointStyle&&(i=Math.min(i,e),s=t.pointStyleWidth||Math.min(s,e)),{boxWidth:s,boxHeight:i,itemHeight:Math.max(e,i)}};class ya extends $s{constructor(t){super(),this._added=!1,this.legendHitBoxes=[],this._hoveredItem=null,this.doughnutMode=!1,this.chart=t.chart,this.options=t.options,this.ctx=t.ctx,this.legendItems=void 0,this.columnSizes=void 0,this.lineWidths=void 0,this.maxHeight=void 0,this.maxWidth=void 0,this.top=void 0,this.bottom=void 0,this.left=void 0,this.right=void 0,this.height=void 0,this.width=void 0,this._margins=void 0,this.position=void 0,this.weight=void 0,this.fullSize=void 0}update(t,e,i){this.maxWidth=t,this.maxHeight=e,this._margins=i,this.setDimensions(),this.buildLabels(),this.fit()}setDimensions(){this.isHorizontal()?(this.width=this.maxWidth,this.left=this._margins.left,this.right=this.width):(this.height=this.maxHeight,this.top=this._margins.top,this.bottom=this.height)}buildLabels(){const t=this.options.labels||{};let e=d(t.generateLabels,[this.chart],this)||[];t.filter&&(e=e.filter((e=>t.filter(e,this.chart.data)))),t.sort&&(e=e.sort(((e,i)=>t.sort(e,i,this.chart.data)))),this.options.reverse&&e.reverse(),this.legendItems=e}fit(){const{options:t,ctx:e}=this;if(!t.display)return void(this.width=this.height=0);const i=t.labels,s=Si(i.font),n=s.size,o=this._computeTitleHeight(),{boxWidth:a,itemHeight:r}=_a(i,n);let l,h;e.font=s.string,this.isHorizontal()?(l=this.maxWidth,h=this._fitRows(o,n,a,r)+10):(h=this.maxHeight,l=this._fitCols(o,s,a,r)+10),this.width=Math.min(l,t.maxWidth||this.maxWidth),this.height=Math.min(h,t.maxHeight||this.maxHeight)}_fitRows(t,e,i,s){const{ctx:n,maxWidth:o,options:{labels:{padding:a}}}=this,r=this.legendHitBoxes=[],l=this.lineWidths=[0],h=s+a;let c=t;n.textAlign="left",n.textBaseline="middle";let d=-1,u=-h;return this.legendItems.forEach(((t,f)=>{const g=i+e/2+n.measureText(t.text).width;(0===f||l[l.length-1]+g+2*a>o)&&(c+=h,l[l.length-(f>0?0:1)]=0,u+=h,d++),r[f]={left:0,top:u,row:d,width:g,height:s},l[l.length-1]+=g+a})),c}_fitCols(t,e,i,s){const{ctx:n,maxHeight:o,options:{labels:{padding:a}}}=this,r=this.legendHitBoxes=[],l=this.columnSizes=[],h=o-t;let c=a,d=0,u=0,f=0,g=0;return this.legendItems.forEach(((t,o)=>{const{itemWidth:p,itemHeight:m}=function(t,e,i,s,n){const o=function(t,e,i,s){let n=t.text;n&&"string"!=typeof n&&(n=n.reduce(((t,e)=>t.length>e.length?t:e)));return e+i.size/2+s.measureText(n).width}(s,t,e,i),a=function(t,e,i){let s=t;"string"!=typeof e.text&&(s=va(e,i));return s}(n,s,e.lineHeight);return{itemWidth:o,itemHeight:a}}(i,e,n,t,s);o>0&&u+m+2*a>h&&(c+=d+a,l.push({width:d,height:u}),f+=d+a,g++,d=u=0),r[o]={left:f,top:u,col:g,width:p,height:m},d=Math.max(d,p),u+=m+a})),c+=d,l.push({width:d,height:u}),c}adjustHitBoxes(){if(!this.options.display)return;const t=this._computeTitleHeight(),{legendHitBoxes:e,options:{align:i,labels:{padding:s},rtl:n}}=this,o=Oi(n,this.left,this.width);if(this.isHorizontal()){let n=0,a=ft(i,this.left+s,this.right-this.lineWidths[n]);for(const r of e)n!==r.row&&(n=r.row,a=ft(i,this.left+s,this.right-this.lineWidths[n])),r.top+=this.top+t+s,r.left=o.leftForLtr(o.x(a),r.width),a+=r.width+s}else{let n=0,a=ft(i,this.top+t+s,this.bottom-this.columnSizes[n].height);for(const r of e)r.col!==n&&(n=r.col,a=ft(i,this.top+t+s,this.bottom-this.columnSizes[n].height)),r.top=a,r.left+=this.left+s,r.left=o.leftForLtr(o.x(r.left),r.width),a+=r.height+s}}isHorizontal(){return"top"===this.options.position||"bottom"===this.options.position}draw(){if(this.options.display){const t=this.ctx;Ie(t,this),this._draw(),ze(t)}}_draw(){const{options:t,columnSizes:e,lineWidths:i,ctx:s}=this,{align:n,labels:o}=t,a=ue.color,r=Oi(t.rtl,this.left,this.width),h=Si(o.font),{padding:c}=o,d=h.size,u=d/2;let f;this.drawTitle(),s.textAlign=r.textAlign("left"),s.textBaseline="middle",s.lineWidth=.5,s.font=h.string;const{boxWidth:g,boxHeight:p,itemHeight:m}=_a(o,d),x=this.isHorizontal(),b=this._computeTitleHeight();f=x?{x:ft(n,this.left+c,this.right-i[0]),y:this.top+c+b,line:0}:{x:this.left+c,y:ft(n,this.top+b+c,this.bottom-e[0].height),line:0},Ai(this.ctx,t.textDirection);const _=m+c;this.legendItems.forEach(((y,v)=>{s.strokeStyle=y.fontColor,s.fillStyle=y.fontColor;const M=s.measureText(y.text).width,w=r.textAlign(y.textAlign||(y.textAlign=o.textAlign)),k=g+u+M;let S=f.x,P=f.y;r.setWidth(this.width),x?v>0&&S+k+c>this.right&&(P=f.y+=_,f.line++,S=f.x=ft(n,this.left+c,this.right-i[f.line])):v>0&&P+_>this.bottom&&(S=f.x=S+e[f.line].width+c,f.line++,P=f.y=ft(n,this.top+b+c,this.bottom-e[f.line].height));if(function(t,e,i){if(isNaN(g)||g<=0||isNaN(p)||p<0)return;s.save();const n=l(i.lineWidth,1);if(s.fillStyle=l(i.fillStyle,a),s.lineCap=l(i.lineCap,"butt"),s.lineDashOffset=l(i.lineDashOffset,0),s.lineJoin=l(i.lineJoin,"miter"),s.lineWidth=n,s.strokeStyle=l(i.strokeStyle,a),s.setLineDash(l(i.lineDash,[])),o.usePointStyle){const a={radius:p*Math.SQRT2/2,pointStyle:i.pointStyle,rotation:i.rotation,borderWidth:n},l=r.xPlus(t,g/2);Ee(s,a,l,e+u,o.pointStyleWidth&&g)}else{const o=e+Math.max((d-p)/2,0),a=r.leftForLtr(t,g),l=wi(i.borderRadius);s.beginPath(),Object.values(l).some((t=>0!==t))?He(s,{x:a,y:o,w:g,h:p,radius:l}):s.rect(a,o,g,p),s.fill(),0!==n&&s.stroke()}s.restore()}(r.x(S),P,y),S=gt(w,S+g+u,x?S+k:this.right,t.rtl),function(t,e,i){Ne(s,i.text,t,e+m/2,h,{strikethrough:i.hidden,textAlign:r.textAlign(i.textAlign)})}(r.x(S),P,y),x)f.x+=k+c;else if("string"!=typeof y.text){const t=h.lineHeight;f.y+=va(y,t)+c}else f.y+=_})),Ti(this.ctx,t.textDirection)}drawTitle(){const t=this.options,e=t.title,i=Si(e.font),s=ki(e.padding);if(!e.display)return;const n=Oi(t.rtl,this.left,this.width),o=this.ctx,a=e.position,r=i.size/2,l=s.top+r;let h,c=this.left,d=this.width;if(this.isHorizontal())d=Math.max(...this.lineWidths),h=this.top+l,c=ft(t.align,c,this.right-d);else{const e=this.columnSizes.reduce(((t,e)=>Math.max(t,e.height)),0);h=l+ft(t.align,this.top,this.bottom-e-t.labels.padding-this._computeTitleHeight())}const u=ft(a,c,c+d);o.textAlign=n.textAlign(ut(a)),o.textBaseline="middle",o.strokeStyle=e.color,o.fillStyle=e.color,o.font=i.string,Ne(o,e.text,u,h,i)}_computeTitleHeight(){const t=this.options.title,e=Si(t.font),i=ki(t.padding);return t.display?e.lineHeight+i.height:0}_getLegendItemAt(t,e){let i,s,n;if(tt(t,this.left,this.right)&&tt(e,this.top,this.bottom))for(n=this.legendHitBoxes,i=0;it.chart.options.color,boxWidth:40,padding:10,generateLabels(t){const e=t.data.datasets,{labels:{usePointStyle:i,pointStyle:s,textAlign:n,color:o,useBorderRadius:a,borderRadius:r}}=t.legend.options;return t._getSortedDatasetMetas().map((t=>{const l=t.controller.getStyle(i?0:void 0),h=ki(l.borderWidth);return{text:e[t.index].label,fillStyle:l.backgroundColor,fontColor:o,hidden:!t.visible,lineCap:l.borderCapStyle,lineDash:l.borderDash,lineDashOffset:l.borderDashOffset,lineJoin:l.borderJoinStyle,lineWidth:(h.width+h.height)/4,strokeStyle:l.borderColor,pointStyle:s||l.pointStyle,rotation:l.rotation,textAlign:n||l.textAlign,borderRadius:a&&(r||l.borderRadius),datasetIndex:t.index}}),this)}},title:{color:t=>t.chart.options.color,display:!1,position:"center",text:""}},descriptors:{_scriptable:t=>!t.startsWith("on"),labels:{_scriptable:t=>!["generateLabels","filter","sort"].includes(t)}}};class wa extends $s{constructor(t){super(),this.chart=t.chart,this.options=t.options,this.ctx=t.ctx,this._padding=void 0,this.top=void 0,this.bottom=void 0,this.left=void 0,this.right=void 0,this.width=void 0,this.height=void 0,this.position=void 0,this.weight=void 0,this.fullSize=void 0}update(t,e){const i=this.options;if(this.left=0,this.top=0,!i.display)return void(this.width=this.height=this.right=this.bottom=0);this.width=this.right=t,this.height=this.bottom=e;const s=n(i.text)?i.text.length:1;this._padding=ki(i.padding);const o=s*Si(i.font).lineHeight+this._padding.height;this.isHorizontal()?this.height=o:this.width=o}isHorizontal(){const t=this.options.position;return"top"===t||"bottom"===t}_drawArgs(t){const{top:e,left:i,bottom:s,right:n,options:o}=this,a=o.align;let r,l,h,c=0;return this.isHorizontal()?(l=ft(a,i,n),h=e+t,r=n-i):("left"===o.position?(l=i+t,h=ft(a,s,e),c=-.5*C):(l=n-t,h=ft(a,e,s),c=.5*C),r=s-e),{titleX:l,titleY:h,maxWidth:r,rotation:c}}draw(){const t=this.ctx,e=this.options;if(!e.display)return;const i=Si(e.font),s=i.lineHeight/2+this._padding.top,{titleX:n,titleY:o,maxWidth:a,rotation:r}=this._drawArgs(s);Ne(t,e.text,0,0,i,{color:e.color,maxWidth:a,rotation:r,textAlign:ut(e.align),textBaseline:"middle",translation:[n,o]})}}var ka={id:"title",_element:wa,start(t,e,i){!function(t,e){const i=new wa({ctx:t.ctx,options:e,chart:t});ls.configure(t,i,e),ls.addBox(t,i),t.titleBlock=i}(t,i)},stop(t){const e=t.titleBlock;ls.removeBox(t,e),delete t.titleBlock},beforeUpdate(t,e,i){const s=t.titleBlock;ls.configure(t,s,i),s.options=i},defaults:{align:"center",display:!1,font:{weight:"bold"},fullSize:!0,padding:10,position:"top",text:"",weight:2e3},defaultRoutes:{color:"color"},descriptors:{_scriptable:!0,_indexable:!1}};const Sa=new WeakMap;var Pa={id:"subtitle",start(t,e,i){const s=new wa({ctx:t.ctx,options:i,chart:t});ls.configure(t,s,i),ls.addBox(t,s),Sa.set(t,s)},stop(t){ls.removeBox(t,Sa.get(t)),Sa.delete(t)},beforeUpdate(t,e,i){const s=Sa.get(t);ls.configure(t,s,i),s.options=i},defaults:{align:"center",display:!1,font:{weight:"normal"},fullSize:!0,padding:0,position:"top",text:"",weight:1500},defaultRoutes:{color:"color"},descriptors:{_scriptable:!0,_indexable:!1}};const Da={average(t){if(!t.length)return!1;let e,i,s=new Set,n=0,o=0;for(e=0,i=t.length;et+e))/s.size,y:n/o}},nearest(t,e){if(!t.length)return!1;let i,s,n,o=e.x,a=e.y,r=Number.POSITIVE_INFINITY;for(i=0,s=t.length;i-1?t.split("\n"):t}function Aa(t,e){const{element:i,datasetIndex:s,index:n}=e,o=t.getDatasetMeta(s).controller,{label:a,value:r}=o.getLabelAndValue(n);return{chart:t,label:a,parsed:o.getParsed(n),raw:t.data.datasets[s].data[n],formattedValue:r,dataset:o.getDataset(),dataIndex:n,datasetIndex:s,element:i}}function Ta(t,e){const i=t.chart.ctx,{body:s,footer:n,title:o}=t,{boxWidth:a,boxHeight:r}=e,l=Si(e.bodyFont),h=Si(e.titleFont),c=Si(e.footerFont),d=o.length,f=n.length,g=s.length,p=ki(e.padding);let m=p.height,x=0,b=s.reduce(((t,e)=>t+e.before.length+e.lines.length+e.after.length),0);if(b+=t.beforeBody.length+t.afterBody.length,d&&(m+=d*h.lineHeight+(d-1)*e.titleSpacing+e.titleMarginBottom),b){m+=g*(e.displayColors?Math.max(r,l.lineHeight):l.lineHeight)+(b-g)*l.lineHeight+(b-1)*e.bodySpacing}f&&(m+=e.footerMarginTop+f*c.lineHeight+(f-1)*e.footerSpacing);let _=0;const y=function(t){x=Math.max(x,i.measureText(t).width+_)};return i.save(),i.font=h.string,u(t.title,y),i.font=l.string,u(t.beforeBody.concat(t.afterBody),y),_=e.displayColors?a+2+e.boxPadding:0,u(s,(t=>{u(t.before,y),u(t.lines,y),u(t.after,y)})),_=0,i.font=c.string,u(t.footer,y),i.restore(),x+=p.width,{width:x,height:m}}function La(t,e,i,s){const{x:n,width:o}=i,{width:a,chartArea:{left:r,right:l}}=t;let h="center";return"center"===s?h=n<=(r+l)/2?"left":"right":n<=o/2?h="left":n>=a-o/2&&(h="right"),function(t,e,i,s){const{x:n,width:o}=s,a=i.caretSize+i.caretPadding;return"left"===t&&n+o+a>e.width||"right"===t&&n-o-a<0||void 0}(h,t,e,i)&&(h="center"),h}function Ea(t,e,i){const s=i.yAlign||e.yAlign||function(t,e){const{y:i,height:s}=e;return it.height-s/2?"bottom":"center"}(t,i);return{xAlign:i.xAlign||e.xAlign||La(t,e,i,s),yAlign:s}}function Ra(t,e,i,s){const{caretSize:n,caretPadding:o,cornerRadius:a}=t,{xAlign:r,yAlign:l}=i,h=n+o,{topLeft:c,topRight:d,bottomLeft:u,bottomRight:f}=wi(a);let g=function(t,e){let{x:i,width:s}=t;return"right"===e?i-=s:"center"===e&&(i-=s/2),i}(e,r);const p=function(t,e,i){let{y:s,height:n}=t;return"top"===e?s+=i:s-="bottom"===e?n+i:n/2,s}(e,l,h);return"center"===l?"left"===r?g+=h:"right"===r&&(g-=h):"left"===r?g-=Math.max(c,u)+n:"right"===r&&(g+=Math.max(d,f)+n),{x:Z(g,0,s.width-e.width),y:Z(p,0,s.height-e.height)}}function Ia(t,e,i){const s=ki(i.padding);return"center"===e?t.x+t.width/2:"right"===e?t.x+t.width-s.right:t.x+s.left}function za(t){return Ca([],Oa(t))}function Fa(t,e){const i=e&&e.dataset&&e.dataset.tooltip&&e.dataset.tooltip.callbacks;return i?t.override(i):t}const Va={beforeTitle:e,title(t){if(t.length>0){const e=t[0],i=e.chart.data.labels,s=i?i.length:0;if(this&&this.options&&"dataset"===this.options.mode)return e.dataset.label||"";if(e.label)return e.label;if(s>0&&e.dataIndex{const e={before:[],lines:[],after:[]},n=Fa(i,t);Ca(e.before,Oa(Ba(n,"beforeLabel",this,t))),Ca(e.lines,Ba(n,"label",this,t)),Ca(e.after,Oa(Ba(n,"afterLabel",this,t))),s.push(e)})),s}getAfterBody(t,e){return za(Ba(e.callbacks,"afterBody",this,t))}getFooter(t,e){const{callbacks:i}=e,s=Ba(i,"beforeFooter",this,t),n=Ba(i,"footer",this,t),o=Ba(i,"afterFooter",this,t);let a=[];return a=Ca(a,Oa(s)),a=Ca(a,Oa(n)),a=Ca(a,Oa(o)),a}_createItems(t){const e=this._active,i=this.chart.data,s=[],n=[],o=[];let a,r,l=[];for(a=0,r=e.length;at.filter(e,s,n,i)))),t.itemSort&&(l=l.sort(((e,s)=>t.itemSort(e,s,i)))),u(l,(e=>{const i=Fa(t.callbacks,e);s.push(Ba(i,"labelColor",this,e)),n.push(Ba(i,"labelPointStyle",this,e)),o.push(Ba(i,"labelTextColor",this,e))})),this.labelColors=s,this.labelPointStyles=n,this.labelTextColors=o,this.dataPoints=l,l}update(t,e){const i=this.options.setContext(this.getContext()),s=this._active;let n,o=[];if(s.length){const t=Da[i.position].call(this,s,this._eventPosition);o=this._createItems(i),this.title=this.getTitle(o,i),this.beforeBody=this.getBeforeBody(o,i),this.body=this.getBody(o,i),this.afterBody=this.getAfterBody(o,i),this.footer=this.getFooter(o,i);const e=this._size=Ta(this,i),a=Object.assign({},t,e),r=Ea(this.chart,i,a),l=Ra(i,a,r,this.chart);this.xAlign=r.xAlign,this.yAlign=r.yAlign,n={opacity:1,x:l.x,y:l.y,width:e.width,height:e.height,caretX:t.x,caretY:t.y}}else 0!==this.opacity&&(n={opacity:0});this._tooltipItems=o,this.$context=void 0,n&&this._resolveAnimations().update(this,n),t&&i.external&&i.external.call(this,{chart:this.chart,tooltip:this,replay:e})}drawCaret(t,e,i,s){const n=this.getCaretPosition(t,i,s);e.lineTo(n.x1,n.y1),e.lineTo(n.x2,n.y2),e.lineTo(n.x3,n.y3)}getCaretPosition(t,e,i){const{xAlign:s,yAlign:n}=this,{caretSize:o,cornerRadius:a}=i,{topLeft:r,topRight:l,bottomLeft:h,bottomRight:c}=wi(a),{x:d,y:u}=t,{width:f,height:g}=e;let p,m,x,b,_,y;return"center"===n?(_=u+g/2,"left"===s?(p=d,m=p-o,b=_+o,y=_-o):(p=d+f,m=p+o,b=_-o,y=_+o),x=p):(m="left"===s?d+Math.max(r,h)+o:"right"===s?d+f-Math.max(l,c)-o:this.caretX,"top"===n?(b=u,_=b-o,p=m-o,x=m+o):(b=u+g,_=b+o,p=m+o,x=m-o),y=b),{x1:p,x2:m,x3:x,y1:b,y2:_,y3:y}}drawTitle(t,e,i){const s=this.title,n=s.length;let o,a,r;if(n){const l=Oi(i.rtl,this.x,this.width);for(t.x=Ia(this,i.titleAlign,i),e.textAlign=l.textAlign(i.titleAlign),e.textBaseline="middle",o=Si(i.titleFont),a=i.titleSpacing,e.fillStyle=i.titleColor,e.font=o.string,r=0;r0!==t))?(t.beginPath(),t.fillStyle=n.multiKeyBackground,He(t,{x:e,y:g,w:h,h:l,radius:r}),t.fill(),t.stroke(),t.fillStyle=a.backgroundColor,t.beginPath(),He(t,{x:i,y:g+1,w:h-2,h:l-2,radius:r}),t.fill()):(t.fillStyle=n.multiKeyBackground,t.fillRect(e,g,h,l),t.strokeRect(e,g,h,l),t.fillStyle=a.backgroundColor,t.fillRect(i,g+1,h-2,l-2))}t.fillStyle=this.labelTextColors[i]}drawBody(t,e,i){const{body:s}=this,{bodySpacing:n,bodyAlign:o,displayColors:a,boxHeight:r,boxWidth:l,boxPadding:h}=i,c=Si(i.bodyFont);let d=c.lineHeight,f=0;const g=Oi(i.rtl,this.x,this.width),p=function(i){e.fillText(i,g.x(t.x+f),t.y+d/2),t.y+=d+n},m=g.textAlign(o);let x,b,_,y,v,M,w;for(e.textAlign=o,e.textBaseline="middle",e.font=c.string,t.x=Ia(this,m,i),e.fillStyle=i.bodyColor,u(this.beforeBody,p),f=a&&"right"!==m?"center"===o?l/2+h:l+2+h:0,y=0,M=s.length;y0&&e.stroke()}_updateAnimationTarget(t){const e=this.chart,i=this.$animations,s=i&&i.x,n=i&&i.y;if(s||n){const i=Da[t.position].call(this,this._active,this._eventPosition);if(!i)return;const o=this._size=Ta(this,t),a=Object.assign({},i,this._size),r=Ea(e,t,a),l=Ra(t,a,r,e);s._to===l.x&&n._to===l.y||(this.xAlign=r.xAlign,this.yAlign=r.yAlign,this.width=o.width,this.height=o.height,this.caretX=i.x,this.caretY=i.y,this._resolveAnimations().update(this,l))}}_willRender(){return!!this.opacity}draw(t){const e=this.options.setContext(this.getContext());let i=this.opacity;if(!i)return;this._updateAnimationTarget(e);const s={width:this.width,height:this.height},n={x:this.x,y:this.y};i=Math.abs(i)<.001?0:i;const o=ki(e.padding),a=this.title.length||this.beforeBody.length||this.body.length||this.afterBody.length||this.footer.length;e.enabled&&a&&(t.save(),t.globalAlpha=i,this.drawBackground(n,t,s,e),Ai(t,e.textDirection),n.y+=o.top,this.drawTitle(n,t,e),this.drawBody(n,t,e),this.drawFooter(n,t,e),Ti(t,e.textDirection),t.restore())}getActiveElements(){return this._active||[]}setActiveElements(t,e){const i=this._active,s=t.map((({datasetIndex:t,index:e})=>{const i=this.chart.getDatasetMeta(t);if(!i)throw new Error("Cannot find a dataset at index "+t);return{datasetIndex:t,element:i.data[e],index:e}})),n=!f(i,s),o=this._positionChanged(s,e);(n||o)&&(this._active=s,this._eventPosition=e,this._ignoreReplayEvents=!0,this.update(!0))}handleEvent(t,e,i=!0){if(e&&this._ignoreReplayEvents)return!1;this._ignoreReplayEvents=!1;const s=this.options,n=this._active||[],o=this._getActiveElements(t,n,e,i),a=this._positionChanged(o,t),r=e||!f(o,n)||a;return r&&(this._active=o,(s.enabled||s.external)&&(this._eventPosition={x:t.x,y:t.y},this.update(!0,e))),r}_getActiveElements(t,e,i,s){const n=this.options;if("mouseout"===t.type)return[];if(!s)return e.filter((t=>this.chart.data.datasets[t.datasetIndex]&&void 0!==this.chart.getDatasetMeta(t.datasetIndex).controller.getParsed(t.index)));const o=this.chart.getElementsAtEventForMode(t,n.mode,n,i);return n.reverse&&o.reverse(),o}_positionChanged(t,e){const{caretX:i,caretY:s,options:n}=this,o=Da[n.position].call(this,t,e);return!1!==o&&(i!==o.x||s!==o.y)}}var Na={id:"tooltip",_element:Wa,positioners:Da,afterInit(t,e,i){i&&(t.tooltip=new Wa({chart:t,options:i}))},beforeUpdate(t,e,i){t.tooltip&&t.tooltip.initialize(i)},reset(t,e,i){t.tooltip&&t.tooltip.initialize(i)},afterDraw(t){const e=t.tooltip;if(e&&e._willRender()){const i={tooltip:e};if(!1===t.notifyPlugins("beforeTooltipDraw",{...i,cancelable:!0}))return;e.draw(t.ctx),t.notifyPlugins("afterTooltipDraw",i)}},afterEvent(t,e){if(t.tooltip){const i=e.replay;t.tooltip.handleEvent(e.event,i,e.inChartArea)&&(e.changed=!0)}},defaults:{enabled:!0,external:null,position:"average",backgroundColor:"rgba(0,0,0,0.8)",titleColor:"#fff",titleFont:{weight:"bold"},titleSpacing:2,titleMarginBottom:6,titleAlign:"left",bodyColor:"#fff",bodySpacing:2,bodyFont:{},bodyAlign:"left",footerColor:"#fff",footerSpacing:2,footerMarginTop:6,footerFont:{weight:"bold"},footerAlign:"left",padding:6,caretPadding:2,caretSize:5,cornerRadius:6,boxHeight:(t,e)=>e.bodyFont.size,boxWidth:(t,e)=>e.bodyFont.size,multiKeyBackground:"#fff",displayColors:!0,boxPadding:0,borderColor:"rgba(0,0,0,0)",borderWidth:0,animation:{duration:400,easing:"easeOutQuart"},animations:{numbers:{type:"number",properties:["x","y","width","height","caretX","caretY"]},opacity:{easing:"linear",duration:200}},callbacks:Va},defaultRoutes:{bodyFont:"font",footerFont:"font",titleFont:"font"},descriptors:{_scriptable:t=>"filter"!==t&&"itemSort"!==t&&"external"!==t,_indexable:!1,callbacks:{_scriptable:!1,_indexable:!1},animation:{_fallback:!1},animations:{_fallback:"animation"}},additionalOptionScopes:["interaction"]};return Tn.register(Un,$o,go,t),Tn.helpers={...Hi},Tn._adapters=In,Tn.Animation=As,Tn.Animations=Ts,Tn.animator=bt,Tn.controllers=nn.controllers.items,Tn.DatasetController=js,Tn.Element=$s,Tn.elements=go,Tn.Interaction=Ki,Tn.layouts=ls,Tn.platforms=Ds,Tn.Scale=tn,Tn.Ticks=ae,Object.assign(Tn,Un,$o,go,t,Ds),Tn.Chart=Tn,"undefined"!=typeof window&&(window.Chart=Tn),Tn})); +//# sourceMappingURL=chart.umd.js.map diff --git a/public/assets/js/custom-select.js b/public/assets/js/custom-select.js new file mode 100644 index 0000000..0ae194e --- /dev/null +++ b/public/assets/js/custom-select.js @@ -0,0 +1,261 @@ +class CustomSelect { + static instances = []; + + constructor(selectElement) { + if (selectElement.dataset.customSelectInitialized === 'true') { + return; + } + selectElement.dataset.customSelectInitialized = 'true'; + + this.originalSelect = selectElement; + this.originalSelect.style.display = 'none'; + this.options = Array.from(this.originalSelect.options); + + // Settings + this.wrapper = document.createElement('div'); + + // Standard classes + let wrapperClasses = 'custom-select-wrapper relative active-select'; + + // Intelligent Width: + // If original select expects full width, wrapper must be full width. + // Otherwise, use w-fit (Crucial for Right-Alignment in toolbars to work). + const widthClass = Array.from(this.originalSelect.classList).find(c => c.startsWith('w-') && c !== 'w-full'); + const isFullWidth = this.originalSelect.classList.contains('w-full') || + this.originalSelect.classList.contains('form-control') || + this.originalSelect.classList.contains('form-input'); + + if (widthClass) { + wrapperClasses += ' ' + widthClass; + } else if (isFullWidth) { + wrapperClasses += ' w-full'; + } else { + wrapperClasses += ' w-fit'; + } + + this.wrapper.className = wrapperClasses; + + this.init(); + + // Store instance + if (!CustomSelect.instances) CustomSelect.instances = []; + CustomSelect.instances.push(this); + } + + init() { + // Create Trigger + this.trigger = document.createElement('div'); + + const isFilter = this.originalSelect.classList.contains('form-filter'); + const baseClass = isFilter ? 'form-filter' : 'form-input'; + + this.trigger.className = `${baseClass} flex items-center justify-between cursor-pointer pr-3`; + this.trigger.style.paddingLeft = '0.75rem'; + + this.trigger.innerHTML = ` + ${this.originalSelect.options[this.originalSelect.selectedIndex].text} +
+ +
+ `; + + // Inherit classes from original select (excluding custom-select marker) + if (this.originalSelect.classList.length > 0) { + const inheritedClasses = Array.from(this.originalSelect.classList) + .filter(c => c !== 'custom-select' && c !== 'hidden') + .join(' '); + if (inheritedClasses) { + this.trigger.className += ' ' + inheritedClasses; + } + } + + // Final sanity check for full width + if (this.wrapper.classList.contains('w-full')) { + this.trigger.classList.add('w-full'); + } + + // Create Options Menu Wrapper (No Scroll Here) + this.menu = document.createElement('div'); + // Create Options Menu Wrapper (No Scroll Here) + // Create Options Menu Wrapper (No Scroll Here) + this.menu = document.createElement('div'); + this.menu.className = 'custom-select-dropdown'; + + // Create Scrollable List Container + this.listContainer = document.createElement('div'); + this.listContainer.className = 'overflow-y-auto flex-1 py-1 custom-scrollbar'; + + // Search Functionality + if (this.originalSelect.dataset.search === 'true') { + const searchContainer = document.createElement('div'); + searchContainer.className = 'p-2 bg-background z-10 border-b border-accents-2 flex-shrink-0 rounded-t-md'; + + this.searchInput = document.createElement('input'); + this.searchInput.type = 'text'; + this.searchInput.className = 'w-full px-2 py-1 text-sm bg-accents-1 border border-accents-2 rounded focus:outline-none focus:ring-1 focus:ring-foreground'; + this.searchInput.placeholder = 'Search...'; + + searchContainer.appendChild(this.searchInput); + this.menu.appendChild(searchContainer); + + // Search Event + this.searchInput.addEventListener('input', (e) => { + const term = e.target.value.toLowerCase(); + this.options.forEach((option, index) => { + const item = this.listContainer.querySelector(`[data-index="${index}"]`); + if (item) { + const text = option.text.toLowerCase(); + item.style.display = text.includes(term) ? 'flex' : 'none'; + } + }); + }); + + this.searchInput.addEventListener('click', (e) => e.stopPropagation()); + } + + // Build Options + this.options.forEach((option, index) => { + const item = document.createElement('div'); + item.className = 'px-3 py-2 text-sm cursor-pointer hover:bg-accents-1 transition-colors flex items-center justify-between whitespace-nowrap'; + if(option.selected) item.classList.add('bg-accents-1', 'font-medium'); + + item.textContent = option.text; + item.dataset.value = option.value; + item.dataset.index = index; + + item.addEventListener('click', () => { + this.select(index); + }); + + this.listContainer.appendChild(item); + }); + + // Append List to Menu + this.menu.appendChild(this.listContainer); + + // Append to wrapper + this.wrapper.appendChild(this.trigger); + this.wrapper.appendChild(this.menu); + this.originalSelect.parentNode.insertBefore(this.wrapper, this.originalSelect); + + // Event Listeners + this.trigger.addEventListener('click', (e) => { + e.stopPropagation(); + this.toggle(); + }); + + document.addEventListener('click', (e) => { + if (!this.wrapper.contains(e.target)) { + this.close(); + } + }); + + if (typeof lucide !== 'undefined') { + lucide.createIcons({ root: this.trigger }); + } + } + + toggle() { + if (!this.menu.classList.contains('open')) { + this.open(); + } else { + this.close(); + } + } + + open() { + CustomSelect.instances.forEach(instance => { + if (instance !== this) instance.close(); + }); + + // Smart Positioning + const rect = this.wrapper.getBoundingClientRect(); + const spaceRight = window.innerWidth - rect.left; + + // Reset positioning classes + this.menu.classList.remove('right-0', 'origin-top-right', 'left-0', 'origin-top-left'); + + // Logic: Zone Check - If near right edge (< 300px), Force Right Align. + // Doing this purely based on coordinates prevents "Layout Jumping" caused by measuring content width. + if (spaceRight < 300) { + this.menu.classList.add('right-0', 'origin-top-right'); + } else { + this.menu.classList.add('left-0', 'origin-top-left'); + } + + // Apply visual open states + this.menu.classList.add('open'); + + this.trigger.classList.add('ring-1', 'ring-foreground'); + const icon = this.trigger.querySelector('.custom-select-icon'); + if(icon) icon.classList.add('rotate-180'); + + if (this.searchInput) { + setTimeout(() => this.searchInput.focus(), 50); + } + } + + close() { + this.menu.classList.remove('open'); + + this.trigger.classList.remove('ring-1', 'ring-foreground'); + const icon = this.trigger.querySelector('.custom-select-icon'); + if(icon) icon.classList.remove('rotate-180'); + } + + select(index) { + // Update Original Select + this.originalSelect.selectedIndex = index; + + // Update UI + this.trigger.querySelector('.custom-select-value').textContent = this.options[index].text; + + // Update Active State in List + Array.from(this.listContainer.children).forEach((child) => { + // Safe check + if (!child.dataset.index) return; + + if (parseInt(child.dataset.index) === index) { + child.classList.add('bg-accents-1', 'font-medium'); + } else { + child.classList.remove('bg-accents-1', 'font-medium'); + } + }); + + this.close(); + this.originalSelect.dispatchEvent(new Event('change')); + } + + refresh() { + // Clear list items + this.listContainer.innerHTML = ''; + + // Re-read options + this.options = Array.from(this.originalSelect.options); + + this.options.forEach((option, index) => { + const item = document.createElement('div'); + item.className = 'px-3 py-2 text-sm cursor-pointer hover:bg-accents-1 transition-colors flex items-center justify-between whitespace-nowrap'; + if(option.selected) item.classList.add('bg-accents-1', 'font-medium'); + + item.textContent = option.text; + item.dataset.value = option.value; + item.dataset.index = index; + + item.addEventListener('click', () => { + this.select(index); + }); + + this.listContainer.appendChild(item); + }); + + // Update Trigger + if (this.originalSelect.selectedIndex >= 0) { + this.trigger.querySelector('.custom-select-value').textContent = this.originalSelect.options[this.originalSelect.selectedIndex].text; + } + } +} + +document.addEventListener('DOMContentLoaded', () => { + document.querySelectorAll('select.custom-select').forEach(el => new CustomSelect(el)); +}); diff --git a/public/assets/js/datatable.js b/public/assets/js/datatable.js new file mode 100644 index 0000000..dabb6c6 --- /dev/null +++ b/public/assets/js/datatable.js @@ -0,0 +1,336 @@ +class SimpleDataTable { + constructor(tableSelector, options = {}) { + this.table = document.querySelector(tableSelector); + if (!this.table) return; + + this.tbody = this.table.querySelector('tbody'); + this.rows = Array.from(this.tbody.querySelectorAll('tr')); + this.originalRows = [...this.rows]; // Keep copy + + this.options = { + itemsPerPage: 10, + searchable: true, + pagination: true, + filters: [], // Array of { index: number, label: string } + ...options + }; + + this.currentPage = 1; + this.searchQuery = ''; + this.activeFilters = {}; // { columnIndex: value } + this.filteredRows = [...this.originalRows]; + + // Wait for translations to load if i18n is used + if (window.i18n && window.i18n.ready) { + window.i18n.ready.then(() => this.init()); + } else { + this.init(); + } + + // Listen for language change + window.addEventListener('languageChanged', () => { + this.reTranslate(); + this.render(); + }); + } + + reTranslate() { + // Update perPage label + const labels = this.wrapper.querySelectorAll('span.text-accents-5'); + labels.forEach(label => { + if (label.textContent.includes('entries per page') || (window.i18n && label.textContent === window.i18n.t('common.table.entries_per_page'))) { + label.textContent = window.i18n ? window.i18n.t('common.table.entries_per_page') : 'entries per page'; + } + }); + + // Update search placeholder + const searchInput = this.wrapper.querySelector('input[type="text"]'); + if (searchInput) { + searchInput.placeholder = window.i18n ? window.i18n.t('common.table.search_placeholder') : 'Search...'; + } + + // Update All option + const perPageSelect = this.wrapper.querySelector('select'); + if (perPageSelect) { + const allOption = Array.from(perPageSelect.options).find(opt => opt.value === "-1"); + if (allOption) { + allOption.text = window.i18n ? window.i18n.t('common.table.all') : 'All'; + } + } + } + + init() { + // Create Wrapper + this.wrapper = document.createElement('div'); + this.wrapper.className = 'datatable-wrapper space-y-4'; + this.table.parentNode.insertBefore(this.wrapper, this.table); + + // Create Controls Header + const header = document.createElement('div'); + header.className = 'flex flex-col sm:flex-row justify-between items-center gap-4 mb-4'; + + // Show Entries Wrapper + const controlsLeft = document.createElement('div'); + controlsLeft.className = 'flex items-center gap-3 w-full sm:w-auto flex-wrap'; + + const perPageSelect = document.createElement('select'); + perPageSelect.className = 'form-filter w-20'; + + [5, 10, 25, 50, 100].forEach(num => { + const option = document.createElement('option'); + option.value = num; + option.text = num; + if (num === this.options.itemsPerPage) option.selected = true; + perPageSelect.appendChild(option); + }); + + // All option + const allOption = document.createElement('option'); + allOption.value = -1; + allOption.text = window.i18n ? window.i18n.t('common.table.all') : 'All'; + perPageSelect.appendChild(allOption); + + perPageSelect.addEventListener('change', (e) => { + const val = parseInt(e.target.value); + this.options.itemsPerPage = val === -1 ? this.originalRows.length : val; + this.currentPage = 1; + this.render(); + }); + + // Label + const label = document.createElement('span'); + label.className = 'text-sm text-accents-5 whitespace-nowrap'; + label.textContent = window.i18n ? window.i18n.t('common.table.entries_per_page') : 'entries per page'; + + controlsLeft.appendChild(perPageSelect); + controlsLeft.appendChild(label); + + // Initialize Filters if provided + if (this.options.filters && this.options.filters.length > 0) { + this.options.filters.forEach(filterConfig => { + this.initFilter(filterConfig, controlsLeft); // Append to Left Controls + }); + } + + header.appendChild(controlsLeft); + + // Initialize CustomSelect if available (for perPage) + if (typeof CustomSelect !== 'undefined') { + new CustomSelect(perPageSelect); + } + + // Search Input + if (this.options.searchable) { + const searchWrapper = document.createElement('div'); + searchWrapper.className = 'input-group sm:w-64 z-10'; + const placeholder = window.i18n ? window.i18n.t('common.table.search_placeholder') : 'Search...'; + searchWrapper.innerHTML = ` +
+ +
+ + `; + const input = searchWrapper.querySelector('input'); + input.addEventListener('input', (e) => this.handleSearch(e.target.value)); + header.appendChild(searchWrapper); + } + + this.wrapper.appendChild(header); + + // Move Table into Wrapper + // Move Table into Wrapper + this.tableWrapper = document.createElement('div'); + this.tableWrapper.className = 'rounded-md border border-accents-2 overflow-x-auto bg-white/30 dark:bg-black/30 backdrop-blur-sm'; // overflow-x-auto for responsiveness + this.tableWrapper.appendChild(this.table); + this.wrapper.appendChild(this.tableWrapper); + + // Render Icons for Header Controls + if (typeof lucide !== 'undefined') { + lucide.createIcons({ + root: header + }); + } + + // Pagination Controls + if (this.options.pagination) { + this.paginationContainer = document.createElement('div'); + this.paginationContainer.className = 'flex items-center justify-between px-2'; + this.wrapper.appendChild(this.paginationContainer); + } + + this.render(); + } + + initFilter(config, container) { + // config = { index: number, label: string } + const colIndex = config.index; + + // Get unique values + const values = new Set(); + this.originalRows.forEach(row => { + const cell = row.cells[colIndex]; + if (cell) { + const text = cell.textContent.trim(); + // Basic cleanup: remove extra whitespace + if(text && text !== '-' && text !== '') values.add(text); + } + }); + + // Create Select + const select = document.createElement('select'); + select.className = 'form-filter datatable-select'; // Use a different class to avoid auto-init by custom-select.js + + // Default Option + const defaultOption = document.createElement('option'); + defaultOption.value = ''; + defaultOption.text = config.label; + select.appendChild(defaultOption); + + Array.from(values).sort().forEach(val => { + const opt = document.createElement('option'); + opt.value = val; + opt.text = val; + select.appendChild(opt); + }); + + // Event Listener + select.addEventListener('change', (e) => { + const val = e.target.value; + if (val === '') { + delete this.activeFilters[colIndex]; + } else { + this.activeFilters[colIndex] = val; + } + this.currentPage = 1; + this.filterRows(); + this.render(); + }); + + container.appendChild(select); + + if (typeof CustomSelect !== 'undefined') { + new CustomSelect(select); + } + } + + handleSearch(query) { + this.searchQuery = query.toLowerCase(); + this.currentPage = 1; + this.filterRows(); + this.render(); + } + + filterRows() { + this.filteredRows = this.originalRows.filter(row => { + // 1. Text Search + let matchesSearch = true; + if (this.searchQuery) { + const text = row.textContent.toLowerCase(); + matchesSearch = text.includes(this.searchQuery); + } + + // 2. Column Filters + let matchesFilters = true; + for (const [colIndex, filterValue] of Object.entries(this.activeFilters)) { + const cell = row.cells[colIndex]; + if (!cell) { + matchesFilters = false; + break; + } + // Exact match (trimmed) + if (cell.textContent.trim() !== filterValue) { + matchesFilters = false; + break; + } + } + + return matchesSearch && matchesFilters; + }); + } + + render() { + // Calculate pagination + const totalItems = this.filteredRows.length; + const totalPages = Math.ceil(totalItems / this.options.itemsPerPage); + + // Ensure current page is valid + if (this.currentPage > totalPages) this.currentPage = totalPages || 1; + if (this.currentPage < 1) this.currentPage = 1; + + const start = (this.currentPage - 1) * this.options.itemsPerPage; + const end = start + this.options.itemsPerPage; + const currentItems = this.filteredRows.slice(start, end); + + // Clear and Re-append rows + this.tbody.innerHTML = ''; + if (currentItems.length > 0) { + currentItems.forEach(row => this.tbody.appendChild(row)); + } else { + // Empty State + const emptyRow = document.createElement('tr'); + const noMatchText = window.i18n ? window.i18n.t('common.table.no_match') : 'No match found.'; + emptyRow.innerHTML = ` + + ${noMatchText} + + `; + this.tbody.appendChild(emptyRow); + } + + // Render Pagination + if (this.options.pagination) { + this.renderPagination(totalItems, totalPages, start + 1, Math.min(end, totalItems)); + } + + // Re-initialize icons if Lucide is available + if (typeof lucide !== 'undefined') { + lucide.createIcons(); + } + } + + renderPagination(totalItems, totalPages, start, end) { + if (totalItems === 0) { + this.paginationContainer.innerHTML = ''; + return; + } + + const showingText = window.i18n ? window.i18n.t('common.table.showing', {start, end, total: totalItems}) : `Showing ${start} to ${end} of ${totalItems}`; + const previousText = window.i18n ? window.i18n.t('common.previous') : 'Previous'; + const nextText = window.i18n ? window.i18n.t('common.next') : 'Next'; + const pageText = window.i18n ? window.i18n.t('common.page_of', {current: this.currentPage, total: totalPages}) : `Page ${this.currentPage} of ${totalPages}`; + + this.paginationContainer.innerHTML = ` +
+ ${showingText} +
+
+ +
${pageText}
+ +
+ `; + + this.paginationContainer.querySelector('.btn-prev').addEventListener('click', () => { + if (this.currentPage > 1) { + this.currentPage--; + this.render(); + } + }); + + this.paginationContainer.querySelector('.btn-next').addEventListener('click', () => { + if (this.currentPage < totalPages) { + this.currentPage++; + this.render(); + } + }); + } +} + +// Export if using modules, otherwise it's global +if (typeof module !== 'undefined' && module.exports) { + module.exports = SimpleDataTable; +} diff --git a/public/assets/js/i18n.js b/public/assets/js/i18n.js new file mode 100644 index 0000000..0f11a1f --- /dev/null +++ b/public/assets/js/i18n.js @@ -0,0 +1,93 @@ +class I18n { + constructor() { + this.currentLang = localStorage.getItem('mivo_lang') || 'en'; + this.translations = {}; + this.isLoaded = false; + // The ready promise resolves after the first language load + this.ready = this.init(); + } + + async init() { + await this.loadLanguage(this.currentLang); + this.isLoaded = true; + } + + async loadLanguage(lang) { + try { + // Add cache busting to ensure fresh translation files + const cacheBuster = Date.now(); + const response = await fetch(`/lang/${lang}.json?v=${cacheBuster}`); + if (!response.ok) throw new Error(`Failed to load language: ${lang}`); + + this.translations = await response.json(); + this.currentLang = lang; + localStorage.setItem('mivo_lang', lang); + this.applyTranslations(); + + // Dispatch event for other components + window.dispatchEvent(new CustomEvent('languageChanged', { detail: { lang } })); + + // Update html lang attribute + document.documentElement.lang = lang; + } catch (error) { + console.error('I18n Error:', error); + } + } + + applyTranslations() { + document.querySelectorAll('[data-i18n]').forEach(element => { + const key = element.getAttribute('data-i18n'); + const translation = this.getNestedValue(this.translations, key); + + if (translation) { + if (element.tagName === 'INPUT' && element.getAttribute('placeholder')) { + element.placeholder = translation; + } else { + // Check if element has child nodes that are not text (e.g. icons) + // If simple text, just replace + // If complex, try to preserve icon? + // For now, let's assume strictly text replacement or user wraps text in span + // Better approach: Look for a text node? + // Simplest for now: innerText + element.textContent = translation; + } + } else { + // Log missing translation for developers (only if fully loaded) + if (this.isLoaded) { + console.warn(`[i18n] Missing translation for key: "${key}" (lang: ${this.currentLang})`); + } + } + }); + } + + getNestedValue(obj, path) { + return path.split('.').reduce((acc, part) => acc && acc[part], obj); + } + + t(key, params = {}) { + let text = this.getNestedValue(this.translations, key); + + if (!text) { + if (this.isLoaded) { + console.warn(`[i18n] Missing translation for key: "${key}" (lang: ${this.currentLang})`); + } + text = key; // Fallback to key + } + + // Simple interpolation: {key} + if (params) { + Object.keys(params).forEach(param => { + text = text.replace(new RegExp(`{${param}}`, 'g'), params[param]); + }); + } + return text; + } +} + +// Initialize +window.i18n = new I18n(); + +// Global helper +function changeLanguage(lang) { + window.i18n.loadLanguage(lang); +} diff --git a/public/assets/js/jquery.min.js b/public/assets/js/jquery.min.js new file mode 100644 index 0000000..4d9b3a2 --- /dev/null +++ b/public/assets/js/jquery.min.js @@ -0,0 +1,2 @@ +/*! jQuery v3.3.1 | (c) JS Foundation and other contributors | jquery.org/license */ +!function(e,t){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=e.document?t(e,!0):function(e){if(!e.document)throw new Error("jQuery requires a window with a document");return t(e)}:t(e)}("undefined"!=typeof window?window:this,function(e,t){"use strict";var n=[],r=e.document,i=Object.getPrototypeOf,o=n.slice,a=n.concat,s=n.push,u=n.indexOf,l={},c=l.toString,f=l.hasOwnProperty,p=f.toString,d=p.call(Object),h={},g=function e(t){return"function"==typeof t&&"number"!=typeof t.nodeType},y=function e(t){return null!=t&&t===t.window},v={type:!0,src:!0,noModule:!0};function m(e,t,n){var i,o=(t=t||r).createElement("script");if(o.text=e,n)for(i in v)n[i]&&(o[i]=n[i]);t.head.appendChild(o).parentNode.removeChild(o)}function x(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?l[c.call(e)]||"object":typeof e}var b="3.3.1",w=function(e,t){return new w.fn.init(e,t)},T=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g;w.fn=w.prototype={jquery:"3.3.1",constructor:w,length:0,toArray:function(){return o.call(this)},get:function(e){return null==e?o.call(this):e<0?this[e+this.length]:this[e]},pushStack:function(e){var t=w.merge(this.constructor(),e);return t.prevObject=this,t},each:function(e){return w.each(this,e)},map:function(e){return this.pushStack(w.map(this,function(t,n){return e.call(t,n,t)}))},slice:function(){return this.pushStack(o.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(e){var t=this.length,n=+e+(e<0?t:0);return this.pushStack(n>=0&&n0&&t-1 in e)}var E=function(e){var t,n,r,i,o,a,s,u,l,c,f,p,d,h,g,y,v,m,x,b="sizzle"+1*new Date,w=e.document,T=0,C=0,E=ae(),k=ae(),S=ae(),D=function(e,t){return e===t&&(f=!0),0},N={}.hasOwnProperty,A=[],j=A.pop,q=A.push,L=A.push,H=A.slice,O=function(e,t){for(var n=0,r=e.length;n+~]|"+M+")"+M+"*"),z=new RegExp("="+M+"*([^\\]'\"]*?)"+M+"*\\]","g"),X=new RegExp(W),U=new RegExp("^"+R+"$"),V={ID:new RegExp("^#("+R+")"),CLASS:new RegExp("^\\.("+R+")"),TAG:new RegExp("^("+R+"|[*])"),ATTR:new RegExp("^"+I),PSEUDO:new RegExp("^"+W),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+M+"*(even|odd|(([+-]|)(\\d*)n|)"+M+"*(?:([+-]|)"+M+"*(\\d+)|))"+M+"*\\)|)","i"),bool:new RegExp("^(?:"+P+")$","i"),needsContext:new RegExp("^"+M+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+M+"*((?:-\\d)?\\d*)"+M+"*\\)|)(?=[^-]|$)","i")},G=/^(?:input|select|textarea|button)$/i,Y=/^h\d$/i,Q=/^[^{]+\{\s*\[native \w/,J=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,K=/[+~]/,Z=new RegExp("\\\\([\\da-f]{1,6}"+M+"?|("+M+")|.)","ig"),ee=function(e,t,n){var r="0x"+t-65536;return r!==r||n?t:r<0?String.fromCharCode(r+65536):String.fromCharCode(r>>10|55296,1023&r|56320)},te=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,ne=function(e,t){return t?"\0"===e?"\ufffd":e.slice(0,-1)+"\\"+e.charCodeAt(e.length-1).toString(16)+" ":"\\"+e},re=function(){p()},ie=me(function(e){return!0===e.disabled&&("form"in e||"label"in e)},{dir:"parentNode",next:"legend"});try{L.apply(A=H.call(w.childNodes),w.childNodes),A[w.childNodes.length].nodeType}catch(e){L={apply:A.length?function(e,t){q.apply(e,H.call(t))}:function(e,t){var n=e.length,r=0;while(e[n++]=t[r++]);e.length=n-1}}}function oe(e,t,r,i){var o,s,l,c,f,h,v,m=t&&t.ownerDocument,T=t?t.nodeType:9;if(r=r||[],"string"!=typeof e||!e||1!==T&&9!==T&&11!==T)return r;if(!i&&((t?t.ownerDocument||t:w)!==d&&p(t),t=t||d,g)){if(11!==T&&(f=J.exec(e)))if(o=f[1]){if(9===T){if(!(l=t.getElementById(o)))return r;if(l.id===o)return r.push(l),r}else if(m&&(l=m.getElementById(o))&&x(t,l)&&l.id===o)return r.push(l),r}else{if(f[2])return L.apply(r,t.getElementsByTagName(e)),r;if((o=f[3])&&n.getElementsByClassName&&t.getElementsByClassName)return L.apply(r,t.getElementsByClassName(o)),r}if(n.qsa&&!S[e+" "]&&(!y||!y.test(e))){if(1!==T)m=t,v=e;else if("object"!==t.nodeName.toLowerCase()){(c=t.getAttribute("id"))?c=c.replace(te,ne):t.setAttribute("id",c=b),s=(h=a(e)).length;while(s--)h[s]="#"+c+" "+ve(h[s]);v=h.join(","),m=K.test(e)&&ge(t.parentNode)||t}if(v)try{return L.apply(r,m.querySelectorAll(v)),r}catch(e){}finally{c===b&&t.removeAttribute("id")}}}return u(e.replace(B,"$1"),t,r,i)}function ae(){var e=[];function t(n,i){return e.push(n+" ")>r.cacheLength&&delete t[e.shift()],t[n+" "]=i}return t}function se(e){return e[b]=!0,e}function ue(e){var t=d.createElement("fieldset");try{return!!e(t)}catch(e){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function le(e,t){var n=e.split("|"),i=n.length;while(i--)r.attrHandle[n[i]]=t}function ce(e,t){var n=t&&e,r=n&&1===e.nodeType&&1===t.nodeType&&e.sourceIndex-t.sourceIndex;if(r)return r;if(n)while(n=n.nextSibling)if(n===t)return-1;return e?1:-1}function fe(e){return function(t){return"input"===t.nodeName.toLowerCase()&&t.type===e}}function pe(e){return function(t){var n=t.nodeName.toLowerCase();return("input"===n||"button"===n)&&t.type===e}}function de(e){return function(t){return"form"in t?t.parentNode&&!1===t.disabled?"label"in t?"label"in t.parentNode?t.parentNode.disabled===e:t.disabled===e:t.isDisabled===e||t.isDisabled!==!e&&ie(t)===e:t.disabled===e:"label"in t&&t.disabled===e}}function he(e){return se(function(t){return t=+t,se(function(n,r){var i,o=e([],n.length,t),a=o.length;while(a--)n[i=o[a]]&&(n[i]=!(r[i]=n[i]))})})}function ge(e){return e&&"undefined"!=typeof e.getElementsByTagName&&e}n=oe.support={},o=oe.isXML=function(e){var t=e&&(e.ownerDocument||e).documentElement;return!!t&&"HTML"!==t.nodeName},p=oe.setDocument=function(e){var t,i,a=e?e.ownerDocument||e:w;return a!==d&&9===a.nodeType&&a.documentElement?(d=a,h=d.documentElement,g=!o(d),w!==d&&(i=d.defaultView)&&i.top!==i&&(i.addEventListener?i.addEventListener("unload",re,!1):i.attachEvent&&i.attachEvent("onunload",re)),n.attributes=ue(function(e){return e.className="i",!e.getAttribute("className")}),n.getElementsByTagName=ue(function(e){return e.appendChild(d.createComment("")),!e.getElementsByTagName("*").length}),n.getElementsByClassName=Q.test(d.getElementsByClassName),n.getById=ue(function(e){return h.appendChild(e).id=b,!d.getElementsByName||!d.getElementsByName(b).length}),n.getById?(r.filter.ID=function(e){var t=e.replace(Z,ee);return function(e){return e.getAttribute("id")===t}},r.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&g){var n=t.getElementById(e);return n?[n]:[]}}):(r.filter.ID=function(e){var t=e.replace(Z,ee);return function(e){var n="undefined"!=typeof e.getAttributeNode&&e.getAttributeNode("id");return n&&n.value===t}},r.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&g){var n,r,i,o=t.getElementById(e);if(o){if((n=o.getAttributeNode("id"))&&n.value===e)return[o];i=t.getElementsByName(e),r=0;while(o=i[r++])if((n=o.getAttributeNode("id"))&&n.value===e)return[o]}return[]}}),r.find.TAG=n.getElementsByTagName?function(e,t){return"undefined"!=typeof t.getElementsByTagName?t.getElementsByTagName(e):n.qsa?t.querySelectorAll(e):void 0}:function(e,t){var n,r=[],i=0,o=t.getElementsByTagName(e);if("*"===e){while(n=o[i++])1===n.nodeType&&r.push(n);return r}return o},r.find.CLASS=n.getElementsByClassName&&function(e,t){if("undefined"!=typeof t.getElementsByClassName&&g)return t.getElementsByClassName(e)},v=[],y=[],(n.qsa=Q.test(d.querySelectorAll))&&(ue(function(e){h.appendChild(e).innerHTML="",e.querySelectorAll("[msallowcapture^='']").length&&y.push("[*^$]="+M+"*(?:''|\"\")"),e.querySelectorAll("[selected]").length||y.push("\\["+M+"*(?:value|"+P+")"),e.querySelectorAll("[id~="+b+"-]").length||y.push("~="),e.querySelectorAll(":checked").length||y.push(":checked"),e.querySelectorAll("a#"+b+"+*").length||y.push(".#.+[+~]")}),ue(function(e){e.innerHTML="";var t=d.createElement("input");t.setAttribute("type","hidden"),e.appendChild(t).setAttribute("name","D"),e.querySelectorAll("[name=d]").length&&y.push("name"+M+"*[*^$|!~]?="),2!==e.querySelectorAll(":enabled").length&&y.push(":enabled",":disabled"),h.appendChild(e).disabled=!0,2!==e.querySelectorAll(":disabled").length&&y.push(":enabled",":disabled"),e.querySelectorAll("*,:x"),y.push(",.*:")})),(n.matchesSelector=Q.test(m=h.matches||h.webkitMatchesSelector||h.mozMatchesSelector||h.oMatchesSelector||h.msMatchesSelector))&&ue(function(e){n.disconnectedMatch=m.call(e,"*"),m.call(e,"[s!='']:x"),v.push("!=",W)}),y=y.length&&new RegExp(y.join("|")),v=v.length&&new RegExp(v.join("|")),t=Q.test(h.compareDocumentPosition),x=t||Q.test(h.contains)?function(e,t){var n=9===e.nodeType?e.documentElement:e,r=t&&t.parentNode;return e===r||!(!r||1!==r.nodeType||!(n.contains?n.contains(r):e.compareDocumentPosition&&16&e.compareDocumentPosition(r)))}:function(e,t){if(t)while(t=t.parentNode)if(t===e)return!0;return!1},D=t?function(e,t){if(e===t)return f=!0,0;var r=!e.compareDocumentPosition-!t.compareDocumentPosition;return r||(1&(r=(e.ownerDocument||e)===(t.ownerDocument||t)?e.compareDocumentPosition(t):1)||!n.sortDetached&&t.compareDocumentPosition(e)===r?e===d||e.ownerDocument===w&&x(w,e)?-1:t===d||t.ownerDocument===w&&x(w,t)?1:c?O(c,e)-O(c,t):0:4&r?-1:1)}:function(e,t){if(e===t)return f=!0,0;var n,r=0,i=e.parentNode,o=t.parentNode,a=[e],s=[t];if(!i||!o)return e===d?-1:t===d?1:i?-1:o?1:c?O(c,e)-O(c,t):0;if(i===o)return ce(e,t);n=e;while(n=n.parentNode)a.unshift(n);n=t;while(n=n.parentNode)s.unshift(n);while(a[r]===s[r])r++;return r?ce(a[r],s[r]):a[r]===w?-1:s[r]===w?1:0},d):d},oe.matches=function(e,t){return oe(e,null,null,t)},oe.matchesSelector=function(e,t){if((e.ownerDocument||e)!==d&&p(e),t=t.replace(z,"='$1']"),n.matchesSelector&&g&&!S[t+" "]&&(!v||!v.test(t))&&(!y||!y.test(t)))try{var r=m.call(e,t);if(r||n.disconnectedMatch||e.document&&11!==e.document.nodeType)return r}catch(e){}return oe(t,d,null,[e]).length>0},oe.contains=function(e,t){return(e.ownerDocument||e)!==d&&p(e),x(e,t)},oe.attr=function(e,t){(e.ownerDocument||e)!==d&&p(e);var i=r.attrHandle[t.toLowerCase()],o=i&&N.call(r.attrHandle,t.toLowerCase())?i(e,t,!g):void 0;return void 0!==o?o:n.attributes||!g?e.getAttribute(t):(o=e.getAttributeNode(t))&&o.specified?o.value:null},oe.escape=function(e){return(e+"").replace(te,ne)},oe.error=function(e){throw new Error("Syntax error, unrecognized expression: "+e)},oe.uniqueSort=function(e){var t,r=[],i=0,o=0;if(f=!n.detectDuplicates,c=!n.sortStable&&e.slice(0),e.sort(D),f){while(t=e[o++])t===e[o]&&(i=r.push(o));while(i--)e.splice(r[i],1)}return c=null,e},i=oe.getText=function(e){var t,n="",r=0,o=e.nodeType;if(o){if(1===o||9===o||11===o){if("string"==typeof e.textContent)return e.textContent;for(e=e.firstChild;e;e=e.nextSibling)n+=i(e)}else if(3===o||4===o)return e.nodeValue}else while(t=e[r++])n+=i(t);return n},(r=oe.selectors={cacheLength:50,createPseudo:se,match:V,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(Z,ee),e[3]=(e[3]||e[4]||e[5]||"").replace(Z,ee),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||oe.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&oe.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return V.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"":n&&X.test(n)&&(t=a(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(Z,ee).toLowerCase();return"*"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=E[e+" "];return t||(t=new RegExp("(^|"+M+")"+e+"("+M+"|$)"))&&E(e,function(e){return t.test("string"==typeof e.className&&e.className||"undefined"!=typeof e.getAttribute&&e.getAttribute("class")||"")})},ATTR:function(e,t,n){return function(r){var i=oe.attr(r,e);return null==i?"!="===t:!t||(i+="","="===t?i===n:"!="===t?i!==n:"^="===t?n&&0===i.indexOf(n):"*="===t?n&&i.indexOf(n)>-1:"$="===t?n&&i.slice(-n.length)===n:"~="===t?(" "+i.replace($," ")+" ").indexOf(n)>-1:"|="===t&&(i===n||i.slice(0,n.length+1)===n+"-"))}},CHILD:function(e,t,n,r,i){var o="nth"!==e.slice(0,3),a="last"!==e.slice(-4),s="of-type"===t;return 1===r&&0===i?function(e){return!!e.parentNode}:function(t,n,u){var l,c,f,p,d,h,g=o!==a?"nextSibling":"previousSibling",y=t.parentNode,v=s&&t.nodeName.toLowerCase(),m=!u&&!s,x=!1;if(y){if(o){while(g){p=t;while(p=p[g])if(s?p.nodeName.toLowerCase()===v:1===p.nodeType)return!1;h=g="only"===e&&!h&&"nextSibling"}return!0}if(h=[a?y.firstChild:y.lastChild],a&&m){x=(d=(l=(c=(f=(p=y)[b]||(p[b]={}))[p.uniqueID]||(f[p.uniqueID]={}))[e]||[])[0]===T&&l[1])&&l[2],p=d&&y.childNodes[d];while(p=++d&&p&&p[g]||(x=d=0)||h.pop())if(1===p.nodeType&&++x&&p===t){c[e]=[T,d,x];break}}else if(m&&(x=d=(l=(c=(f=(p=t)[b]||(p[b]={}))[p.uniqueID]||(f[p.uniqueID]={}))[e]||[])[0]===T&&l[1]),!1===x)while(p=++d&&p&&p[g]||(x=d=0)||h.pop())if((s?p.nodeName.toLowerCase()===v:1===p.nodeType)&&++x&&(m&&((c=(f=p[b]||(p[b]={}))[p.uniqueID]||(f[p.uniqueID]={}))[e]=[T,x]),p===t))break;return(x-=i)===r||x%r==0&&x/r>=0}}},PSEUDO:function(e,t){var n,i=r.pseudos[e]||r.setFilters[e.toLowerCase()]||oe.error("unsupported pseudo: "+e);return i[b]?i(t):i.length>1?(n=[e,e,"",t],r.setFilters.hasOwnProperty(e.toLowerCase())?se(function(e,n){var r,o=i(e,t),a=o.length;while(a--)e[r=O(e,o[a])]=!(n[r]=o[a])}):function(e){return i(e,0,n)}):i}},pseudos:{not:se(function(e){var t=[],n=[],r=s(e.replace(B,"$1"));return r[b]?se(function(e,t,n,i){var o,a=r(e,null,i,[]),s=e.length;while(s--)(o=a[s])&&(e[s]=!(t[s]=o))}):function(e,i,o){return t[0]=e,r(t,null,o,n),t[0]=null,!n.pop()}}),has:se(function(e){return function(t){return oe(e,t).length>0}}),contains:se(function(e){return e=e.replace(Z,ee),function(t){return(t.textContent||t.innerText||i(t)).indexOf(e)>-1}}),lang:se(function(e){return U.test(e||"")||oe.error("unsupported lang: "+e),e=e.replace(Z,ee).toLowerCase(),function(t){var n;do{if(n=g?t.lang:t.getAttribute("xml:lang")||t.getAttribute("lang"))return(n=n.toLowerCase())===e||0===n.indexOf(e+"-")}while((t=t.parentNode)&&1===t.nodeType);return!1}}),target:function(t){var n=e.location&&e.location.hash;return n&&n.slice(1)===t.id},root:function(e){return e===h},focus:function(e){return e===d.activeElement&&(!d.hasFocus||d.hasFocus())&&!!(e.type||e.href||~e.tabIndex)},enabled:de(!1),disabled:de(!0),checked:function(e){var t=e.nodeName.toLowerCase();return"input"===t&&!!e.checked||"option"===t&&!!e.selected},selected:function(e){return e.parentNode&&e.parentNode.selectedIndex,!0===e.selected},empty:function(e){for(e=e.firstChild;e;e=e.nextSibling)if(e.nodeType<6)return!1;return!0},parent:function(e){return!r.pseudos.empty(e)},header:function(e){return Y.test(e.nodeName)},input:function(e){return G.test(e.nodeName)},button:function(e){var t=e.nodeName.toLowerCase();return"input"===t&&"button"===e.type||"button"===t},text:function(e){var t;return"input"===e.nodeName.toLowerCase()&&"text"===e.type&&(null==(t=e.getAttribute("type"))||"text"===t.toLowerCase())},first:he(function(){return[0]}),last:he(function(e,t){return[t-1]}),eq:he(function(e,t,n){return[n<0?n+t:n]}),even:he(function(e,t){for(var n=0;n=0;)e.push(r);return e}),gt:he(function(e,t,n){for(var r=n<0?n+t:n;++r1?function(t,n,r){var i=e.length;while(i--)if(!e[i](t,n,r))return!1;return!0}:e[0]}function be(e,t,n){for(var r=0,i=t.length;r-1&&(o[l]=!(a[l]=f))}}else v=we(v===a?v.splice(h,v.length):v),i?i(null,a,v,u):L.apply(a,v)})}function Ce(e){for(var t,n,i,o=e.length,a=r.relative[e[0].type],s=a||r.relative[" "],u=a?1:0,c=me(function(e){return e===t},s,!0),f=me(function(e){return O(t,e)>-1},s,!0),p=[function(e,n,r){var i=!a&&(r||n!==l)||((t=n).nodeType?c(e,n,r):f(e,n,r));return t=null,i}];u1&&xe(p),u>1&&ve(e.slice(0,u-1).concat({value:" "===e[u-2].type?"*":""})).replace(B,"$1"),n,u0,i=e.length>0,o=function(o,a,s,u,c){var f,h,y,v=0,m="0",x=o&&[],b=[],w=l,C=o||i&&r.find.TAG("*",c),E=T+=null==w?1:Math.random()||.1,k=C.length;for(c&&(l=a===d||a||c);m!==k&&null!=(f=C[m]);m++){if(i&&f){h=0,a||f.ownerDocument===d||(p(f),s=!g);while(y=e[h++])if(y(f,a||d,s)){u.push(f);break}c&&(T=E)}n&&((f=!y&&f)&&v--,o&&x.push(f))}if(v+=m,n&&m!==v){h=0;while(y=t[h++])y(x,b,a,s);if(o){if(v>0)while(m--)x[m]||b[m]||(b[m]=j.call(u));b=we(b)}L.apply(u,b),c&&!o&&b.length>0&&v+t.length>1&&oe.uniqueSort(u)}return c&&(T=E,l=w),x};return n?se(o):o}return s=oe.compile=function(e,t){var n,r=[],i=[],o=S[e+" "];if(!o){t||(t=a(e)),n=t.length;while(n--)(o=Ce(t[n]))[b]?r.push(o):i.push(o);(o=S(e,Ee(i,r))).selector=e}return o},u=oe.select=function(e,t,n,i){var o,u,l,c,f,p="function"==typeof e&&e,d=!i&&a(e=p.selector||e);if(n=n||[],1===d.length){if((u=d[0]=d[0].slice(0)).length>2&&"ID"===(l=u[0]).type&&9===t.nodeType&&g&&r.relative[u[1].type]){if(!(t=(r.find.ID(l.matches[0].replace(Z,ee),t)||[])[0]))return n;p&&(t=t.parentNode),e=e.slice(u.shift().value.length)}o=V.needsContext.test(e)?0:u.length;while(o--){if(l=u[o],r.relative[c=l.type])break;if((f=r.find[c])&&(i=f(l.matches[0].replace(Z,ee),K.test(u[0].type)&&ge(t.parentNode)||t))){if(u.splice(o,1),!(e=i.length&&ve(u)))return L.apply(n,i),n;break}}}return(p||s(e,d))(i,t,!g,n,!t||K.test(e)&&ge(t.parentNode)||t),n},n.sortStable=b.split("").sort(D).join("")===b,n.detectDuplicates=!!f,p(),n.sortDetached=ue(function(e){return 1&e.compareDocumentPosition(d.createElement("fieldset"))}),ue(function(e){return e.innerHTML="","#"===e.firstChild.getAttribute("href")})||le("type|href|height|width",function(e,t,n){if(!n)return e.getAttribute(t,"type"===t.toLowerCase()?1:2)}),n.attributes&&ue(function(e){return e.innerHTML="",e.firstChild.setAttribute("value",""),""===e.firstChild.getAttribute("value")})||le("value",function(e,t,n){if(!n&&"input"===e.nodeName.toLowerCase())return e.defaultValue}),ue(function(e){return null==e.getAttribute("disabled")})||le(P,function(e,t,n){var r;if(!n)return!0===e[t]?t.toLowerCase():(r=e.getAttributeNode(t))&&r.specified?r.value:null}),oe}(e);w.find=E,w.expr=E.selectors,w.expr[":"]=w.expr.pseudos,w.uniqueSort=w.unique=E.uniqueSort,w.text=E.getText,w.isXMLDoc=E.isXML,w.contains=E.contains,w.escapeSelector=E.escape;var k=function(e,t,n){var r=[],i=void 0!==n;while((e=e[t])&&9!==e.nodeType)if(1===e.nodeType){if(i&&w(e).is(n))break;r.push(e)}return r},S=function(e,t){for(var n=[];e;e=e.nextSibling)1===e.nodeType&&e!==t&&n.push(e);return n},D=w.expr.match.needsContext;function N(e,t){return e.nodeName&&e.nodeName.toLowerCase()===t.toLowerCase()}var A=/^<([a-z][^\/\0>:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i;function j(e,t,n){return g(t)?w.grep(e,function(e,r){return!!t.call(e,r,e)!==n}):t.nodeType?w.grep(e,function(e){return e===t!==n}):"string"!=typeof t?w.grep(e,function(e){return u.call(t,e)>-1!==n}):w.filter(t,e,n)}w.filter=function(e,t,n){var r=t[0];return n&&(e=":not("+e+")"),1===t.length&&1===r.nodeType?w.find.matchesSelector(r,e)?[r]:[]:w.find.matches(e,w.grep(t,function(e){return 1===e.nodeType}))},w.fn.extend({find:function(e){var t,n,r=this.length,i=this;if("string"!=typeof e)return this.pushStack(w(e).filter(function(){for(t=0;t1?w.uniqueSort(n):n},filter:function(e){return this.pushStack(j(this,e||[],!1))},not:function(e){return this.pushStack(j(this,e||[],!0))},is:function(e){return!!j(this,"string"==typeof e&&D.test(e)?w(e):e||[],!1).length}});var q,L=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]+))$/;(w.fn.init=function(e,t,n){var i,o;if(!e)return this;if(n=n||q,"string"==typeof e){if(!(i="<"===e[0]&&">"===e[e.length-1]&&e.length>=3?[null,e,null]:L.exec(e))||!i[1]&&t)return!t||t.jquery?(t||n).find(e):this.constructor(t).find(e);if(i[1]){if(t=t instanceof w?t[0]:t,w.merge(this,w.parseHTML(i[1],t&&t.nodeType?t.ownerDocument||t:r,!0)),A.test(i[1])&&w.isPlainObject(t))for(i in t)g(this[i])?this[i](t[i]):this.attr(i,t[i]);return this}return(o=r.getElementById(i[2]))&&(this[0]=o,this.length=1),this}return e.nodeType?(this[0]=e,this.length=1,this):g(e)?void 0!==n.ready?n.ready(e):e(w):w.makeArray(e,this)}).prototype=w.fn,q=w(r);var H=/^(?:parents|prev(?:Until|All))/,O={children:!0,contents:!0,next:!0,prev:!0};w.fn.extend({has:function(e){var t=w(e,this),n=t.length;return this.filter(function(){for(var e=0;e-1:1===n.nodeType&&w.find.matchesSelector(n,e))){o.push(n);break}return this.pushStack(o.length>1?w.uniqueSort(o):o)},index:function(e){return e?"string"==typeof e?u.call(w(e),this[0]):u.call(this,e.jquery?e[0]:e):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(e,t){return this.pushStack(w.uniqueSort(w.merge(this.get(),w(e,t))))},addBack:function(e){return this.add(null==e?this.prevObject:this.prevObject.filter(e))}});function P(e,t){while((e=e[t])&&1!==e.nodeType);return e}w.each({parent:function(e){var t=e.parentNode;return t&&11!==t.nodeType?t:null},parents:function(e){return k(e,"parentNode")},parentsUntil:function(e,t,n){return k(e,"parentNode",n)},next:function(e){return P(e,"nextSibling")},prev:function(e){return P(e,"previousSibling")},nextAll:function(e){return k(e,"nextSibling")},prevAll:function(e){return k(e,"previousSibling")},nextUntil:function(e,t,n){return k(e,"nextSibling",n)},prevUntil:function(e,t,n){return k(e,"previousSibling",n)},siblings:function(e){return S((e.parentNode||{}).firstChild,e)},children:function(e){return S(e.firstChild)},contents:function(e){return N(e,"iframe")?e.contentDocument:(N(e,"template")&&(e=e.content||e),w.merge([],e.childNodes))}},function(e,t){w.fn[e]=function(n,r){var i=w.map(this,t,n);return"Until"!==e.slice(-5)&&(r=n),r&&"string"==typeof r&&(i=w.filter(r,i)),this.length>1&&(O[e]||w.uniqueSort(i),H.test(e)&&i.reverse()),this.pushStack(i)}});var M=/[^\x20\t\r\n\f]+/g;function R(e){var t={};return w.each(e.match(M)||[],function(e,n){t[n]=!0}),t}w.Callbacks=function(e){e="string"==typeof e?R(e):w.extend({},e);var t,n,r,i,o=[],a=[],s=-1,u=function(){for(i=i||e.once,r=t=!0;a.length;s=-1){n=a.shift();while(++s-1)o.splice(n,1),n<=s&&s--}),this},has:function(e){return e?w.inArray(e,o)>-1:o.length>0},empty:function(){return o&&(o=[]),this},disable:function(){return i=a=[],o=n="",this},disabled:function(){return!o},lock:function(){return i=a=[],n||t||(o=n=""),this},locked:function(){return!!i},fireWith:function(e,n){return i||(n=[e,(n=n||[]).slice?n.slice():n],a.push(n),t||u()),this},fire:function(){return l.fireWith(this,arguments),this},fired:function(){return!!r}};return l};function I(e){return e}function W(e){throw e}function $(e,t,n,r){var i;try{e&&g(i=e.promise)?i.call(e).done(t).fail(n):e&&g(i=e.then)?i.call(e,t,n):t.apply(void 0,[e].slice(r))}catch(e){n.apply(void 0,[e])}}w.extend({Deferred:function(t){var n=[["notify","progress",w.Callbacks("memory"),w.Callbacks("memory"),2],["resolve","done",w.Callbacks("once memory"),w.Callbacks("once memory"),0,"resolved"],["reject","fail",w.Callbacks("once memory"),w.Callbacks("once memory"),1,"rejected"]],r="pending",i={state:function(){return r},always:function(){return o.done(arguments).fail(arguments),this},"catch":function(e){return i.then(null,e)},pipe:function(){var e=arguments;return w.Deferred(function(t){w.each(n,function(n,r){var i=g(e[r[4]])&&e[r[4]];o[r[1]](function(){var e=i&&i.apply(this,arguments);e&&g(e.promise)?e.promise().progress(t.notify).done(t.resolve).fail(t.reject):t[r[0]+"With"](this,i?[e]:arguments)})}),e=null}).promise()},then:function(t,r,i){var o=0;function a(t,n,r,i){return function(){var s=this,u=arguments,l=function(){var e,l;if(!(t=o&&(r!==W&&(s=void 0,u=[e]),n.rejectWith(s,u))}};t?c():(w.Deferred.getStackHook&&(c.stackTrace=w.Deferred.getStackHook()),e.setTimeout(c))}}return w.Deferred(function(e){n[0][3].add(a(0,e,g(i)?i:I,e.notifyWith)),n[1][3].add(a(0,e,g(t)?t:I)),n[2][3].add(a(0,e,g(r)?r:W))}).promise()},promise:function(e){return null!=e?w.extend(e,i):i}},o={};return w.each(n,function(e,t){var a=t[2],s=t[5];i[t[1]]=a.add,s&&a.add(function(){r=s},n[3-e][2].disable,n[3-e][3].disable,n[0][2].lock,n[0][3].lock),a.add(t[3].fire),o[t[0]]=function(){return o[t[0]+"With"](this===o?void 0:this,arguments),this},o[t[0]+"With"]=a.fireWith}),i.promise(o),t&&t.call(o,o),o},when:function(e){var t=arguments.length,n=t,r=Array(n),i=o.call(arguments),a=w.Deferred(),s=function(e){return function(n){r[e]=this,i[e]=arguments.length>1?o.call(arguments):n,--t||a.resolveWith(r,i)}};if(t<=1&&($(e,a.done(s(n)).resolve,a.reject,!t),"pending"===a.state()||g(i[n]&&i[n].then)))return a.then();while(n--)$(i[n],s(n),a.reject);return a.promise()}});var B=/^(Eval|Internal|Range|Reference|Syntax|Type|URI)Error$/;w.Deferred.exceptionHook=function(t,n){e.console&&e.console.warn&&t&&B.test(t.name)&&e.console.warn("jQuery.Deferred exception: "+t.message,t.stack,n)},w.readyException=function(t){e.setTimeout(function(){throw t})};var F=w.Deferred();w.fn.ready=function(e){return F.then(e)["catch"](function(e){w.readyException(e)}),this},w.extend({isReady:!1,readyWait:1,ready:function(e){(!0===e?--w.readyWait:w.isReady)||(w.isReady=!0,!0!==e&&--w.readyWait>0||F.resolveWith(r,[w]))}}),w.ready.then=F.then;function _(){r.removeEventListener("DOMContentLoaded",_),e.removeEventListener("load",_),w.ready()}"complete"===r.readyState||"loading"!==r.readyState&&!r.documentElement.doScroll?e.setTimeout(w.ready):(r.addEventListener("DOMContentLoaded",_),e.addEventListener("load",_));var z=function(e,t,n,r,i,o,a){var s=0,u=e.length,l=null==n;if("object"===x(n)){i=!0;for(s in n)z(e,t,s,n[s],!0,o,a)}else if(void 0!==r&&(i=!0,g(r)||(a=!0),l&&(a?(t.call(e,r),t=null):(l=t,t=function(e,t,n){return l.call(w(e),n)})),t))for(;s1,null,!0)},removeData:function(e){return this.each(function(){K.remove(this,e)})}}),w.extend({queue:function(e,t,n){var r;if(e)return t=(t||"fx")+"queue",r=J.get(e,t),n&&(!r||Array.isArray(n)?r=J.access(e,t,w.makeArray(n)):r.push(n)),r||[]},dequeue:function(e,t){t=t||"fx";var n=w.queue(e,t),r=n.length,i=n.shift(),o=w._queueHooks(e,t),a=function(){w.dequeue(e,t)};"inprogress"===i&&(i=n.shift(),r--),i&&("fx"===t&&n.unshift("inprogress"),delete o.stop,i.call(e,a,o)),!r&&o&&o.empty.fire()},_queueHooks:function(e,t){var n=t+"queueHooks";return J.get(e,n)||J.access(e,n,{empty:w.Callbacks("once memory").add(function(){J.remove(e,[t+"queue",n])})})}}),w.fn.extend({queue:function(e,t){var n=2;return"string"!=typeof e&&(t=e,e="fx",n--),arguments.length\x20\t\r\n\f]+)/i,he=/^$|^module$|\/(?:java|ecma)script/i,ge={option:[1,""],thead:[1,"","
"],col:[2,"","
"],tr:[2,"","
"],td:[3,"","
"],_default:[0,"",""]};ge.optgroup=ge.option,ge.tbody=ge.tfoot=ge.colgroup=ge.caption=ge.thead,ge.th=ge.td;function ye(e,t){var n;return n="undefined"!=typeof e.getElementsByTagName?e.getElementsByTagName(t||"*"):"undefined"!=typeof e.querySelectorAll?e.querySelectorAll(t||"*"):[],void 0===t||t&&N(e,t)?w.merge([e],n):n}function ve(e,t){for(var n=0,r=e.length;n-1)i&&i.push(o);else if(l=w.contains(o.ownerDocument,o),a=ye(f.appendChild(o),"script"),l&&ve(a),n){c=0;while(o=a[c++])he.test(o.type||"")&&n.push(o)}return f}!function(){var e=r.createDocumentFragment().appendChild(r.createElement("div")),t=r.createElement("input");t.setAttribute("type","radio"),t.setAttribute("checked","checked"),t.setAttribute("name","t"),e.appendChild(t),h.checkClone=e.cloneNode(!0).cloneNode(!0).lastChild.checked,e.innerHTML="",h.noCloneChecked=!!e.cloneNode(!0).lastChild.defaultValue}();var be=r.documentElement,we=/^key/,Te=/^(?:mouse|pointer|contextmenu|drag|drop)|click/,Ce=/^([^.]*)(?:\.(.+)|)/;function Ee(){return!0}function ke(){return!1}function Se(){try{return r.activeElement}catch(e){}}function De(e,t,n,r,i,o){var a,s;if("object"==typeof t){"string"!=typeof n&&(r=r||n,n=void 0);for(s in t)De(e,s,n,r,t[s],o);return e}if(null==r&&null==i?(i=n,r=n=void 0):null==i&&("string"==typeof n?(i=r,r=void 0):(i=r,r=n,n=void 0)),!1===i)i=ke;else if(!i)return e;return 1===o&&(a=i,(i=function(e){return w().off(e),a.apply(this,arguments)}).guid=a.guid||(a.guid=w.guid++)),e.each(function(){w.event.add(this,t,i,r,n)})}w.event={global:{},add:function(e,t,n,r,i){var o,a,s,u,l,c,f,p,d,h,g,y=J.get(e);if(y){n.handler&&(n=(o=n).handler,i=o.selector),i&&w.find.matchesSelector(be,i),n.guid||(n.guid=w.guid++),(u=y.events)||(u=y.events={}),(a=y.handle)||(a=y.handle=function(t){return"undefined"!=typeof w&&w.event.triggered!==t.type?w.event.dispatch.apply(e,arguments):void 0}),l=(t=(t||"").match(M)||[""]).length;while(l--)d=g=(s=Ce.exec(t[l])||[])[1],h=(s[2]||"").split(".").sort(),d&&(f=w.event.special[d]||{},d=(i?f.delegateType:f.bindType)||d,f=w.event.special[d]||{},c=w.extend({type:d,origType:g,data:r,handler:n,guid:n.guid,selector:i,needsContext:i&&w.expr.match.needsContext.test(i),namespace:h.join(".")},o),(p=u[d])||((p=u[d]=[]).delegateCount=0,f.setup&&!1!==f.setup.call(e,r,h,a)||e.addEventListener&&e.addEventListener(d,a)),f.add&&(f.add.call(e,c),c.handler.guid||(c.handler.guid=n.guid)),i?p.splice(p.delegateCount++,0,c):p.push(c),w.event.global[d]=!0)}},remove:function(e,t,n,r,i){var o,a,s,u,l,c,f,p,d,h,g,y=J.hasData(e)&&J.get(e);if(y&&(u=y.events)){l=(t=(t||"").match(M)||[""]).length;while(l--)if(s=Ce.exec(t[l])||[],d=g=s[1],h=(s[2]||"").split(".").sort(),d){f=w.event.special[d]||{},p=u[d=(r?f.delegateType:f.bindType)||d]||[],s=s[2]&&new RegExp("(^|\\.)"+h.join("\\.(?:.*\\.|)")+"(\\.|$)"),a=o=p.length;while(o--)c=p[o],!i&&g!==c.origType||n&&n.guid!==c.guid||s&&!s.test(c.namespace)||r&&r!==c.selector&&("**"!==r||!c.selector)||(p.splice(o,1),c.selector&&p.delegateCount--,f.remove&&f.remove.call(e,c));a&&!p.length&&(f.teardown&&!1!==f.teardown.call(e,h,y.handle)||w.removeEvent(e,d,y.handle),delete u[d])}else for(d in u)w.event.remove(e,d+t[l],n,r,!0);w.isEmptyObject(u)&&J.remove(e,"handle events")}},dispatch:function(e){var t=w.event.fix(e),n,r,i,o,a,s,u=new Array(arguments.length),l=(J.get(this,"events")||{})[t.type]||[],c=w.event.special[t.type]||{};for(u[0]=t,n=1;n=1))for(;l!==this;l=l.parentNode||this)if(1===l.nodeType&&("click"!==e.type||!0!==l.disabled)){for(o=[],a={},n=0;n-1:w.find(i,this,null,[l]).length),a[i]&&o.push(r);o.length&&s.push({elem:l,handlers:o})}return l=this,u\x20\t\r\n\f]*)[^>]*)\/>/gi,Ae=/\s*$/g;function Le(e,t){return N(e,"table")&&N(11!==t.nodeType?t:t.firstChild,"tr")?w(e).children("tbody")[0]||e:e}function He(e){return e.type=(null!==e.getAttribute("type"))+"/"+e.type,e}function Oe(e){return"true/"===(e.type||"").slice(0,5)?e.type=e.type.slice(5):e.removeAttribute("type"),e}function Pe(e,t){var n,r,i,o,a,s,u,l;if(1===t.nodeType){if(J.hasData(e)&&(o=J.access(e),a=J.set(t,o),l=o.events)){delete a.handle,a.events={};for(i in l)for(n=0,r=l[i].length;n1&&"string"==typeof y&&!h.checkClone&&je.test(y))return e.each(function(i){var o=e.eq(i);v&&(t[0]=y.call(this,i,o.html())),Re(o,t,n,r)});if(p&&(i=xe(t,e[0].ownerDocument,!1,e,r),o=i.firstChild,1===i.childNodes.length&&(i=o),o||r)){for(u=(s=w.map(ye(i,"script"),He)).length;f")},clone:function(e,t,n){var r,i,o,a,s=e.cloneNode(!0),u=w.contains(e.ownerDocument,e);if(!(h.noCloneChecked||1!==e.nodeType&&11!==e.nodeType||w.isXMLDoc(e)))for(a=ye(s),r=0,i=(o=ye(e)).length;r0&&ve(a,!u&&ye(e,"script")),s},cleanData:function(e){for(var t,n,r,i=w.event.special,o=0;void 0!==(n=e[o]);o++)if(Y(n)){if(t=n[J.expando]){if(t.events)for(r in t.events)i[r]?w.event.remove(n,r):w.removeEvent(n,r,t.handle);n[J.expando]=void 0}n[K.expando]&&(n[K.expando]=void 0)}}}),w.fn.extend({detach:function(e){return Ie(this,e,!0)},remove:function(e){return Ie(this,e)},text:function(e){return z(this,function(e){return void 0===e?w.text(this):this.empty().each(function(){1!==this.nodeType&&11!==this.nodeType&&9!==this.nodeType||(this.textContent=e)})},null,e,arguments.length)},append:function(){return Re(this,arguments,function(e){1!==this.nodeType&&11!==this.nodeType&&9!==this.nodeType||Le(this,e).appendChild(e)})},prepend:function(){return Re(this,arguments,function(e){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var t=Le(this,e);t.insertBefore(e,t.firstChild)}})},before:function(){return Re(this,arguments,function(e){this.parentNode&&this.parentNode.insertBefore(e,this)})},after:function(){return Re(this,arguments,function(e){this.parentNode&&this.parentNode.insertBefore(e,this.nextSibling)})},empty:function(){for(var e,t=0;null!=(e=this[t]);t++)1===e.nodeType&&(w.cleanData(ye(e,!1)),e.textContent="");return this},clone:function(e,t){return e=null!=e&&e,t=null==t?e:t,this.map(function(){return w.clone(this,e,t)})},html:function(e){return z(this,function(e){var t=this[0]||{},n=0,r=this.length;if(void 0===e&&1===t.nodeType)return t.innerHTML;if("string"==typeof e&&!Ae.test(e)&&!ge[(de.exec(e)||["",""])[1].toLowerCase()]){e=w.htmlPrefilter(e);try{for(;n=0&&(u+=Math.max(0,Math.ceil(e["offset"+t[0].toUpperCase()+t.slice(1)]-o-u-s-.5))),u}function et(e,t,n){var r=$e(e),i=Fe(e,t,r),o="border-box"===w.css(e,"boxSizing",!1,r),a=o;if(We.test(i)){if(!n)return i;i="auto"}return a=a&&(h.boxSizingReliable()||i===e.style[t]),("auto"===i||!parseFloat(i)&&"inline"===w.css(e,"display",!1,r))&&(i=e["offset"+t[0].toUpperCase()+t.slice(1)],a=!0),(i=parseFloat(i)||0)+Ze(e,t,n||(o?"border":"content"),a,r,i)+"px"}w.extend({cssHooks:{opacity:{get:function(e,t){if(t){var n=Fe(e,"opacity");return""===n?"1":n}}}},cssNumber:{animationIterationCount:!0,columnCount:!0,fillOpacity:!0,flexGrow:!0,flexShrink:!0,fontWeight:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,widows:!0,zIndex:!0,zoom:!0},cssProps:{},style:function(e,t,n,r){if(e&&3!==e.nodeType&&8!==e.nodeType&&e.style){var i,o,a,s=G(t),u=Xe.test(t),l=e.style;if(u||(t=Je(s)),a=w.cssHooks[t]||w.cssHooks[s],void 0===n)return a&&"get"in a&&void 0!==(i=a.get(e,!1,r))?i:l[t];"string"==(o=typeof n)&&(i=ie.exec(n))&&i[1]&&(n=ue(e,t,i),o="number"),null!=n&&n===n&&("number"===o&&(n+=i&&i[3]||(w.cssNumber[s]?"":"px")),h.clearCloneStyle||""!==n||0!==t.indexOf("background")||(l[t]="inherit"),a&&"set"in a&&void 0===(n=a.set(e,n,r))||(u?l.setProperty(t,n):l[t]=n))}},css:function(e,t,n,r){var i,o,a,s=G(t);return Xe.test(t)||(t=Je(s)),(a=w.cssHooks[t]||w.cssHooks[s])&&"get"in a&&(i=a.get(e,!0,n)),void 0===i&&(i=Fe(e,t,r)),"normal"===i&&t in Ve&&(i=Ve[t]),""===n||n?(o=parseFloat(i),!0===n||isFinite(o)?o||0:i):i}}),w.each(["height","width"],function(e,t){w.cssHooks[t]={get:function(e,n,r){if(n)return!ze.test(w.css(e,"display"))||e.getClientRects().length&&e.getBoundingClientRect().width?et(e,t,r):se(e,Ue,function(){return et(e,t,r)})},set:function(e,n,r){var i,o=$e(e),a="border-box"===w.css(e,"boxSizing",!1,o),s=r&&Ze(e,t,r,a,o);return a&&h.scrollboxSize()===o.position&&(s-=Math.ceil(e["offset"+t[0].toUpperCase()+t.slice(1)]-parseFloat(o[t])-Ze(e,t,"border",!1,o)-.5)),s&&(i=ie.exec(n))&&"px"!==(i[3]||"px")&&(e.style[t]=n,n=w.css(e,t)),Ke(e,n,s)}}}),w.cssHooks.marginLeft=_e(h.reliableMarginLeft,function(e,t){if(t)return(parseFloat(Fe(e,"marginLeft"))||e.getBoundingClientRect().left-se(e,{marginLeft:0},function(){return e.getBoundingClientRect().left}))+"px"}),w.each({margin:"",padding:"",border:"Width"},function(e,t){w.cssHooks[e+t]={expand:function(n){for(var r=0,i={},o="string"==typeof n?n.split(" "):[n];r<4;r++)i[e+oe[r]+t]=o[r]||o[r-2]||o[0];return i}},"margin"!==e&&(w.cssHooks[e+t].set=Ke)}),w.fn.extend({css:function(e,t){return z(this,function(e,t,n){var r,i,o={},a=0;if(Array.isArray(t)){for(r=$e(e),i=t.length;a1)}});function tt(e,t,n,r,i){return new tt.prototype.init(e,t,n,r,i)}w.Tween=tt,tt.prototype={constructor:tt,init:function(e,t,n,r,i,o){this.elem=e,this.prop=n,this.easing=i||w.easing._default,this.options=t,this.start=this.now=this.cur(),this.end=r,this.unit=o||(w.cssNumber[n]?"":"px")},cur:function(){var e=tt.propHooks[this.prop];return e&&e.get?e.get(this):tt.propHooks._default.get(this)},run:function(e){var t,n=tt.propHooks[this.prop];return this.options.duration?this.pos=t=w.easing[this.easing](e,this.options.duration*e,0,1,this.options.duration):this.pos=t=e,this.now=(this.end-this.start)*t+this.start,this.options.step&&this.options.step.call(this.elem,this.now,this),n&&n.set?n.set(this):tt.propHooks._default.set(this),this}},tt.prototype.init.prototype=tt.prototype,tt.propHooks={_default:{get:function(e){var t;return 1!==e.elem.nodeType||null!=e.elem[e.prop]&&null==e.elem.style[e.prop]?e.elem[e.prop]:(t=w.css(e.elem,e.prop,""))&&"auto"!==t?t:0},set:function(e){w.fx.step[e.prop]?w.fx.step[e.prop](e):1!==e.elem.nodeType||null==e.elem.style[w.cssProps[e.prop]]&&!w.cssHooks[e.prop]?e.elem[e.prop]=e.now:w.style(e.elem,e.prop,e.now+e.unit)}}},tt.propHooks.scrollTop=tt.propHooks.scrollLeft={set:function(e){e.elem.nodeType&&e.elem.parentNode&&(e.elem[e.prop]=e.now)}},w.easing={linear:function(e){return e},swing:function(e){return.5-Math.cos(e*Math.PI)/2},_default:"swing"},w.fx=tt.prototype.init,w.fx.step={};var nt,rt,it=/^(?:toggle|show|hide)$/,ot=/queueHooks$/;function at(){rt&&(!1===r.hidden&&e.requestAnimationFrame?e.requestAnimationFrame(at):e.setTimeout(at,w.fx.interval),w.fx.tick())}function st(){return e.setTimeout(function(){nt=void 0}),nt=Date.now()}function ut(e,t){var n,r=0,i={height:e};for(t=t?1:0;r<4;r+=2-t)i["margin"+(n=oe[r])]=i["padding"+n]=e;return t&&(i.opacity=i.width=e),i}function lt(e,t,n){for(var r,i=(pt.tweeners[t]||[]).concat(pt.tweeners["*"]),o=0,a=i.length;o1)},removeAttr:function(e){return this.each(function(){w.removeAttr(this,e)})}}),w.extend({attr:function(e,t,n){var r,i,o=e.nodeType;if(3!==o&&8!==o&&2!==o)return"undefined"==typeof e.getAttribute?w.prop(e,t,n):(1===o&&w.isXMLDoc(e)||(i=w.attrHooks[t.toLowerCase()]||(w.expr.match.bool.test(t)?dt:void 0)),void 0!==n?null===n?void w.removeAttr(e,t):i&&"set"in i&&void 0!==(r=i.set(e,n,t))?r:(e.setAttribute(t,n+""),n):i&&"get"in i&&null!==(r=i.get(e,t))?r:null==(r=w.find.attr(e,t))?void 0:r)},attrHooks:{type:{set:function(e,t){if(!h.radioValue&&"radio"===t&&N(e,"input")){var n=e.value;return e.setAttribute("type",t),n&&(e.value=n),t}}}},removeAttr:function(e,t){var n,r=0,i=t&&t.match(M);if(i&&1===e.nodeType)while(n=i[r++])e.removeAttribute(n)}}),dt={set:function(e,t,n){return!1===t?w.removeAttr(e,n):e.setAttribute(n,n),n}},w.each(w.expr.match.bool.source.match(/\w+/g),function(e,t){var n=ht[t]||w.find.attr;ht[t]=function(e,t,r){var i,o,a=t.toLowerCase();return r||(o=ht[a],ht[a]=i,i=null!=n(e,t,r)?a:null,ht[a]=o),i}});var gt=/^(?:input|select|textarea|button)$/i,yt=/^(?:a|area)$/i;w.fn.extend({prop:function(e,t){return z(this,w.prop,e,t,arguments.length>1)},removeProp:function(e){return this.each(function(){delete this[w.propFix[e]||e]})}}),w.extend({prop:function(e,t,n){var r,i,o=e.nodeType;if(3!==o&&8!==o&&2!==o)return 1===o&&w.isXMLDoc(e)||(t=w.propFix[t]||t,i=w.propHooks[t]),void 0!==n?i&&"set"in i&&void 0!==(r=i.set(e,n,t))?r:e[t]=n:i&&"get"in i&&null!==(r=i.get(e,t))?r:e[t]},propHooks:{tabIndex:{get:function(e){var t=w.find.attr(e,"tabindex");return t?parseInt(t,10):gt.test(e.nodeName)||yt.test(e.nodeName)&&e.href?0:-1}}},propFix:{"for":"htmlFor","class":"className"}}),h.optSelected||(w.propHooks.selected={get:function(e){var t=e.parentNode;return t&&t.parentNode&&t.parentNode.selectedIndex,null},set:function(e){var t=e.parentNode;t&&(t.selectedIndex,t.parentNode&&t.parentNode.selectedIndex)}}),w.each(["tabIndex","readOnly","maxLength","cellSpacing","cellPadding","rowSpan","colSpan","useMap","frameBorder","contentEditable"],function(){w.propFix[this.toLowerCase()]=this});function vt(e){return(e.match(M)||[]).join(" ")}function mt(e){return e.getAttribute&&e.getAttribute("class")||""}function xt(e){return Array.isArray(e)?e:"string"==typeof e?e.match(M)||[]:[]}w.fn.extend({addClass:function(e){var t,n,r,i,o,a,s,u=0;if(g(e))return this.each(function(t){w(this).addClass(e.call(this,t,mt(this)))});if((t=xt(e)).length)while(n=this[u++])if(i=mt(n),r=1===n.nodeType&&" "+vt(i)+" "){a=0;while(o=t[a++])r.indexOf(" "+o+" ")<0&&(r+=o+" ");i!==(s=vt(r))&&n.setAttribute("class",s)}return this},removeClass:function(e){var t,n,r,i,o,a,s,u=0;if(g(e))return this.each(function(t){w(this).removeClass(e.call(this,t,mt(this)))});if(!arguments.length)return this.attr("class","");if((t=xt(e)).length)while(n=this[u++])if(i=mt(n),r=1===n.nodeType&&" "+vt(i)+" "){a=0;while(o=t[a++])while(r.indexOf(" "+o+" ")>-1)r=r.replace(" "+o+" "," ");i!==(s=vt(r))&&n.setAttribute("class",s)}return this},toggleClass:function(e,t){var n=typeof e,r="string"===n||Array.isArray(e);return"boolean"==typeof t&&r?t?this.addClass(e):this.removeClass(e):g(e)?this.each(function(n){w(this).toggleClass(e.call(this,n,mt(this),t),t)}):this.each(function(){var t,i,o,a;if(r){i=0,o=w(this),a=xt(e);while(t=a[i++])o.hasClass(t)?o.removeClass(t):o.addClass(t)}else void 0!==e&&"boolean"!==n||((t=mt(this))&&J.set(this,"__className__",t),this.setAttribute&&this.setAttribute("class",t||!1===e?"":J.get(this,"__className__")||""))})},hasClass:function(e){var t,n,r=0;t=" "+e+" ";while(n=this[r++])if(1===n.nodeType&&(" "+vt(mt(n))+" ").indexOf(t)>-1)return!0;return!1}});var bt=/\r/g;w.fn.extend({val:function(e){var t,n,r,i=this[0];{if(arguments.length)return r=g(e),this.each(function(n){var i;1===this.nodeType&&(null==(i=r?e.call(this,n,w(this).val()):e)?i="":"number"==typeof i?i+="":Array.isArray(i)&&(i=w.map(i,function(e){return null==e?"":e+""})),(t=w.valHooks[this.type]||w.valHooks[this.nodeName.toLowerCase()])&&"set"in t&&void 0!==t.set(this,i,"value")||(this.value=i))});if(i)return(t=w.valHooks[i.type]||w.valHooks[i.nodeName.toLowerCase()])&&"get"in t&&void 0!==(n=t.get(i,"value"))?n:"string"==typeof(n=i.value)?n.replace(bt,""):null==n?"":n}}}),w.extend({valHooks:{option:{get:function(e){var t=w.find.attr(e,"value");return null!=t?t:vt(w.text(e))}},select:{get:function(e){var t,n,r,i=e.options,o=e.selectedIndex,a="select-one"===e.type,s=a?null:[],u=a?o+1:i.length;for(r=o<0?u:a?o:0;r-1)&&(n=!0);return n||(e.selectedIndex=-1),o}}}}),w.each(["radio","checkbox"],function(){w.valHooks[this]={set:function(e,t){if(Array.isArray(t))return e.checked=w.inArray(w(e).val(),t)>-1}},h.checkOn||(w.valHooks[this].get=function(e){return null===e.getAttribute("value")?"on":e.value})}),h.focusin="onfocusin"in e;var wt=/^(?:focusinfocus|focusoutblur)$/,Tt=function(e){e.stopPropagation()};w.extend(w.event,{trigger:function(t,n,i,o){var a,s,u,l,c,p,d,h,v=[i||r],m=f.call(t,"type")?t.type:t,x=f.call(t,"namespace")?t.namespace.split("."):[];if(s=h=u=i=i||r,3!==i.nodeType&&8!==i.nodeType&&!wt.test(m+w.event.triggered)&&(m.indexOf(".")>-1&&(m=(x=m.split(".")).shift(),x.sort()),c=m.indexOf(":")<0&&"on"+m,t=t[w.expando]?t:new w.Event(m,"object"==typeof t&&t),t.isTrigger=o?2:3,t.namespace=x.join("."),t.rnamespace=t.namespace?new RegExp("(^|\\.)"+x.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,t.result=void 0,t.target||(t.target=i),n=null==n?[t]:w.makeArray(n,[t]),d=w.event.special[m]||{},o||!d.trigger||!1!==d.trigger.apply(i,n))){if(!o&&!d.noBubble&&!y(i)){for(l=d.delegateType||m,wt.test(l+m)||(s=s.parentNode);s;s=s.parentNode)v.push(s),u=s;u===(i.ownerDocument||r)&&v.push(u.defaultView||u.parentWindow||e)}a=0;while((s=v[a++])&&!t.isPropagationStopped())h=s,t.type=a>1?l:d.bindType||m,(p=(J.get(s,"events")||{})[t.type]&&J.get(s,"handle"))&&p.apply(s,n),(p=c&&s[c])&&p.apply&&Y(s)&&(t.result=p.apply(s,n),!1===t.result&&t.preventDefault());return t.type=m,o||t.isDefaultPrevented()||d._default&&!1!==d._default.apply(v.pop(),n)||!Y(i)||c&&g(i[m])&&!y(i)&&((u=i[c])&&(i[c]=null),w.event.triggered=m,t.isPropagationStopped()&&h.addEventListener(m,Tt),i[m](),t.isPropagationStopped()&&h.removeEventListener(m,Tt),w.event.triggered=void 0,u&&(i[c]=u)),t.result}},simulate:function(e,t,n){var r=w.extend(new w.Event,n,{type:e,isSimulated:!0});w.event.trigger(r,null,t)}}),w.fn.extend({trigger:function(e,t){return this.each(function(){w.event.trigger(e,t,this)})},triggerHandler:function(e,t){var n=this[0];if(n)return w.event.trigger(e,t,n,!0)}}),h.focusin||w.each({focus:"focusin",blur:"focusout"},function(e,t){var n=function(e){w.event.simulate(t,e.target,w.event.fix(e))};w.event.special[t]={setup:function(){var r=this.ownerDocument||this,i=J.access(r,t);i||r.addEventListener(e,n,!0),J.access(r,t,(i||0)+1)},teardown:function(){var r=this.ownerDocument||this,i=J.access(r,t)-1;i?J.access(r,t,i):(r.removeEventListener(e,n,!0),J.remove(r,t))}}});var Ct=e.location,Et=Date.now(),kt=/\?/;w.parseXML=function(t){var n;if(!t||"string"!=typeof t)return null;try{n=(new e.DOMParser).parseFromString(t,"text/xml")}catch(e){n=void 0}return n&&!n.getElementsByTagName("parsererror").length||w.error("Invalid XML: "+t),n};var St=/\[\]$/,Dt=/\r?\n/g,Nt=/^(?:submit|button|image|reset|file)$/i,At=/^(?:input|select|textarea|keygen)/i;function jt(e,t,n,r){var i;if(Array.isArray(t))w.each(t,function(t,i){n||St.test(e)?r(e,i):jt(e+"["+("object"==typeof i&&null!=i?t:"")+"]",i,n,r)});else if(n||"object"!==x(t))r(e,t);else for(i in t)jt(e+"["+i+"]",t[i],n,r)}w.param=function(e,t){var n,r=[],i=function(e,t){var n=g(t)?t():t;r[r.length]=encodeURIComponent(e)+"="+encodeURIComponent(null==n?"":n)};if(Array.isArray(e)||e.jquery&&!w.isPlainObject(e))w.each(e,function(){i(this.name,this.value)});else for(n in e)jt(n,e[n],t,i);return r.join("&")},w.fn.extend({serialize:function(){return w.param(this.serializeArray())},serializeArray:function(){return this.map(function(){var e=w.prop(this,"elements");return e?w.makeArray(e):this}).filter(function(){var e=this.type;return this.name&&!w(this).is(":disabled")&&At.test(this.nodeName)&&!Nt.test(e)&&(this.checked||!pe.test(e))}).map(function(e,t){var n=w(this).val();return null==n?null:Array.isArray(n)?w.map(n,function(e){return{name:t.name,value:e.replace(Dt,"\r\n")}}):{name:t.name,value:n.replace(Dt,"\r\n")}}).get()}});var qt=/%20/g,Lt=/#.*$/,Ht=/([?&])_=[^&]*/,Ot=/^(.*?):[ \t]*([^\r\n]*)$/gm,Pt=/^(?:about|app|app-storage|.+-extension|file|res|widget):$/,Mt=/^(?:GET|HEAD)$/,Rt=/^\/\//,It={},Wt={},$t="*/".concat("*"),Bt=r.createElement("a");Bt.href=Ct.href;function Ft(e){return function(t,n){"string"!=typeof t&&(n=t,t="*");var r,i=0,o=t.toLowerCase().match(M)||[];if(g(n))while(r=o[i++])"+"===r[0]?(r=r.slice(1)||"*",(e[r]=e[r]||[]).unshift(n)):(e[r]=e[r]||[]).push(n)}}function _t(e,t,n,r){var i={},o=e===Wt;function a(s){var u;return i[s]=!0,w.each(e[s]||[],function(e,s){var l=s(t,n,r);return"string"!=typeof l||o||i[l]?o?!(u=l):void 0:(t.dataTypes.unshift(l),a(l),!1)}),u}return a(t.dataTypes[0])||!i["*"]&&a("*")}function zt(e,t){var n,r,i=w.ajaxSettings.flatOptions||{};for(n in t)void 0!==t[n]&&((i[n]?e:r||(r={}))[n]=t[n]);return r&&w.extend(!0,e,r),e}function Xt(e,t,n){var r,i,o,a,s=e.contents,u=e.dataTypes;while("*"===u[0])u.shift(),void 0===r&&(r=e.mimeType||t.getResponseHeader("Content-Type"));if(r)for(i in s)if(s[i]&&s[i].test(r)){u.unshift(i);break}if(u[0]in n)o=u[0];else{for(i in n){if(!u[0]||e.converters[i+" "+u[0]]){o=i;break}a||(a=i)}o=o||a}if(o)return o!==u[0]&&u.unshift(o),n[o]}function Ut(e,t,n,r){var i,o,a,s,u,l={},c=e.dataTypes.slice();if(c[1])for(a in e.converters)l[a.toLowerCase()]=e.converters[a];o=c.shift();while(o)if(e.responseFields[o]&&(n[e.responseFields[o]]=t),!u&&r&&e.dataFilter&&(t=e.dataFilter(t,e.dataType)),u=o,o=c.shift())if("*"===o)o=u;else if("*"!==u&&u!==o){if(!(a=l[u+" "+o]||l["* "+o]))for(i in l)if((s=i.split(" "))[1]===o&&(a=l[u+" "+s[0]]||l["* "+s[0]])){!0===a?a=l[i]:!0!==l[i]&&(o=s[0],c.unshift(s[1]));break}if(!0!==a)if(a&&e["throws"])t=a(t);else try{t=a(t)}catch(e){return{state:"parsererror",error:a?e:"No conversion from "+u+" to "+o}}}return{state:"success",data:t}}w.extend({active:0,lastModified:{},etag:{},ajaxSettings:{url:Ct.href,type:"GET",isLocal:Pt.test(Ct.protocol),global:!0,processData:!0,async:!0,contentType:"application/x-www-form-urlencoded; charset=UTF-8",accepts:{"*":$t,text:"text/plain",html:"text/html",xml:"application/xml, text/xml",json:"application/json, text/javascript"},contents:{xml:/\bxml\b/,html:/\bhtml/,json:/\bjson\b/},responseFields:{xml:"responseXML",text:"responseText",json:"responseJSON"},converters:{"* text":String,"text html":!0,"text json":JSON.parse,"text xml":w.parseXML},flatOptions:{url:!0,context:!0}},ajaxSetup:function(e,t){return t?zt(zt(e,w.ajaxSettings),t):zt(w.ajaxSettings,e)},ajaxPrefilter:Ft(It),ajaxTransport:Ft(Wt),ajax:function(t,n){"object"==typeof t&&(n=t,t=void 0),n=n||{};var i,o,a,s,u,l,c,f,p,d,h=w.ajaxSetup({},n),g=h.context||h,y=h.context&&(g.nodeType||g.jquery)?w(g):w.event,v=w.Deferred(),m=w.Callbacks("once memory"),x=h.statusCode||{},b={},T={},C="canceled",E={readyState:0,getResponseHeader:function(e){var t;if(c){if(!s){s={};while(t=Ot.exec(a))s[t[1].toLowerCase()]=t[2]}t=s[e.toLowerCase()]}return null==t?null:t},getAllResponseHeaders:function(){return c?a:null},setRequestHeader:function(e,t){return null==c&&(e=T[e.toLowerCase()]=T[e.toLowerCase()]||e,b[e]=t),this},overrideMimeType:function(e){return null==c&&(h.mimeType=e),this},statusCode:function(e){var t;if(e)if(c)E.always(e[E.status]);else for(t in e)x[t]=[x[t],e[t]];return this},abort:function(e){var t=e||C;return i&&i.abort(t),k(0,t),this}};if(v.promise(E),h.url=((t||h.url||Ct.href)+"").replace(Rt,Ct.protocol+"//"),h.type=n.method||n.type||h.method||h.type,h.dataTypes=(h.dataType||"*").toLowerCase().match(M)||[""],null==h.crossDomain){l=r.createElement("a");try{l.href=h.url,l.href=l.href,h.crossDomain=Bt.protocol+"//"+Bt.host!=l.protocol+"//"+l.host}catch(e){h.crossDomain=!0}}if(h.data&&h.processData&&"string"!=typeof h.data&&(h.data=w.param(h.data,h.traditional)),_t(It,h,n,E),c)return E;(f=w.event&&h.global)&&0==w.active++&&w.event.trigger("ajaxStart"),h.type=h.type.toUpperCase(),h.hasContent=!Mt.test(h.type),o=h.url.replace(Lt,""),h.hasContent?h.data&&h.processData&&0===(h.contentType||"").indexOf("application/x-www-form-urlencoded")&&(h.data=h.data.replace(qt,"+")):(d=h.url.slice(o.length),h.data&&(h.processData||"string"==typeof h.data)&&(o+=(kt.test(o)?"&":"?")+h.data,delete h.data),!1===h.cache&&(o=o.replace(Ht,"$1"),d=(kt.test(o)?"&":"?")+"_="+Et+++d),h.url=o+d),h.ifModified&&(w.lastModified[o]&&E.setRequestHeader("If-Modified-Since",w.lastModified[o]),w.etag[o]&&E.setRequestHeader("If-None-Match",w.etag[o])),(h.data&&h.hasContent&&!1!==h.contentType||n.contentType)&&E.setRequestHeader("Content-Type",h.contentType),E.setRequestHeader("Accept",h.dataTypes[0]&&h.accepts[h.dataTypes[0]]?h.accepts[h.dataTypes[0]]+("*"!==h.dataTypes[0]?", "+$t+"; q=0.01":""):h.accepts["*"]);for(p in h.headers)E.setRequestHeader(p,h.headers[p]);if(h.beforeSend&&(!1===h.beforeSend.call(g,E,h)||c))return E.abort();if(C="abort",m.add(h.complete),E.done(h.success),E.fail(h.error),i=_t(Wt,h,n,E)){if(E.readyState=1,f&&y.trigger("ajaxSend",[E,h]),c)return E;h.async&&h.timeout>0&&(u=e.setTimeout(function(){E.abort("timeout")},h.timeout));try{c=!1,i.send(b,k)}catch(e){if(c)throw e;k(-1,e)}}else k(-1,"No Transport");function k(t,n,r,s){var l,p,d,b,T,C=n;c||(c=!0,u&&e.clearTimeout(u),i=void 0,a=s||"",E.readyState=t>0?4:0,l=t>=200&&t<300||304===t,r&&(b=Xt(h,E,r)),b=Ut(h,b,E,l),l?(h.ifModified&&((T=E.getResponseHeader("Last-Modified"))&&(w.lastModified[o]=T),(T=E.getResponseHeader("etag"))&&(w.etag[o]=T)),204===t||"HEAD"===h.type?C="nocontent":304===t?C="notmodified":(C=b.state,p=b.data,l=!(d=b.error))):(d=C,!t&&C||(C="error",t<0&&(t=0))),E.status=t,E.statusText=(n||C)+"",l?v.resolveWith(g,[p,C,E]):v.rejectWith(g,[E,C,d]),E.statusCode(x),x=void 0,f&&y.trigger(l?"ajaxSuccess":"ajaxError",[E,h,l?p:d]),m.fireWith(g,[E,C]),f&&(y.trigger("ajaxComplete",[E,h]),--w.active||w.event.trigger("ajaxStop")))}return E},getJSON:function(e,t,n){return w.get(e,t,n,"json")},getScript:function(e,t){return w.get(e,void 0,t,"script")}}),w.each(["get","post"],function(e,t){w[t]=function(e,n,r,i){return g(n)&&(i=i||r,r=n,n=void 0),w.ajax(w.extend({url:e,type:t,dataType:i,data:n,success:r},w.isPlainObject(e)&&e))}}),w._evalUrl=function(e){return w.ajax({url:e,type:"GET",dataType:"script",cache:!0,async:!1,global:!1,"throws":!0})},w.fn.extend({wrapAll:function(e){var t;return this[0]&&(g(e)&&(e=e.call(this[0])),t=w(e,this[0].ownerDocument).eq(0).clone(!0),this[0].parentNode&&t.insertBefore(this[0]),t.map(function(){var e=this;while(e.firstElementChild)e=e.firstElementChild;return e}).append(this)),this},wrapInner:function(e){return g(e)?this.each(function(t){w(this).wrapInner(e.call(this,t))}):this.each(function(){var t=w(this),n=t.contents();n.length?n.wrapAll(e):t.append(e)})},wrap:function(e){var t=g(e);return this.each(function(n){w(this).wrapAll(t?e.call(this,n):e)})},unwrap:function(e){return this.parent(e).not("body").each(function(){w(this).replaceWith(this.childNodes)}),this}}),w.expr.pseudos.hidden=function(e){return!w.expr.pseudos.visible(e)},w.expr.pseudos.visible=function(e){return!!(e.offsetWidth||e.offsetHeight||e.getClientRects().length)},w.ajaxSettings.xhr=function(){try{return new e.XMLHttpRequest}catch(e){}};var Vt={0:200,1223:204},Gt=w.ajaxSettings.xhr();h.cors=!!Gt&&"withCredentials"in Gt,h.ajax=Gt=!!Gt,w.ajaxTransport(function(t){var n,r;if(h.cors||Gt&&!t.crossDomain)return{send:function(i,o){var a,s=t.xhr();if(s.open(t.type,t.url,t.async,t.username,t.password),t.xhrFields)for(a in t.xhrFields)s[a]=t.xhrFields[a];t.mimeType&&s.overrideMimeType&&s.overrideMimeType(t.mimeType),t.crossDomain||i["X-Requested-With"]||(i["X-Requested-With"]="XMLHttpRequest");for(a in i)s.setRequestHeader(a,i[a]);n=function(e){return function(){n&&(n=r=s.onload=s.onerror=s.onabort=s.ontimeout=s.onreadystatechange=null,"abort"===e?s.abort():"error"===e?"number"!=typeof s.status?o(0,"error"):o(s.status,s.statusText):o(Vt[s.status]||s.status,s.statusText,"text"!==(s.responseType||"text")||"string"!=typeof s.responseText?{binary:s.response}:{text:s.responseText},s.getAllResponseHeaders()))}},s.onload=n(),r=s.onerror=s.ontimeout=n("error"),void 0!==s.onabort?s.onabort=r:s.onreadystatechange=function(){4===s.readyState&&e.setTimeout(function(){n&&r()})},n=n("abort");try{s.send(t.hasContent&&t.data||null)}catch(e){if(n)throw e}},abort:function(){n&&n()}}}),w.ajaxPrefilter(function(e){e.crossDomain&&(e.contents.script=!1)}),w.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/\b(?:java|ecma)script\b/},converters:{"text script":function(e){return w.globalEval(e),e}}}),w.ajaxPrefilter("script",function(e){void 0===e.cache&&(e.cache=!1),e.crossDomain&&(e.type="GET")}),w.ajaxTransport("script",function(e){if(e.crossDomain){var t,n;return{send:function(i,o){t=w("