commit 1559ce017d80eaef7ae6c220a10054bb9e831f1a Author: Carlos Arias Date: Sat Jul 4 22:53:10 2026 +0000 chore: scaffold SeedProject base (Phase 1) Clean-room copy of the reusable engines from comiida, with all instance data, secrets, dependencies, and build output excluded: - app/ Astro theme skeleton (no comiida blog posts; hero image -> placeholder) - api/ SeedProject PHP framework (no vendor/.env/config.php) - content-pipeline/ engine only (scripts/admin/prompts; empty runtime state) - astroagent.config.json + app/.astroagent/skills Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01SYHWLHihq3v9nxNwoPCKSn diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d7feb64 --- /dev/null +++ b/.gitignore @@ -0,0 +1,42 @@ +# ============================================================================ +# SeedProject base — cloneable Astro + PHP foundation +# Tracked: engine code (app/src, api framework, content-pipeline, astroagent). +# Ignored: secrets, dependencies, build output, and per-site runtime state. +# ============================================================================ + +# --- secrets (NEVER commit) --- +**/.env +api/config.php +api/system/.installed + +# --- dependencies (installed per site via scripts/new-site.sh) --- +node_modules/ +api/vendor/ + +# --- build output (regenerable) --- +app/dist/ +app/.astro/ +public/ +public-preview/ + +# --- logs --- +*.log + +# --- per-site runtime state (regenerated; dirs kept via .gitkeep) --- +content-pipeline/logs/* +!content-pipeline/logs/.gitkeep +content-pipeline/drafts/* +!content-pipeline/drafts/.gitkeep +content-pipeline/state/* +!content-pipeline/state/.gitkeep +content-pipeline/calendar.json +content-pipeline/messages.json +content-pipeline/news-queue.json +content-pipeline/news-scan.json +content-pipeline/research-scan.json + +# --- astroagent runtime workspaces --- +.astroagent/jobs/ +.astroagent/work/ +app/.astroagent/jobs/ +app/.astroagent/work/ diff --git a/api/.htaccess b/api/.htaccess new file mode 100644 index 0000000..42212ca --- /dev/null +++ b/api/.htaccess @@ -0,0 +1,12 @@ +RewriteEngine On + +#RewriteCond %{HTTPS} !=on +#RewriteRule ^(.*)$ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301,NE] +Header always set Content-Security-Policy "upgrade-insecure-requests;" + +RewriteCond %{REQUEST_FILENAME} !-d +RewriteCond %{REQUEST_FILENAME} !-f +RewriteCond %{REQUEST_FILENAME} !-l +RewriteRule ^(.+)$ index.php?url=$1 [QSA,L] + +# enable PHP error logging diff --git a/api/.memory/changelog.md b/api/.memory/changelog.md new file mode 100644 index 0000000..ee04f3c --- /dev/null +++ b/api/.memory/changelog.md @@ -0,0 +1,16 @@ +# SeedProject Changelog + +## 2026-04-27 — Cleanup and permissions + +- **`.claude/settings.local.json`** — added `COMPOSER_ALLOW_SUPERUSER=1 composer install` to allowed commands; reordered hooks block +- **`SeedProjectNaked.zip`** — removed stale zip artifact from repo + +## 2026-04-27 — Fix HTTP 500 on boot + +### PHP 8.3 Compatibility +- **`core/Database.php`** — renamed `query()` to `execQuery()` to resolve fatal signature mismatch with `PDO::query()` in PHP 8.3; updated internal calls in `retrieve()` and `pagination()` +- **`app/Components/Role.php`** — updated `deletePerm()` to call `execQuery()` instead of `query()` + +### Composer / Autoloader +- **`composer.json`** — created from scratch (was missing); added `autoload.classmap` for `core/`, `app/Helpers`, `app/Controllers`, `app/Components`, `app/Gateways`, `system/` so framework classes load via composer +- **`vendor/`** — ran `composer dump-autoload` to regenerate autoload files with proper classmap diff --git a/api/.memory/documentation.md b/api/.memory/documentation.md new file mode 100644 index 0000000..24fb858 --- /dev/null +++ b/api/.memory/documentation.md @@ -0,0 +1,2392 @@ +# Framework Infrastructure and Directory System + +This section provides a comprehensive overview of the framework's architecture, outlining the purpose of its main directories and the overall file structure. + +## Core Concepts + +The framework is organized into several key directories, each with a specific responsibility. This separation of concerns is crucial for maintaining a clean and scalable codebase. + +### `api/` - API Endpoints +_Handles all API requests in a controller-only MVC structure._ + +### `app/` - Application Core & Business Logic +_The heart of the application, replacing traditional models. Contains business logic, database interactions, and reusable components._ + +### `public/` - User Interface +_The document root for the application's UI, containing controllers, views, and static assets._ + +### `commands/` - CLI Tasks +_Houses command-line scripts for automated tasks, cron jobs, and internal server processing._ + +### `core/` - Framework Core +_Contains the foundational framework files that should rarely be modified._ + +## Directory Structure +``` +/www/wwwroot/(project_directory)/ +├── api/ +├── app/ +├── commands/ +├── core/ +├── plugins/ +├── public/ +├── templates/ +└── vendor/ +``` + +### Detailed `app/` Directory Structure +``` +app/ +├── Helpers/ +│ ├── DB.php # Database operations +│ ├── Session.php # Session management +│ ├── Validator.php # Input validation +│ └── Auth.php # Authentication helpers +├── Services/ +│ ├── EmailService.php +│ ├── PaymentService.php +│ └── NotificationService.php +├── Libraries/ +│ └── CustomAuth.php +└── Models/ # Optional: Domain models/entities + └── Client.php +``` + +**Example Helper:** +```php += $min; + } +} +``` + +**Example Service:** +```php + 1]); + +// Fetch single record +$client = Db::getRow("SELECT * FROM clients WHERE id = :id", [':id' => $id]); + +// Insert record +$clientId = Db::insert('clients', [ + 'name' => 'John Doe', + 'email' => 'john@example.com', + 'created_at' => date('Y-m-d H:i:s') +]); + +// Update record +$affected = Db::update('clients', + ['name' => 'Jane Doe', 'updated_at' => date('Y-m-d H:i:s')], + 'id = :id', + [':id' => $id] +); + +// Delete record +$affected = Db::delete('clients', 'id = :id', [':id' => $id]); + +// Execute custom query +Db::execute("UPDATE clients SET status = :status WHERE last_login < :date", [ + ':status' => 'inactive', + ':date' => date('Y-m-d', strtotime('-1 year')) +]); + +// Transactions +Db::beginTransaction(); +try { + Db::insert('orders', ['client_id' => $clientId, 'total' => 100]); + Db::update('clients', ['balance' => 'balance - 100'], 'id = :id', [':id' => $clientId]); + Db::commit(); +} catch (Exception $e) { + Db::rollback(); + throw $e; +} +``` + +### Common Controller Patterns +```php +// Render a view with data +$this->view->data['clients'] = $clients; +$this->view->render('clients/index', true, 'site'); + +// Render without wrapper (e.g., login page) +$this->view->render('auth/login', false); + +// Redirect +redirect('/clients'); + +// JSON response +header('Content-Type: application/json'); +echo json_encode(['success' => true, 'data' => $client]); + +// 404 response +http_response_code(404); +$this->view->render('errors/404', true, 'site'); +``` + +# Developer Workflows & Tutorials + +This section provides practical, step-by-step guides for common development tasks within the framework. + +## Request Lifecycle Overview + +Understanding the request lifecycle is key to knowing how the framework processes a request and generates a response. + +1. **Entry Point:** A user request first hits either `public/index.php` (for UI) or `api/index.php` (for API). +2. **Bootstrapping:** The `core/Bootstrap.php` file is initiated. It sets up the environment, loads the configuration from `config.php`, and initializes necessary services. +3. **Routing:** The incoming URL is processed by the routing engine. + * For simple URLs, **auto-routing** maps the URL directly to a `Controller/method` pair (e.g., `/clients/show` maps to `Clients::show()`). + * For complex URLs defined in the routing configuration, **AltoRouter** matches the pattern and executes the associated callback or controller. +4. **Controller Execution:** The matched controller method is called. +5. **Business Logic:** The controller interacts with the `app/` directory (e.g., calling helpers like `Db::select()`) to fetch data or perform business logic. +6. **Response Generation:** + * **UI (Public):** The controller calls `$this->view->render()`, passing data to a view file. The view is rendered within a wrapper (if specified), and the final HTML is sent to the browser. + * **API:** The controller typically returns data encoded as a JSON string and sets the appropriate `Content-Type` header. + +## Tutorial: Creating a New Page (UI) + +This tutorial walks through creating a "Clients" page that displays a list of clients. + +### Step 1: Create the Controller + +Create a new file at `public/controllers/Clients.php`. This controller will handle the logic for the clients page. +```php +view->data['clients'] = $clients; + $this->view->data['page_title'] = 'Our Clients'; + + // Render the view + $this->view->render('clients/index', true, 'site'); + } + + public function show($id) + { + // Fetch single client + $client = Db::getRow("SELECT * FROM clients WHERE id = :id", [':id' => $id]); + + if (!$client) { + http_response_code(404); + $this->view->render('errors/404', true, 'site'); + return; + } + + // Fetch related data + $orders = Db::select("SELECT * FROM orders WHERE client_id = :id ORDER BY created_at DESC", + [':id' => $id] + ); + + $this->view->data['client'] = $client; + $this->view->data['orders'] = $orders; + $this->view->data['page_title'] = $client['name']; + + $this->view->render('clients/show', true, 'site'); + } + + public function create() + { + // Show create form + $this->view->data['page_title'] = 'New Client'; + $this->view->render('clients/create', true, 'site'); + } + + public function store() + { + // Handle form submission + try { + // Validate input + if (!Validator::required($_POST['name'])) { + throw new Exception('Name is required'); + } + if (!Validator::email($_POST['email'])) { + throw new Exception('Valid email is required'); + } + + // Insert into database + $clientId = Db::insert('clients', [ + 'name' => $_POST['name'], + 'email' => $_POST['email'], + 'phone' => $_POST['phone'] ?? null, + 'created_at' => date('Y-m-d H:i:s') + ]); + + // Optional: Send welcome email + // EmailService::sendWelcomeEmail($_POST['email'], $_POST['name']); + + // Redirect to the new client's page + redirect('/clients/show/' . $clientId); + + } catch (Exception $e) { + // Handle errors + if (DEBUG) { + echo $e->getMessage(); + } else { + $this->view->data['error'] = 'Could not create client. Please try again.'; + $this->view->data['form_data'] = $_POST; + $this->view->render('clients/create', true, 'site'); + } + } + } + + public function edit($id) + { + $client = Db::getRow("SELECT * FROM clients WHERE id = :id", [':id' => $id]); + + if (!$client) { + http_response_code(404); + $this->view->render('errors/404', true, 'site'); + return; + } + + $this->view->data['client'] = $client; + $this->view->data['page_title'] = 'Edit ' . $client['name']; + $this->view->render('clients/edit', true, 'site'); + } + + public function update($id) + { + try { + // Validate + if (!Validator::required($_POST['name'])) { + throw new Exception('Name is required'); + } + + // Update + Db::update('clients', [ + 'name' => $_POST['name'], + 'email' => $_POST['email'], + 'phone' => $_POST['phone'] ?? null, + 'updated_at' => date('Y-m-d H:i:s') + ], 'id = :id', [':id' => $id]); + + redirect('/clients/show/' . $id); + + } catch (Exception $e) { + if (DEBUG) { + echo $e->getMessage(); + } else { + $this->view->data['error'] = 'Could not update client. Please try again.'; + $this->view->data['client'] = $_POST; + $this->view->data['client']['id'] = $id; + $this->view->render('clients/edit', true, 'site'); + } + } + } + + public function delete($id) + { + try { + Db::delete('clients', 'id = :id', [':id' => $id]); + redirect('/clients'); + } catch (Exception $e) { + if (DEBUG) { + echo $e->getMessage(); + } else { + redirect('/clients/show/' . $id . '?error=delete_failed'); + } + } + } +} +``` + +### Step 2: Create the Views + +Create view files at `public/views/clients/`: + +**Index View (`public/views/clients/index.php`):** +```php +
+

data['page_title']; ?>

+ + Add New Client + + data['clients'])): ?> +

No clients found.

+ + + + + + + + + + + + data['clients'] as $client): ?> + + + + + + + + +
NameEmailPhoneActions
+ View + Edit +
+ +
+``` + +**Show View (`public/views/clients/show.php`):** +```php +
+

data['client']['name']); ?>

+ +
+

Email: data['client']['email']); ?>

+

Phone: data['client']['phone'] ?? '-'); ?>

+
+ + + +

Orders

+ data['orders'])): ?> +

No orders yet.

+ + + + + + + + + + + data['orders'] as $order): ?> + + + + + + + +
Order IDTotalDate
#$
+ +
+``` + +**Create/Edit View (`public/views/clients/create.php`):** +```php +
+

data['page_title']; ?>

+ + data['error'])): ?> +
+ data['error']); ?> +
+ + +
+
+ + +
+ +
+ + +
+ +
+ + +
+ + + Cancel +
+
+``` + +### Step 3: Create Page-Specific Assets + +Create the corresponding CSS and JavaScript files for this page. + +* `public/assets/css/clients.css` +* `public/assets/js/clients.js` + +These will be loaded by the site's wrapper. + +### Step 4: Access Your New Page + +You can now access your new page by navigating to: +- `http://yoursite.com/clients` - List all clients +- `http://yoursite.com/clients/show/1` - View client #1 +- `http://yoursite.com/clients/create` - Create new client form +- `http://yoursite.com/clients/edit/1` - Edit client #1 + +The auto-routing will automatically map these URLs to the corresponding methods in your `Clients` controller. + +## Tutorial: Creating a New API Endpoint + +This tutorial shows how to create API endpoints for product management. + +### Step 1: Create the API Controller + +Create a new file at `api/controllers/Products.php`. +```php + true, + 'data' => $products, + 'count' => count($products) + ]); + + } catch (Exception $e) { + http_response_code(500); + echo json_encode([ + 'success' => false, + 'error' => DEBUG ? $e->getMessage() : 'Internal server error' + ]); + } + } + + /** + * GET /api/products/get/{id} + * Get a single product by ID + */ + public function get($id) + { + header('Content-Type: application/json'); + + try { + $product = Db::getRow("SELECT * FROM products WHERE id = :id", [':id' => $id]); + + if ($product) { + echo json_encode([ + 'success' => true, + 'data' => $product + ]); + } else { + http_response_code(404); + echo json_encode([ + 'success' => false, + 'error' => 'Product not found' + ]); + } + + } catch (Exception $e) { + http_response_code(500); + echo json_encode([ + 'success' => false, + 'error' => DEBUG ? $e->getMessage() : 'Internal server error' + ]); + } + } + + /** + * POST /api/products/create + * Create a new product + */ + public function create() + { + header('Content-Type: application/json'); + + try { + // Get JSON input + $input = json_decode(file_get_contents('php://input'), true); + + // Validate + if (!isset($input['name']) || !isset($input['price'])) { + http_response_code(400); + echo json_encode([ + 'success' => false, + 'error' => 'Name and price are required' + ]); + return; + } + + // Insert + $productId = Db::insert('products', [ + 'name' => $input['name'], + 'description' => $input['description'] ?? null, + 'price' => $input['price'], + 'category' => $input['category'] ?? null, + 'created_at' => date('Y-m-d H:i:s') + ]); + + http_response_code(201); + echo json_encode([ + 'success' => true, + 'data' => ['id' => $productId] + ]); + + } catch (Exception $e) { + http_response_code(500); + echo json_encode([ + 'success' => false, + 'error' => DEBUG ? $e->getMessage() : 'Internal server error' + ]); + } + } + + /** + * PUT /api/products/update/{id} + * Update an existing product + */ + public function update($id) + { + header('Content-Type: application/json'); + + try { + // Check if product exists + $product = Db::getRow("SELECT * FROM products WHERE id = :id", [':id' => $id]); + + if (!$product) { + http_response_code(404); + echo json_encode([ + 'success' => false, + 'error' => 'Product not found' + ]); + return; + } + + // Get JSON input + $input = json_decode(file_get_contents('php://input'), true); + + // Build update data + $updateData = []; + if (isset($input['name'])) $updateData['name'] = $input['name']; + if (isset($input['description'])) $updateData['description'] = $input['description']; + if (isset($input['price'])) $updateData['price'] = $input['price']; + if (isset($input['category'])) $updateData['category'] = $input['category']; + $updateData['updated_at'] = date('Y-m-d H:i:s'); + + // Update + Db::update('products', $updateData, 'id = :id', [':id' => $id]); + + echo json_encode([ + 'success' => true, + 'data' => ['id' => $id] + ]); + + } catch (Exception $e) { + http_response_code(500); + echo json_encode([ + 'success' => false, + 'error' => DEBUG ? $e->getMessage() : 'Internal server error' + ]); + } + } + + /** + * DELETE /api/products/delete/{id} + * Delete a product + */ + public function delete($id) + { + header('Content-Type: application/json'); + + try { + // Check if product exists + $product = Db::getRow("SELECT * FROM products WHERE id = :id", [':id' => $id]); + + if (!$product) { + http_response_code(404); + echo json_encode([ + 'success' => false, + 'error' => 'Product not found' + ]); + return; + } + + // Delete + Db::delete('products', 'id = :id', [':id' => $id]); + + echo json_encode([ + 'success' => true, + 'data' => ['id' => $id] + ]); + + } catch (Exception $e) { + http_response_code(500); + echo json_encode([ + 'success' => false, + 'error' => DEBUG ? $e->getMessage() : 'Internal server error' + ]); + } + } +} +``` + +### Step 2: Access the API Endpoints + +You can now make requests to these endpoints: + +**Example using cURL:** +```sh +# List all products +curl http://yoursite.com/api/products/list + +# List products with filtering +curl "http://yoursite.com/api/products/list?category=electronics&limit=10" + +# Get product with ID 123 +curl http://yoursite.com/api/products/get/123 + +# Create a new product +curl -X POST http://yoursite.com/api/products/create \ + -H "Content-Type: application/json" \ + -d '{"name":"New Product","price":29.99,"category":"electronics"}' + +# Update product +curl -X PUT http://yoursite.com/api/products/update/123 \ + -H "Content-Type: application/json" \ + -d '{"price":24.99}' + +# Delete product +curl -X DELETE http://yoursite.com/api/products/delete/123 +``` + +If `SECUREAPI` is enabled in `config.php`, you must include the authorization token in the header: +```sh +# Get the authorization key from the AUTHORIZATION define in config.php +API_KEY="your_secret_api_key" + +# Example with authorization +curl -H "Authorization: $API_KEY" http://yoursite.com/api/products/get/123 +``` + +**Example using JavaScript (Fetch API):** +```javascript +// GET request +fetch('/api/products/list?category=electronics') + .then(response => response.json()) + .then(data => console.log(data)); + +// POST request +fetch('/api/products/create', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + // Include if SECUREAPI is enabled + // 'Authorization': 'your_secret_api_key' + }, + body: JSON.stringify({ + name: 'New Product', + price: 29.99, + category: 'electronics' + }) +}) + .then(response => response.json()) + .then(data => console.log(data)); +``` + +## Tutorial: Creating and Running a CLI Command + +This tutorial shows how to create command-line tasks for various purposes. + +### Step 1: Create the Command File + +**Simple Command (`commands/SyncDataCommand.php`):** +```php + $record['id']] + ); + + if ($exists && !$forceSync) { + echo "Skipping existing record: {$record['id']}\n"; + continue; + } + + if ($exists) { + // Update existing + Db::update('synced_data', [ + 'data' => json_encode($record), + 'synced_at' => date('Y-m-d H:i:s') + ], 'external_id = :id', [':id' => $record['id']]); + echo "Updated record: {$record['id']}\n"; + } else { + // Insert new + Db::insert('synced_data', [ + 'external_id' => $record['id'], + 'data' => json_encode($record), + 'synced_at' => date('Y-m-d H:i:s') + ]); + echo "Inserted record: {$record['id']}\n"; + } + } + + echo "\nData sync completed successfully.\n"; + +} catch (Exception $e) { + echo "ERROR: " . $e->getMessage() . "\n"; + exit(1); +} +``` + +**Email Queue Processor (`commands/ProcessEmailQueueCommand.php`):** +```php + (int)$limit] + ); + + echo "Found " . count($emails) . " pending emails\n"; + + $sent = 0; + $failed = 0; + + foreach ($emails as $email) { + try { + // Send email using your EmailService + EmailService::send( + $email['to'], + $email['subject'], + $email['body'] + ); + + // Update status + Db::update('email_queue', [ + 'status' => 'sent', + 'sent_at' => date('Y-m-d H:i:s') + ], 'id = :id', [':id' => $email['id']]); + + $sent++; + echo "."; + + } catch (Exception $e) { + // Mark as failed + Db::update('email_queue', [ + 'status' => 'failed', + 'error_message' => $e->getMessage(), + 'failed_at' => date('Y-m-d H:i:s') + ], 'id = :id', [':id' => $email['id']]); + + $failed++; + echo "F"; + } + } + + echo "\n\nResults:\n"; + echo "Sent: $sent\n"; + echo "Failed: $failed\n"; + +} catch (Exception $e) { + echo "ERROR: " . $e->getMessage() . "\n"; + exit(1); +} +``` + +**Database Cleanup Command (`commands/CleanupDatabaseCommand.php`):** +```php + $cutoffDate] + ); + echo "Logs to delete: " . $oldLogs[0]['count'] . "\n"; + + if (!$dryRun) { + $deleted = Db::delete('logs', 'created_at < :date', [':date' => $cutoffDate]); + echo "Deleted: $deleted logs\n"; + } + + // Clean soft-deleted records + $oldDeleted = Db::select("SELECT COUNT(*) as count FROM clients WHERE deleted_at IS NOT NULL AND deleted_at < :date", + [':date' => $cutoffDate] + ); + echo "\nSoft-deleted clients to remove: " . $oldDeleted[0]['count'] . "\n"; + + if (!$dryRun) { + $deleted = Db::delete('clients', 'deleted_at IS NOT NULL AND deleted_at < :date', + [':date' => $cutoffDate] + ); + echo "Deleted: $deleted clients\n"; + } + + echo "\nCleanup completed.\n"; + +} catch (Exception $e) { + echo "ERROR: " . $e->getMessage() . "\n"; + exit(1); +} +``` + +### Step 2: Run the Commands + +Execute commands from the root directory of your project using the `console` entry point. +```sh +# Run data sync +php console SyncDataCommand + +# Run with options +php console SyncDataCommand --date="2024-10-28" --force + +# Process email queue +php console ProcessEmailQueueCommand --limit=50 + +# Database cleanup (dry run first) +php console CleanupDatabaseCommand --days=60 --dry-run + +# Then run for real +php console CleanupDatabaseCommand --days=60 +``` + +### Step 3: Set Up Cron Jobs + +Add these to your crontab for automated execution: +```sh +# Edit crontab +crontab -e + +# Add these lines: +# Process email queue every 5 minutes +*/5 * * * * cd /www/wwwroot/(project_directory) && php console ProcessEmailQueueCommand >> /var/log/email-queue.log 2>&1 + +# Sync data every hour +0 * * * * cd /www/wwwroot/(project_directory) && php console SyncDataCommand >> /var/log/data-sync.log 2>&1 + +# Cleanup database every night at 2 AM +0 2 * * * cd /www/wwwroot/(project_directory) && php console CleanupDatabaseCommand --days=30 >> /var/log/cleanup.log 2>&1 +``` + +## Error Handling Patterns + +### Controller Error Handling +```php +validateClientData($_POST); + + // Insert client + $clientId = Db::insert('clients', [ + 'name' => $_POST['name'], + 'email' => $_POST['email'] + ]); + + // Insert related data + Db::insert('client_metadata', [ + 'client_id' => $clientId, + 'source' => $_POST['source'] ?? 'website' + ]); + + // Commit transaction + Db::commit(); + + // Send notification email + try { + EmailService::sendWelcomeEmail($_POST['email'], $_POST['name']); + } catch (Exception $e) { + // Log email failure but don't fail the whole operation + error_log("Failed to send welcome email: " . $e->getMessage()); + } + + redirect('/clients/show/' . $clientId); + + } catch (ValidationException $e) { + // Validation errors - show to user + Db::rollback(); + $this->view->data['error'] = $e->getMessage(); + $this->view->data['form_data'] = $_POST; + $this->view->render('clients/create', true, 'site'); + + } catch (Exception $e) { + // Other errors + Db::rollback(); + + if (DEBUG) { + // Show full error in development + echo "
";
+                echo "Error: " . $e->getMessage() . "\n";
+                echo "File: " . $e->getFile() . "\n";
+                echo "Line: " . $e->getLine() . "\n";
+                echo "\nStack Trace:\n" . $e->getTraceAsString();
+                echo "
"; + } else { + // Generic error in production + $this->view->data['error'] = 'An error occurred. Please try again.'; + $this->view->data['form_data'] = $_POST; + $this->view->render('clients/create', true, 'site'); + + // Log the error + error_log("Client creation failed: " . $e->getMessage()); + } + } + } + + private function validateClientData($data) + { + if (!Validator::required($data['name'] ?? '')) { + throw new ValidationException('Name is required'); + } + + if (!Validator::email($data['email'] ?? '')) { + throw new ValidationException('Valid email is required'); + } + + // Check for duplicate email + $existing = Db::getRow("SELECT id FROM clients WHERE email = :email", + [':email' => $data['email']] + ); + + if ($existing) { + throw new ValidationException('A client with this email already exists'); + } + } +} + +// Custom exception for validation errors +class ValidationException extends Exception {} +``` + +### API Error Handling +```php +validateProduct($input); + + $productId = Db::insert('products', [ + 'name' => $input['name'], + 'price' => $input['price'], + 'created_at' => date('Y-m-d H:i:s') + ]); + + http_response_code(201); + echo json_encode([ + 'success' => true, + 'data' => ['id' => $productId] + ]); + + } catch (ApiException $e) { + http_response_code($e->getCode()); + echo json_encode([ + 'success' => false, + 'error' => $e->getMessage() + ]); + + } catch (Exception $e) { + http_response_code(500); + echo json_encode([ + 'success' => false, + 'error' => DEBUG ? $e->getMessage() : 'Internal server error' + ]); + + error_log("API Error: " . $e->getMessage()); + } + } + + private function validateProduct($data) + { + if (!isset($data['name']) || empty(trim($data['name']))) { + throw new ApiException('Product name is required', 400); + } + + if (!isset($data['price']) || !is_numeric($data['price'])) { + throw new ApiException('Valid price is required', 400); + } + + if ($data['price'] < 0) { + throw new ApiException('Price cannot be negative', 400); + } + } +} + +// Custom exception for API errors +class ApiException extends Exception { + public function __construct($message, $code = 400) { + parent::__construct($message, $code); + } +} +``` + +### Command Error Handling +```php + $order['id'], + 'error' => $e->getMessage() + ]; + echo "E"; + } + } + + echo "\n\n"; + echo "Processed: $processed\n"; + echo "Errors: " . count($errors) . "\n"; + + if (!empty($errors)) { + echo "\nFailed Orders:\n"; + foreach ($errors as $error) { + echo "Order #{$error['order_id']}: {$error['error']}\n"; + } + exit(1); // Exit with error code + } + +} catch (Exception $e) { + echo "\nFATAL ERROR: " . $e->getMessage() . "\n"; + exit(1); +} + +function processOrder($order) { + // Order processing logic + // Throw exceptions on failures +} +``` + +# Common Configurations & Features + +## `config.php` - Framework Configuration + +The `config.php` file is the central hub for all framework-wide configurations. It handles security-related keys, API keys, debugging settings, and various integration parameters. + +### Security Configurations + +It is **highly recommended** to change all security-related values for each new project. + +* **`define('DEBUG', false);`**: Set to `true` to enable error reporting for development. **Always set to `false` in production.** +* **`define('SECUREAPI', false);`**: Set to `true` to require an authorization key for API access. +* **`define('AUTHORIZATION', 'your_secret_api_key');`**: The secret key required when `SECUREAPI` is true. **Always change this to a strong, random value.** + +**Example: Generating a secure API key** +```php +// Generate a random API key +define('AUTHORIZATION', bin2hex(random_bytes(32))); +``` + +### Database Configuration +```php +// Database settings +define('DB_HOST', 'localhost'); +define('DB_NAME', 'your_database'); +define('DB_USER', 'your_username'); +define('DB_PASS', 'your_password'); +define('DB_CHARSET', 'utf8mb4'); +``` + +### API Keys and Integrations + +All third-party API keys (Stripe, PHPMailer, etc.) are managed in this file, making them globally accessible via their defined constants. +```php +// Stripe +define('STRIPE_SECRET_KEY', 'sk_test_...'); +define('STRIPE_PUBLIC_KEY', 'pk_test_...'); + +// Email (PHPMailer) +define('SMTP_HOST', 'smtp.gmail.com'); +define('SMTP_PORT', 587); +define('SMTP_USER', 'your-email@gmail.com'); +define('SMTP_PASS', 'your-app-password'); +define('SMTP_FROM', 'noreply@yoursite.com'); +define('SMTP_FROM_NAME', 'Your Site Name'); + +// AWS S3 +define('AWS_KEY', 'your-aws-key'); +define('AWS_SECRET', 'your-aws-secret'); +define('AWS_REGION', 'us-east-1'); +define('AWS_BUCKET', 'your-bucket-name'); + +// Other services +define('RECAPTCHA_SITE_KEY', 'your-site-key'); +define('RECAPTCHA_SECRET_KEY', 'your-secret-key'); +``` + +### Path Constants +```php +// Define base paths +define('ROOT_PATH', __DIR__); +define('APP_PATH', ROOT_PATH . '/app'); +define('PUBLIC_PATH', ROOT_PATH . '/public'); +define('TEMPLATES_PATH', ROOT_PATH . '/templates'); +define('UPLOADS_PATH', PUBLIC_PATH . '/uploads'); +``` + +### Environment-Specific Configuration +```php +// Determine environment +define('ENVIRONMENT', getenv('APP_ENV') ?: 'production'); + +// Environment-specific settings +if (ENVIRONMENT === 'development') { + define('DEBUG', true); + define('BASE_URL', 'http://localhost:8000'); +} else { + define('DEBUG', false); + define('BASE_URL', 'https://yoursite.com'); +} +``` + +## .htaccess and SSL + +The root `.htaccess` file includes a rule for automatic redirection to HTTPS. It is recommended to temporarily disable this rule when setting up a new SSL certificate (e.g., with Let's Encrypt) to avoid issues with the verification process. +```apache +# Force HTTPS +# Comment out these lines when setting up SSL certificate +RewriteCond %{HTTPS} off +RewriteRule ^(.*)$ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301] + +# Standard routing +RewriteEngine On +RewriteCond %{REQUEST_FILENAME} !-f +RewriteCond %{REQUEST_FILENAME} !-d +RewriteRule ^(.*)$ index.php [QSA,L] +``` + +## Public Assets Structure + +The `public/assets` directory is organized to promote performance and streamline debugging by using page-specific CSS and JavaScript files. +``` +public/assets/ +├── css/ +│ ├── global.css # Site-wide styles +│ ├── clients.css # Page-specific: clients +│ ├── products.css # Page-specific: products +│ └── dashboard.css # Page-specific: dashboard +├── js/ +│ ├── global.js # Site-wide scripts +│ ├── clients.js # Page-specific: clients +│ ├── products.js # Page-specific: products +│ └── dashboard.js # Page-specific: dashboard +└── images/ + ├── logo.png + └── icons/ +``` + +**Wrapper automatically loads page-specific assets:** +```php + + +getControllerName()); +if (file_exists(PUBLIC_PATH . "/assets/css/{$controller}.css")) { + echo ""; +} +?> +``` + +## View System + +### Wrappers (Header & Footer) + +A "wrapper" is the main site template (`header.php` and `footer.php`). You can control which wrapper to use or disable it entirely, which is useful for pages like a login screen. + +**Wrapper Location:** +``` +templates/wrappers/ +├── site/ # Main site wrapper +│ ├── header.php +│ └── footer.php +├── admin/ # Admin panel wrapper +│ ├── header.php +│ └── footer.php +└── minimal/ # Minimal wrapper (login, etc.) + ├── header.php + └── footer.php +``` + +**Usage Examples:** +```php +// Use default 'site' wrapper +$this->view->render('clients/index', true, 'site'); + +// Use admin wrapper +$this->view->render('admin/dashboard', true, 'admin'); + +// Use minimal wrapper (login page) +$this->view->render('auth/login', true, 'minimal'); + +// No wrapper at all +$this->view->render('auth/login', false); +``` + +**Example Wrapper (`templates/wrappers/site/header.php`):** +```php + + + + + + <?php echo $this->data['page_title'] ?? 'My Site'; ?> + + + + + + getControllerName()); + if (file_exists(PUBLIC_PATH . "/assets/css/{$controller}.css")) { + echo ""; + } + ?> + + + + +
+``` + +**Example Footer (`templates/wrappers/site/footer.php`):** +```php +
+ +
+

© My Site

+
+ + + + + + getControllerName()); + if (file_exists(PUBLIC_PATH . "/assets/js/{$controller}.js")) { + echo ""; + } + ?> + + +``` + +### Partial Views + +Partial views allow you to load reusable HTML snippets (like sidebars or breadcrumbs) into other views, similar to `require_once()`. + +**Partial Location:** +``` +public/views/ +├── _partials/ +│ ├── breadcrumb.php +│ ├── sidebar.php +│ ├── pagination.php +│ └── client_card.php +``` + +**Usage in Views:** +```php + +
+ partial('_partials/breadcrumb', [ + 'Home' => '/', + 'Clients' => '/clients', + $this->data['client']['name'] => '' + ]); ?> + +
+
+

data['client']['name']); ?>

+ +
+ +
+ partial('_partials/sidebar'); ?> +
+
+
+``` + +**Example Partial (`public/views/_partials/breadcrumb.php`):** +```php + +``` + +## Routing + +The framework uses auto-routing by convention (`/controller/method`) for most cases. For more complex needs, it integrates `AltoRouter`. + +### Auto-Routing Examples +``` +URL: /clients +Maps to: Clients::index() + +URL: /clients/show/123 +Maps to: Clients::show(123) + +URL: /products/edit/456 +Maps to: Products::edit(456) +``` + +### AltoRouter for Complex Routes + +Define custom routes in `public/routes.php` or `api/routes.php`: +```php +map('GET', '/users/[i:id]', 'Users#show', 'user_show'); + +// Multiple parameters +$router->map('GET', '/blog/[i:year]/[i:month]/[*:slug]', 'Blog#show', 'blog_post'); + +// POST route +$router->map('POST', '/api/users/create', 'UsersAPI#create', 'api_user_create'); + +// Optional parameter +$router->map('GET', '/products/category/[a:category]?', 'Products#category', 'products_category'); +``` + +**Match Types:** +* **`[i:id]`**: Match an integer as parameter 'id' +* **`[a:action]`**: Match alphanumeric characters (A-Z, a-z, 0-9, -) +* **`[h:key]`**: Match hexadecimal characters +* **`[*:trailing]`**: Catch-all for the rest of the URL path +* **`[**:path]`**: Match everything including slashes + +**Using Named Routes:** +```php +// Generate URL for a named route +echo $router->generate('user_show', ['id' => 5]); +// Outputs: /users/5 + +echo $router->generate('blog_post', [ + 'year' => 2024, + 'month' => 10, + 'slug' => 'my-first-post' +]); +// Outputs: /blog/2024/10/my-first-post +``` + +**In Controllers:** +```php + $id]); + + $this->view->data['user'] = $user; + $this->view->render('users/show', true, 'site'); + } +} +``` + +## `templates/` - Reusable HTML Documents + +This directory holds reusable HTML documents, such as transactional emails (`/templates/emails/`). These templates use placeholders like `%FNAME%` that are replaced with dynamic data by PHP before being sent. + +**Template Structure:** +``` +templates/ +├── emails/ +│ ├── welcome.html +│ ├── password_reset.html +│ ├── invoice.html +│ └── notification.html +└── wrappers/ + ├── site/ + ├── admin/ + └── minimal/ +``` + +**Example Email Template (`templates/emails/welcome.html`):** +```html + + + + + + +
+
+

Welcome to %SITE_NAME%

+
+
+

Hi %FNAME%,

+

Thank you for signing up! We're excited to have you on board.

+

Your account email is: %EMAIL%

+

+ Verify Your Email +

+

Thanks,
The %SITE_NAME% Team

+
+
+ + +``` + +**Using Templates in Code:** +```php + $firstName, + '%EMAIL%' => $email, + '%SITE_NAME%' => 'My Awesome Site', + '%VERIFY_LINK%' => BASE_URL . '/verify?token=' . self::generateToken($email) + ]; + + $body = str_replace( + array_keys($replacements), + array_values($replacements), + $template + ); + + // Send email (using PHPMailer or other) + return self::send($email, 'Welcome to My Awesome Site', $body); + } + + private static function send($to, $subject, $body) + { + // PHPMailer implementation + $mail = new PHPMailer(true); + + try { + $mail->isSMTP(); + $mail->Host = SMTP_HOST; + $mail->SMTPAuth = true; + $mail->Username = SMTP_USER; + $mail->Password = SMTP_PASS; + $mail->SMTPSecure = PHPMailer::ENCRYPTION_STARTTLS; + $mail->Port = SMTP_PORT; + + $mail->setFrom(SMTP_FROM, SMTP_FROM_NAME); + $mail->addAddress($to); + + $mail->isHTML(true); + $mail->Subject = $subject; + $mail->Body = $body; + + $mail->send(); + return true; + + } catch (Exception $e) { + error_log("Email send failed: " . $mail->ErrorInfo); + return false; + } + } +} +``` + +## `plugins/` - Extendable Functionality (In Development) + +This directory is for modular, self-contained add-ons (like a CRM or weather widget) that can extend the framework's functionality, similar to WordPress plugins. + +**Planned Structure:** +``` +plugins/ +├── crm/ +│ ├── plugin.php # Plugin initialization +│ ├── controllers/ +│ ├── views/ +│ ├── assets/ +│ └── README.md +├── weather-widget/ +│ ├── plugin.php +│ ├── WeatherWidget.php +│ └── assets/ +└── analytics/ + ├── plugin.php + ├── services/ + └── config.php +``` + +# Core Classes Reference + +## Controller (`core/Controller.php`) + +Base controller class that all public controllers extend. + +**Properties:** +- `$this->view` - View renderer instance + +**Methods:** +- `redirect($url)` - Perform HTTP redirect +- `getControllerName()` - Get current controller name +- `getMethodName()` - Get current method name + +**Example:** +```php +data in the view + $this->view->data['clients'] = Db::select("SELECT * FROM clients"); + $this->view->render('clients/index', true, 'site'); + } + + protected function requireAuth() { + if (!Auth::isLoggedIn()) { + $this->redirect('/login'); + } + } +} +``` + +## View (`core/View.php`) + +Handles view rendering and data passing. + +**Properties:** +- `$this->data` - Array of variables to pass to view + +**Methods:** +- `render($viewPath, $useWrapper = true, $wrapperName = 'site')` - Render a view +- `partial($partialPath, $data = [])` - Load a partial view +- `getControllerName()` - Get current controller name (for asset loading) + +**Example:** +```php +// In controller +$this->view->data['title'] = 'My Page'; +$this->view->data['users'] = $users; +$this->view->render('users/index', true, 'site'); + +// In view +

data['title']; ?>

+data['users'] as $user): ?> +

+ + +// Using partials +partial('_partials/breadcrumb', [ + 'breadcrumb' => ['Home' => '/', 'Users' => ''] +]); ?> +``` + +## Db (`app/Helpers/DB.php`) + +Static database helper class for all database operations. + +**Connection Methods:** +- `Db::getConnection()` - Get PDO connection instance +- `Db::beginTransaction()` - Start transaction +- `Db::commit()` - Commit transaction +- `Db::rollback()` - Rollback transaction + +**Query Methods:** +- `Db::select($query, $params = [])` - Execute SELECT and return all rows +- `Db::getRow($query, $params = [])` - Execute SELECT and return first row +- `Db::execute($query, $params = [])` - Execute any query +- `Db::insert($table, $data)` - Insert row and return last insert ID +- `Db::update($table, $data, $where, $whereParams = [])` - Update rows and return affected count +- `Db::delete($table, $where, $whereParams = [])` - Delete rows and return affected count + +**Examples:** +```php +// Select multiple rows +$clients = Db::select("SELECT * FROM clients WHERE active = :active", [':active' => 1]); + +// Select single row +$client = Db::getRow("SELECT * FROM clients WHERE id = :id", [':id' => $id]); + +// Insert +$clientId = Db::insert('clients', [ + 'name' => 'John Doe', + 'email' => 'john@example.com', + 'created_at' => date('Y-m-d H:i:s') +]); + +// Update +$affected = Db::update('clients', + ['name' => 'Jane Doe', 'updated_at' => date('Y-m-d H:i:s')], + 'id = :id', + [':id' => $id] +); + +// Delete +$affected = Db::delete('clients', 'id = :id', [':id' => $id]); + +// Custom query +Db::execute("UPDATE clients SET last_login = NOW() WHERE id = :id", [':id' => $id]); + +// Transactions +Db::beginTransaction(); +try { + Db::insert('orders', ['client_id' => 1, 'total' => 100]); + Db::update('clients', ['balance' => 'balance - 100'], 'id = 1'); + Db::commit(); +} catch (Exception $e) { + Db::rollback(); + throw $e; +} +``` + +## Helper Classes + +### Validator (`app/Helpers/Validator.php`) + +Input validation helper. + +**Methods:** +```php +Validator::required($value) // Check if not empty +Validator::email($value) // Validate email format +Validator::minLength($value, $min) // Check minimum length +Validator::maxLength($value, $max) // Check maximum length +Validator::numeric($value) // Check if numeric +Validator::alpha($value) // Check if alphabetic +Validator::alphanumeric($value) // Check if alphanumeric +Validator::url($value) // Validate URL format +Validator::date($value) // Validate date format +``` + +### Session (`app/Helpers/Session.php`) + +Session management helper. + +**Methods:** +```php +Session::start() // Start session +Session::set($key, $value) // Set session variable +Session::get($key, $default = null) // Get session variable +Session::has($key) // Check if key exists +Session::delete($key) // Delete session variable +Session::destroy() // Destroy session +Session::flash($key, $value) // Set flash message +Session::getFlash($key) // Get and delete flash message +``` + +### Auth (`app/Helpers/Auth.php`) + +Authentication helper. + +**Methods:** +```php +Auth::login($userId) // Log in user +Auth::logout() // Log out user +Auth::isLoggedIn() // Check if user is logged in +Auth::getUserId() // Get current user ID +Auth::getUser() // Get current user data +Auth::check($permission) // Check user permission +``` + +# Database Usage + +The `Db` helper class (`app/Helpers/DB.php`) provides a static interface for all database interactions. All queries use prepared statements for security. + +## Basic Queries + +### SELECT - Multiple Rows +```php +// Simple select +$clients = Db::select("SELECT * FROM clients"); + +// With WHERE clause +$activeClients = Db::select( + "SELECT * FROM clients WHERE active = :active", + [':active' => 1] +); + +// With multiple conditions +$clients = Db::select( + "SELECT * FROM clients WHERE status = :status AND created_at > :date ORDER BY name", + [':status' => 'active', ':date' => '2024-01-01'] +); + +// With JOIN +$orders = Db::select( + "SELECT o.*, c.name as client_name + FROM orders o + JOIN clients c ON o.client_id = c.id + WHERE o.status = :status", + [':status' => 'pending'] +); +``` + +### SELECT - Single Row +```php +// Get single row +$client = Db::getRow("SELECT * FROM clients WHERE id = :id", [':id' => $id]); + +// Check if exists +if ($client) { + echo $client['name']; +} else { + echo "Client not found"; +} + +// Get specific columns +$email = Db::getRow("SELECT email FROM clients WHERE id = :id", [':id' => $id]); +``` + +## INSERT Operations +```php +// Simple insert +$clientId = Db::insert('clients', [ + 'name' => 'John Doe', + 'email' => 'john@example.com', + 'phone' => '555-1234' +]); + +// Insert with timestamp +$clientId = Db::insert('clients', [ + 'name' => 'Jane Doe', + 'email' => 'jane@example.com', + 'created_at' => date('Y-m-d H:i:s'), + 'updated_at' => date('Y-m-d H:i:s') +]); + +// Insert and use the ID +$orderId = Db::insert('orders', [ + 'client_id' => $clientId, + 'total' => 99.99, + 'status' => 'pending' +]); +``` + +## UPDATE Operations +```php +// Update single record +$affected = Db::update('clients', + ['name' => 'John Smith', 'updated_at' => date('Y-m-d H:i:s')], + 'id = :id', + [':id' => $id] +); + +// Update multiple records +$affected = Db::update('orders', + ['status' => 'shipped', 'shipped_at' => date('Y-m-d H:i:s')], + 'status = :old_status AND created_at < :date', + [':old_status' => 'pending', ':date' => '2024-01-01'] +); + +// Increment value +Db::execute( + "UPDATE products SET view_count = view_count + 1 WHERE id = :id", + [':id' => $productId] +); +``` + +## DELETE Operations +```php +// Delete single record +$affected = Db::delete('clients', 'id = :id', [':id' => $id]); + +// Delete multiple records +$affected = Db::delete('logs', 'created_at < :date', [':date' => '2024-01-01']); + +// Soft delete (recommended) +Db::update('clients', + ['deleted_at' => date('Y-m-d H:i:s')], + 'id = :id', + [':id' => $id] +); +``` + +## Transactions + +Use transactions for operations that must all succeed or all fail together. +```php +// Simple transaction +Db::beginTransaction(); +try { + // Create order + $orderId = Db::insert('orders', [ + 'client_id' => $clientId, + 'total' => 100.00, + 'status' => 'pending' + ]); + + // Deduct from client balance + Db::execute( + "UPDATE clients SET balance = balance - :amount WHERE id = :id", + [':amount' => 100.00, ':id' => $clientId] + ); + + // Insert order items + foreach ($items as $item) { + Db::insert('order_items', [ + 'order_id' => $orderId, + 'product_id' => $item['product_id'], + 'quantity' => $item['quantity'], + 'price' => $item['price'] + ]); + } + + Db::commit(); + echo "Order created successfully"; + +} catch (Exception $e) { + Db::rollback(); + echo "Order failed: " . $e->getMessage(); +} +``` + +## Complex Queries + +### Aggregation +```php +// COUNT +$count = Db::getRow("SELECT COUNT(*) as total FROM clients WHERE active = 1"); +echo $count['total']; + +// SUM, AVG, etc. +$stats = Db::getRow(" + SELECT + COUNT(*) as order_count, + SUM(total) as revenue, + AVG(total) as avg_order + FROM orders + WHERE status = 'completed' +"); +``` + +### Subqueries +```php +$clients = Db::select(" + SELECT c.*, + (SELECT COUNT(*) FROM orders WHERE client_id = c.id) as order_count, + (SELECT SUM(total) FROM orders WHERE client_id = c.id) as total_spent + FROM clients c + WHERE c.active = 1 +"); +``` + +### GROUP BY +```php +$monthlySales = Db::select(" + SELECT + DATE_FORMAT(created_at, '%Y-%m') as month, + COUNT(*) as order_count, + SUM(total) as revenue + FROM orders + WHERE status = 'completed' + GROUP BY DATE_FORMAT(created_at, '%Y-%m') + ORDER BY month DESC +"); +``` + +### CASE Statements +```php +$clients = Db::select(" + SELECT + name, + CASE + WHEN total_spent > 10000 THEN 'VIP' + WHEN total_spent > 5000 THEN 'Premium' + ELSE 'Standard' + END as tier + FROM clients +"); +``` + +## Raw Queries with execute() + +For queries that don't fit insert/update/delete patterns: +```php +// Custom UPDATE with calculations +Db::execute(" + UPDATE products + SET discount_price = price * 0.9 + WHERE category = :category", + [':category' => 'electronics'] +); + +// Batch operations +Db::execute(" + INSERT INTO archive_clients + SELECT * FROM clients + WHERE deleted_at < :date", + [':date' => date('Y-m-d', strtotime('-1 year'))] +); +``` + +## Performance Tips + +### Use LIMIT for Large Datasets +```php +// Paginated results +$page = $_GET['page'] ?? 1; +$perPage = 20; +$offset = ($page - 1) * $perPage; + +$clients = Db::select( + "SELECT * FROM clients LIMIT :limit OFFSET :offset", + [':limit' => $perPage, ':offset' => $offset] +); +``` + +### Index Your Queries +```sql +-- Add indexes for frequently queried columns +CREATE INDEX idx_clients_email ON clients(email); +CREATE INDEX idx_orders_status ON orders(status); +CREATE INDEX idx_orders_client_id ON orders(client_id); +``` + +### Use EXISTS Instead of COUNT +```php +// Slow +$exists = Db::getRow("SELECT COUNT(*) as c FROM clients WHERE email = :email", [':email' => $email]); +if ($exists['c'] > 0) { } + +// Fast +$exists = Db::getRow("SELECT 1 FROM clients WHERE email = :email LIMIT 1", [':email' => $email]); +if ($exists) { } +``` + +# Common Real-World Scenarios + +## File Upload Handling +```php + $maxSize) { + throw new Exception('File too large (max 5MB)'); + } + + $allowedTypes = ['application/pdf', 'image/jpeg', 'image/png']; + if (!in_array($file['type'], $allowedTypes)) { + throw new Exception('Invalid file type'); + } + + // Generate unique filename + $ext = pathinfo($file['name'], PATHINFO_EXTENSION); + $filename = uniqid() . '_' . time() . '.' . $ext; + $uploadPath = UPLOADS_PATH . '/documents/' . $filename; + + // Move file + if (!move_uploaded_file($file['tmp_name'], $uploadPath)) { + throw new Exception('Failed to save file'); + } + + // Save to database + $docId = Db::insert('documents', [ + 'user_id' => Auth::getUserId(), + 'filename' => $filename, + 'original_name' => $file['name'], + 'file_size' => $file['size'], + 'mime_type' => $file['type'], + 'uploaded_at' => date('Y-m-d H:i:s') + ]); + + redirect('/documents/show/' . $docId); + + } catch (Exception $e) { + $this->view->data['error'] = $e->getMessage(); + $this->view->render('documents/upload', true, 'site'); + } + } +} +``` + +## Pagination +```php + $perPage, ':offset' => $offset] + ); + + $this->view->data['products'] = $products; + $this->view->data['current_page'] = $page; + $this->view->data['total_pages'] = $totalPages; + $this->view->render('products/index', true, 'site'); + } +} +``` + +## Search with Filters +```php += :date_from"; + $params[':date_from'] = $_GET['date_from']; + } + if (!empty($_GET['date_to'])) { + $query .= " AND created_at <= :date_to"; + $params[':date_to'] = $_GET['date_to'] . ' 23:59:59'; + } + + $query .= " ORDER BY created_at DESC"; + + $clients = Db::select($query, $params); + + $this->view->data['clients'] = $clients; + $this->view->data['filters'] = $_GET; + $this->view->render('clients/search', true, 'site'); + } +} +``` + +## Authentication System +```php + $email]); + + if (!$user || !password_verify($password, $user['password'])) { + throw new Exception('Invalid credentials'); + } + + Session::set('user_id', $user['id']); + Session::set('user_name', $user['name']); + Session::set('user_role', $user['role']); + + // Update last login + Db::update('users', + ['last_login' => date('Y-m-d H:i:s')], + 'id = :id', + [':id' => $user['id']] + ); + + return true; + } + + public static function logout() + { + Session::destroy(); + } + + public static function isLoggedIn() + { + return Session::has('user_id'); + } + + public static function getUserId() + { + return Session::get('user_id'); + } + + public static function getUser() + { + if (!self::isLoggedIn()) { + return null; + } + + return Db::getRow("SELECT * FROM users WHERE id = :id", [':id' => self::getUserId()]); + } + + public static function requireRole($role) + { + if (Session::get('user_role') !== $role) { + throw new Exception('Unauthorized'); + } + } +} + +// Login controller +class Login extends Controller +{ + public function index() + { + if (Auth::isLoggedIn()) { + redirect('/dashboard'); + } + + $this->view->render('auth/login', false); + } + + public function submit() + { + try { + Auth::login($_POST['email'], $_POST['password']); + redirect('/dashboard'); + } catch (Exception $e) { + $this->view->data['error'] = $e->getMessage(); + $this->view->render('auth/login', false); + } + } + + public function logout() + { + Auth::logout(); + redirect('/login'); + } +} +``` +## Modal Integration System +Working with Modals; I have a dynamic modal intergration system. +data-url is the link to the modal +Modal Views are located under public/views/modals/(modal_name.php) + +## Modal Structure +``` +/www/wwwroot/(project_directory)/ +├── public/ + ├── views/ + └── modals/ +``` + +```html + +``` + +### Building the Modal +```html + + + + + + +``` diff --git a/api/.memory/foundation-plan.md b/api/.memory/foundation-plan.md new file mode 100644 index 0000000..5009697 --- /dev/null +++ b/api/.memory/foundation-plan.md @@ -0,0 +1,850 @@ +# Foundation Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Turn the SeedProject `api/` into an installable, DB-backed, two-tier-secured backend that a static Astro site calls same-origin, proven by a live health round-trip. + +**Architecture:** A CLI installer provisions config + schema; a migrations runner evolves it; two namespaced base controllers (`PublicController`, `ApiController`) enforce a public (origin + rate-limit) and privileged (bearer / `api_auth` key) tier over a shared JSON envelope; a `/api/health` endpoint queries MariaDB and returns JSON, called from an Astro page. + +**Tech Stack:** PHP 8.3, Symfony Console 5.4, MariaDB 10.11 (PDO/`Db` facade), Apache + php-fpm, Astro (static). No PHPUnit in this repo — verification is done with `php console`, `curl`, and `mysql` commands. + +**Spec:** `api/.memory/foundation.md`. **Conventions:** [documentation.md](documentation.md), [commands.md](../commands/commands.md). + +**Autoload note:** New classes use PSR-4 — `App\Services\*` → `app/Services/*.php`, `App\Controllers\*` → `app/Controllers/*.php` (composer.json maps `App\` → `app`). No `composer dump-autoload` needed for these. URL controllers stay global in `public/controllers/` (Bootstrap `require`s them) and `use` the namespaced bases. Core files (`core/*`) are never modified. + +--- + +## File structure + +**Create** +- `app/Services/Installer.php` — `App\Services\Installer`: test DB, import `dump.sql`, generate keys, write `config.php`, write install lock. +- `app/Services/Migrator.php` — `App\Services\Migrator`: ensure `migrations` table, list/apply `db/migrations/*.sql`. +- `commands/InstallCommand.php` — `php console app:install`. +- `commands/MigrateCommand.php` — `php console db:migrate [--status]`. +- `app/Controllers/JsonController.php` — `App\Controllers\JsonController extends \Controller`: `json()` envelope + `throttle()`. +- `app/Controllers/PublicController.php` — `App\Controllers\PublicController extends JsonController`: `checkOrigin()`. +- `app/Controllers/ApiController.php` — `App\Controllers\ApiController extends JsonController`: `authenticate()` (bearer/`api_auth`). +- `public/controllers/health.php` — global `Health extends App\Controllers\PublicController`. +- `public/controllers/admin.php` — global `Admin extends App\Controllers\ApiController`. +- `db/migrations/001_create_metrics_placeholder.sql` — a trivial first migration to prove the runner. +- `app/src/pages/api-health-test.astro` — client-side round-trip proof page. + +**Modify** +- `console` — register `InstallCommand`, `MigrateCommand`. +- `install/controllers/index.php` — delegate to `Installer`; refuse if locked. +- Apache vhost `/www/server/panel/vhost/apache/comiida.com.conf` — deny `install/`, `db/`, `.installed`. + +**Server (outside repo)** +- MariaDB `comiida` database + user. + +--- + +## Task 1: Provision the database + +**Files:** none (server state). + +- [ ] **Step 1: Create the database and a dedicated user** + +Operator supplies the MariaDB root password. Choose a strong `APP_DB_PASS`. + +Run: +```bash +mysql -u root -p -e " +CREATE DATABASE IF NOT EXISTS comiida CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; +CREATE USER IF NOT EXISTS 'comiida'@'localhost' IDENTIFIED BY 'APP_DB_PASS'; +GRANT ALL PRIVILEGES ON comiida.* TO 'comiida'@'localhost'; +FLUSH PRIVILEGES;" +``` + +- [ ] **Step 2: Verify the user can connect to the empty DB** + +Run: `mysql -u comiida -p'APP_DB_PASS' comiida -e "SELECT DATABASE();"` +Expected: prints `comiida`, no error. + +- [ ] **Step 3: No commit** (server state, nothing in repo). + +--- + +## Task 2: Installer service + `app:install` command + +**Files:** +- Create: `app/Services/Installer.php` +- Create: `commands/InstallCommand.php` +- Modify: `console` + +- [ ] **Step 1: Write `app/Services/Installer.php`** + +```php + up two levels -> api/ + $this->baseDir = dirname(__DIR__, 2); + } + + public function lockFile(): string + { + return $this->baseDir . '/system/.installed'; + } + + public function isInstalled(): bool + { + return is_file($this->lockFile()); + } + + public function testConnection(array $db): bool + { + try { + new PDO( + "mysql:host={$db['host']};dbname={$db['name']};charset=utf8mb4", + $db['user'], $db['pass'], + [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION] + ); + return true; + } catch (PDOException $e) { + return false; + } + } + + public function importSchema(array $db): void + { + $sqlFile = $this->baseDir . '/install/dump.sql'; + if (!is_file($sqlFile)) { + throw new RuntimeException("Schema dump not found: {$sqlFile}"); + } + $mysqli = new \mysqli($db['host'], $db['user'], $db['pass'], $db['name']); + if ($mysqli->connect_errno) { + throw new RuntimeException('DB connect failed: ' . $mysqli->connect_error); + } + if (!$mysqli->multi_query((string) file_get_contents($sqlFile))) { + throw new RuntimeException('Schema import failed: ' . $mysqli->error); + } + // Drain all result sets so the connection finishes cleanly. + while ($mysqli->more_results() && $mysqli->next_result()) { /* noop */ } + if ($mysqli->errno) { + throw new RuntimeException('Schema import error: ' . $mysqli->error); + } + $mysqli->close(); + } + + public function generateKey(int $bytes = 32): string + { + return bin2hex(random_bytes($bytes)); // hex only: safe inside single-quoted PHP + } + + public function writeConfig(array $c): void + { + $e = fn($v) => addslashes((string) $v); // escape operator-provided values + $tpl = "baseDir . '/config.php', $tpl); + } + + public function lock(): void + { + file_put_contents($this->lockFile(), date('c') . "\n"); + } + + /** + * Full install. $cfg = ['url','name','db'=>['host','name','user','pass']]. + */ + public function run(array $cfg): void + { + if (!$this->testConnection($cfg['db'])) { + throw new RuntimeException('Database connection failed. Check credentials.'); + } + $this->importSchema($cfg['db']); + $cfg['admin_token'] = $this->generateKey(24); + $cfg['hash_password_key'] = $this->generateKey(32); + $cfg['hash_api_key'] = $this->generateKey(32); + $this->writeConfig($cfg); + $this->lock(); + } +} +``` + +- [ ] **Step 2: Write `commands/InstallCommand.php`** + +```php +setName('app:install') + ->setDescription('Install the framework: import schema, write config, lock.') + ->addOption('db-host', null, InputOption::VALUE_REQUIRED, 'DB host', 'localhost') + ->addOption('db-name', null, InputOption::VALUE_REQUIRED, 'DB name') + ->addOption('db-user', null, InputOption::VALUE_REQUIRED, 'DB user') + ->addOption('db-pass', null, InputOption::VALUE_REQUIRED, 'DB password') + ->addOption('url', null, InputOption::VALUE_REQUIRED, 'Site URL', 'https://www.comiida.com') + ->addOption('name', null, InputOption::VALUE_REQUIRED, 'Project name', 'Comiida') + ->addOption('force', null, InputOption::VALUE_NONE, 'Re-run even if already installed'); + } + + protected function execute(InputInterface $input, OutputInterface $output) + { + $installer = new Installer(); + if ($installer->isInstalled() && !$input->getOption('force')) { + $output->writeln('Already installed. Use --force to re-run.'); + return Command::FAILURE; + } + foreach (['db-name', 'db-user', 'db-pass'] as $req) { + if (!$input->getOption($req)) { + $output->writeln("--{$req} is required."); + return Command::FAILURE; + } + } + try { + $installer->run([ + 'url' => $input->getOption('url'), + 'name' => $input->getOption('name'), + 'db' => [ + 'host' => $input->getOption('db-host'), + 'name' => $input->getOption('db-name'), + 'user' => $input->getOption('db-user'), + 'pass' => $input->getOption('db-pass'), + ], + ]); + } catch (\Throwable $e) { + $output->writeln('Install failed: ' . $e->getMessage() . ''); + return Command::FAILURE; + } + $output->writeln('Install complete. config.php written, schema imported, lock set.'); + return Command::SUCCESS; + } +} +``` + +- [ ] **Step 3: Register the command in `console`** + +In `console`, add the `use` and registration (below the existing `GreetCommand` line): + +```php +$application->add(new GreetCommand()); +$application->add(new InstallCommand()); +``` + +- [ ] **Step 4: Verify the command is discoverable** + +Run: `php console list | grep app:install` +Expected: a line `app:install Install the framework...` + +- [ ] **Step 5: Run the installer against the comiida DB** + +Run (operator fills `APP_DB_PASS`): +```bash +php console app:install --db-host=localhost --db-name=comiida --db-user=comiida --db-pass='APP_DB_PASS' --url=https://www.comiida.com --name=Comiida +``` +Expected: `Install complete...` + +- [ ] **Step 6: Verify schema, config, and lock** + +Run: +```bash +mysql -u comiida -p'APP_DB_PASS' comiida -e "SHOW TABLES;" | grep -E "api_auth|config|users" +php -r "require 'config.php'; echo DB_NAME.PHP_EOL; echo (strlen(ADMIN_TOKEN)>=48?'token-ok':'token-bad').PHP_EOL;" +test -f system/.installed && echo "locked" +``` +Expected: tables listed; `comiida`, `token-ok`, `locked`. + +- [ ] **Step 7: Commit** + +```bash +git add app/Services/Installer.php commands/InstallCommand.php console +git commit -m "feat(api): CLI installer (app:install) — schema import, config, lock" +``` +(Note: `config.php` and `system/.installed` are gitignored / not tracked.) + +--- + +## Task 3: Migrations runner + `db:migrate` + +**Files:** +- Create: `app/Services/Migrator.php` +- Create: `commands/MigrateCommand.php` +- Create: `db/migrations/001_create_metrics_placeholder.sql` +- Modify: `console`, `app/Services/Installer.php` + +- [ ] **Step 1: Write `app/Services/Migrator.php`** + +```php +pdo = $pdo; + $this->dir = $dir; + } + + public function ensureTable(): void + { + $this->pdo->exec( + "CREATE TABLE IF NOT EXISTS `migrations` ( + `id` INT AUTO_INCREMENT PRIMARY KEY, + `filename` VARCHAR(255) NOT NULL UNIQUE, + `applied_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4" + ); + } + + /** @return string[] filenames already applied */ + public function applied(): array + { + return $this->pdo->query("SELECT filename FROM `migrations`")->fetchAll(PDO::FETCH_COLUMN) ?: []; + } + + /** @return string[] absolute paths of pending migrations, in order */ + public function pending(): array + { + $all = glob($this->dir . '/*.sql') ?: []; + sort($all); + $applied = $this->applied(); + return array_values(array_filter($all, fn($p) => !in_array(basename($p), $applied, true))); + } + + /** @return string[] filenames applied this run */ + public function migrate(): array + { + $this->ensureTable(); + $done = []; + foreach ($this->pending() as $path) { + $this->pdo->exec((string) file_get_contents($path)); + $stmt = $this->pdo->prepare("INSERT IGNORE INTO `migrations` (filename) VALUES (?)"); + $stmt->execute([basename($path)]); + $done[] = basename($path); + } + return $done; + } +} +``` + +- [ ] **Step 2: Write the first migration `db/migrations/001_create_metrics_placeholder.sql`** + +```sql +-- Foundation smoke migration: proves the runner end-to-end. +-- (Real metrics tables land in the Metrics sub-project.) +CREATE TABLE IF NOT EXISTS `_foundation_check` ( + `id` INT AUTO_INCREMENT PRIMARY KEY, + `note` VARCHAR(64) NOT NULL, + `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; +``` + +- [ ] **Step 3: Write `commands/MigrateCommand.php`** + +```php +setName('db:migrate') + ->setDescription('Apply pending SQL migrations from db/migrations/') + ->addOption('status', null, InputOption::VALUE_NONE, 'Show applied/pending without applying'); + } + + protected function execute(InputInterface $input, OutputInterface $output) + { + $base = dirname(__DIR__); // api/ + if (!is_file($base . '/config.php')) { + $output->writeln('config.php missing — run app:install first.'); + return Command::FAILURE; + } + require_once $base . '/config.php'; // defines DB_* constants + $pdo = new PDO( + 'mysql:host=' . DB_HOST . ';dbname=' . DB_NAME . ';charset=utf8mb4', + DB_USER, DB_PASS, + [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION] + ); + $migrator = new Migrator($pdo, $base . '/db/migrations'); + $migrator->ensureTable(); + + if ($input->getOption('status')) { + $output->writeln('Applied: ' . (implode(', ', $migrator->applied()) ?: '(none)')); + $output->writeln('Pending: ' . (implode(', ', array_map('basename', $migrator->pending())) ?: '(none)')); + return Command::SUCCESS; + } + $done = $migrator->migrate(); + $output->writeln($done ? 'Applied: ' . implode(', ', $done) : 'Nothing to migrate.'); + return Command::SUCCESS; + } +} +``` + +- [ ] **Step 4: Register in `console`** + +```php +$application->add(new InstallCommand()); +$application->add(new MigrateCommand()); +``` + +- [ ] **Step 5: Wire migrations into the installer** + +In `app/Services/Installer.php`, at the end of `run()` (after `$this->lock();`), append: + +```php + // Apply migrations on top of the freshly imported baseline. + $pdo = new PDO( + "mysql:host={$cfg['db']['host']};dbname={$cfg['db']['name']};charset=utf8mb4", + $cfg['db']['user'], $cfg['db']['pass'], + [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION] + ); + (new Migrator($pdo, $this->baseDir . '/db/migrations'))->migrate(); +``` + +Add `use PDO;` is already present; no new import needed (`Migrator` is same namespace). + +- [ ] **Step 6: Verify migration status then apply** + +Run: +```bash +php console db:migrate --status +php console db:migrate +mysql -u comiida -p'APP_DB_PASS' comiida -e "SELECT filename FROM migrations;" +``` +Expected: status shows `001_...` pending → apply prints `Applied: 001_create_metrics_placeholder.sql` → the `migrations` table lists it. (`_foundation_check` table now exists.) + +- [ ] **Step 7: Commit** + +```bash +git add app/Services/Migrator.php commands/MigrateCommand.php db/migrations/001_create_metrics_placeholder.sql app/Services/Installer.php console +git commit -m "feat(api): SQL migrations runner (db:migrate) + wire into installer" +``` + +--- + +## Task 4: JSON base + public tier + `/api/health` + +**Files:** +- Create: `app/Controllers/JsonController.php` +- Create: `app/Controllers/PublicController.php` +- Create: `public/controllers/health.php` + +- [ ] **Step 1: Write `app/Controllers/JsonController.php`** + +```php + $error === null, + 'data' => $data, + 'error' => $error, // ['code' => ..., 'message' => ...] or null + ]); + exit; + } + + /** + * Returns true when the caller has EXCEEDED $max hits on $key within $window seconds. + * Reuses the api_requests table (no new table needed). + */ + protected function throttle(string $key, int $max, int $window): bool + { + $ip = $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0'; + $count = (int) \Db::getValue( + "SELECT COUNT(*) FROM `api_requests` + WHERE `requesting_ip` = ? AND `request` = ? + AND `created_date` > (NOW() - INTERVAL ? SECOND)", + [$ip, $key, $window] + ); + \Db::insert('api_requests', [ + 'requesting_ip' => $ip, + 'request' => $key, + 'service' => 'foundation', + 'domainURI' => $_SERVER['HTTP_HOST'] ?? '', + 'created_date' => date('Y-m-d H:i:s'), + ]); + return $count >= $max; + } +} +``` + +- [ ] **Step 2: Write `app/Controllers/PublicController.php`** + +```php + false. + * Same-origin GET / non-browser callers omit Origin -> allowed. + */ + protected function checkOrigin(): bool + { + $origin = $_SERVER['HTTP_ORIGIN'] ?? ''; + if ($origin === '') { + return true; // same-origin GET or server-side caller + } + $allowed = array_filter(array_map('trim', explode(',', defined('ALLOWED_ORIGINS') ? ALLOWED_ORIGINS : ''))); + if (empty($allowed)) { + return true; // not configured (dev) + } + $originHost = parse_url($origin, PHP_URL_HOST); + foreach ($allowed as $a) { + $host = parse_url($a, PHP_URL_HOST) ?: $a; + if ($originHost && strcasecmp($originHost, $host) === 0) { + header('Access-Control-Allow-Origin: ' . $origin); + return true; + } + } + return false; + } + + /** Guard helper: enforce origin + rate limit, or emit the error envelope and stop. */ + protected function guardPublic(string $key, int $max = 60, int $window = 60): void + { + if (!$this->checkOrigin()) { + $this->json(null, 403, ['code' => 'forbidden_origin', 'message' => 'Origin not allowed']); + } + if ($this->throttle($key, $max, $window)) { + $this->json(null, 429, ['code' => 'rate_limited', 'message' => 'Too many requests']); + } + } +} +``` + +- [ ] **Step 3: Write `public/controllers/health.php`** + +```php +guardPublic('health', 120, 60); + try { + $up = (int) \Db::getValue("SELECT 1"); + $rows = (int) \Db::getValue("SELECT COUNT(*) FROM `config`"); + } catch (\Throwable $e) { + $this->json(null, 500, ['code' => 'db_error', 'message' => DEBUG ? $e->getMessage() : 'Database unavailable']); + } + $this->json([ + 'db' => $up === 1 ? 'connected' : 'unknown', + 'app' => defined('PROJECT_NAME') ? PROJECT_NAME : '', + 'config_rows' => $rows, + 'time' => date('c'), + ]); + } +} +``` + +- [ ] **Step 4: Verify the health endpoint returns live DB JSON** + +Run: `curl -sk https://127.0.0.1/api/health -H "Host: www.comiida.com"` +Expected: `{"ok":true,"data":{"db":"connected","app":"Comiida","config_rows":,"time":"..."},"error":null}` + +- [ ] **Step 5: Verify rate limiting** + +Run: `for i in $(seq 1 130); do curl -s -o /dev/null -w "%{http_code} " -k https://127.0.0.1/api/health -H "Host: www.comiida.com"; done; echo` +Expected: `200` responses turning into `429` after ~120 within the minute. + +- [ ] **Step 6: Commit** + +```bash +git add app/Controllers/JsonController.php app/Controllers/PublicController.php public/controllers/health.php +git commit -m "feat(api): JSON base + public tier (origin+rate-limit) + /api/health" +``` + +--- + +## Task 5: Privileged tier + `/api/admin/ping` + +**Files:** +- Create: `app/Controllers/ApiController.php` +- Create: `public/controllers/admin.php` + +- [ ] **Step 1: Write `app/Controllers/ApiController.php`** + +```php +bearer(); + if ($token === '') { + $this->json(null, 401, ['code' => 'unauthorized', 'message' => 'Missing bearer token']); + } + // First-party admin token (constant-time compare). + if (defined('ADMIN_TOKEN') && hash_equals(ADMIN_TOKEN, $token)) { + if ($this->throttle('admin', $max, $window)) { + $this->json(null, 429, ['code' => 'rate_limited', 'message' => 'Too many requests']); + } + return; + } + // Programmatic api_auth key. + $row = \Db::getRow( + "SELECT `id`, `userid`, `active` FROM `api_auth` WHERE `apikey` = ? LIMIT 1", + [$token] + ); + if (!$row || (int) $row['active'] !== 1) { + $this->json(null, 401, ['code' => 'unauthorized', 'message' => 'Invalid API key']); + } + $this->apiUser = $row; + if ($this->throttle('apikey:' . $row['id'], $max, $window)) { + $this->json(null, 429, ['code' => 'rate_limited', 'message' => 'Too many requests']); + } + // NOTE: monthly/plan quota enforcement (api_plans/api_usage) is deferred to a later sub-project. + } +} +``` + +- [ ] **Step 2: Write `public/controllers/admin.php`** + +```php +requireAuth(); + $this->json([ + 'pong' => true, + 'auth' => $this->apiUser ? 'apikey' : 'admin_token', + 'time' => date('c'), + ]); + } +} +``` + +- [ ] **Step 3: Verify unauthorized is rejected** + +Run: `curl -sk -o /dev/null -w "%{http_code}\n" https://127.0.0.1/api/admin/ping -H "Host: www.comiida.com"` +Expected: `401` + +- [ ] **Step 4: Verify the admin token authenticates** + +Run: +```bash +TOKEN=$(php -r "require 'config.php'; echo ADMIN_TOKEN;") +curl -sk https://127.0.0.1/api/admin/ping -H "Host: www.comiida.com" -H "Authorization: Bearer $TOKEN" +``` +Expected: `{"ok":true,"data":{"pong":true,"auth":"admin_token","time":"..."},"error":null}` + +- [ ] **Step 5: Commit** + +```bash +git add app/Controllers/ApiController.php public/controllers/admin.php +git commit -m "feat(api): privileged tier (bearer/api_auth) + /api/admin/ping" +``` + +--- + +## Task 6: Security hardening — lock the installer + +**Files:** +- Modify: `install/controllers/index.php` +- Modify: Apache vhost `/www/server/panel/vhost/apache/comiida.com.conf` + +- [ ] **Step 1: Make the web installer refuse when locked & delegate to `Installer`** + +At the top of `installation()` in `install/controllers/index.php`, before any work, insert: + +```php + require_once dirname(__DIR__, 2) . '/vendor/autoload.php'; + $installer = new \App\Services\Installer(); + if ($installer->isInstalled()) { + http_response_code(403); + die('Already installed. Remove system/.installed to reinstall.'); + } + if (!$_POST) { header('Location: /api/install/'); die(); } + $installer->run([ + 'url' => 'https://www.comiida.com', + 'name' => 'Comiida', + 'db' => [ + 'host' => $_POST['dbloca'], + 'name' => $_POST['dbname'], + 'user' => $_POST['dbuser'], + 'pass' => $_POST['dbpass'], + ], + ]); + header('Location: /api/install/i/complete'); + return; +``` + +(This replaces the old body that used `$_SERVER['DOCUMENT_ROOT']` and wrote a `seedproject.com` config. The rest of the method below can be removed.) + +- [ ] **Step 2: Extend the Apache denies** + +In `/www/server/panel/vhost/apache/comiida.com.conf`, update the existing `DirectoryMatch` (added when `/api` was mounted) to also cover `install`, `db`, and add a `.installed` file deny. Replace the block with: + +```apache + # Deny web access to framework internals (loaded server-side only) + + Require all denied + + + Require all denied + +``` + +- [ ] **Step 3: Apply and reload** + +Run: `/www/server/apache/bin/httpd -t && /www/server/apache/bin/httpd -k graceful` +Expected: `Syntax OK`, reload succeeds. + +- [ ] **Step 4: Verify the installer is no longer web-reachable** + +Run: `curl -sk -o /dev/null -w "%{http_code}\n" https://127.0.0.1/api/install/ -H "Host: www.comiida.com"` +Expected: `403` + +- [ ] **Step 5: Verify `/api/health` still works (denies didn't over-reach)** + +Run: `curl -sk -o /dev/null -w "%{http_code}\n" https://127.0.0.1/api/health -H "Host: www.comiida.com"` +Expected: `200` + +- [ ] **Step 6: Commit** + +```bash +git add install/controllers/index.php +git commit -m "feat(api): lock installer, delegate to Installer service, deny install/db over HTTP" +``` +(Apache vhost is outside the repo — not committed.) + +--- + +## Task 7: Astro round-trip proof page + +**Files:** +- Create: `app/src/pages/api-health-test.astro` + +- [ ] **Step 1: Write the test page** + +```astro +--- +// Static page; the fetch runs client-side, same-origin, against the PHP API. +--- + + API health test + +

SeedProject /api health

+
loading…
+ + + +``` + +- [ ] **Step 2: Build the Astro site** + +Run: `cd /www/wwwroot/comiida.com/app && npm run build` +Expected: build succeeds; `public/api-health-test/index.html` (or `public/api-health-test.html`) produced. + +- [ ] **Step 3: Verify the page loads and the fetch target resolves** + +Run: `curl -sk https://127.0.0.1/api-health-test -H "Host: www.comiida.com" | grep -c "/api/health"` +Expected: `1` (the page references the endpoint). Loading it in a browser shows the live JSON envelope with `db: "connected"`. + +- [ ] **Step 4: Commit** + +```bash +cd /www/wwwroot/comiida.com +git add app/src/pages/api-health-test.astro +git commit -m "feat(api): Astro page proving browser->PHP->MariaDB health round-trip" +``` + +--- + +## Self-review notes + +- **Spec coverage:** Installer (§1) → Task 2/6; migrations (§2) → Task 3; two-tier auth + envelope (§3) → Task 4/5; health round-trip (§4) → Task 4/5/7; security hardening (§5) → Task 6; DB provisioning → Task 1. All spec sections mapped. +- **Deliberate scope trim (YAGNI):** full `api_plans`/`api_usage` monthly-quota enforcement is deferred (flagged inline in `ApiController::requireAuth`); the foundation ships bearer/api_auth verification + per-minute throttle + request logging, which is enough to prove the privileged tier. +- **Type consistency:** `Installer::run(['url','name','db'=>[...]])` shape is used identically in InstallCommand (Task 2), Installer migration wiring (Task 3), and the web wizard (Task 6). `Migrator($pdo, $dir)` signature consistent across Task 3 and the installer. `json()/throttle()/guardPublic()/requireAuth()` names consistent across controllers. +- **Open item:** operator provides the MariaDB root + app DB password (Task 1) and should still rotate/delete the vestigial `api/.env`. diff --git a/api/.memory/foundation.md b/api/.memory/foundation.md new file mode 100644 index 0000000..00e4f9a --- /dev/null +++ b/api/.memory/foundation.md @@ -0,0 +1,209 @@ +# Foundation — SeedProject as a per-project Astro backend + +**Status:** Design (approved, not yet implemented) · **Date:** 2026-07-04 +**Related:** [documentation.md](documentation.md) (framework internals) · [llm.md](llm.md) (LLM layer) · [changelog.md](changelog.md) + +--- + +## Context & goal + +SeedProject (this `api/` framework) is mounted at `comiida.com/api/` and boots +(see the 2026-07-04 changelog). The goal now: make it a **solid, reusable backend +platform** that a static Astro frontend calls to reach a database, run agents, +trigger CLI processes, and record metrics. + +That vision is four subsystems on a shared base. This doc specifies **only the +Foundation** (sub-project #1) — the database + a secure, reproducible, reusable +request round-trip. Metrics, agents, and CLI *business* commands are separate +specs built on top of it. + +### Locked decisions + +| Question | Decision | +|---|---| +| Reuse model | **Per-project copy** — each Astro site gets its own `api/` install + own DB. A drop-in starter, fully isolated. | +| Auth | **Two-tier** — public (origin + rate-limit) for browser beacons; privileged (bearer token / API key) for reads/admin. | +| Schema mgmt | **Versioned SQL migrations via CLI** — `install/dump.sql` baseline + `db/migrations/NNN_*.sql` applied by `php console db:migrate`. | +| Install | **CLI-first** (`php console app:install`); the existing web wizard is fixed but hard-locked. | + +### Deployment facts (this project) + +- Web server: **Apache** (`/www/server/apache`), DocumentRoot = `.../public` (static Astro build). `/api` is served via an `Alias` → `.../api` with a php-fpm 8.3 handler and front-controller rewrite. (nginx conf in the BT panel is inert.) +- DB engine: **MariaDB 10.11** on `/tmp/mysql.sock` (BT-managed). Foundation provisions a dedicated `comiida` database + user. +- Frontend calls are **browser-side, same-origin** `fetch("/api/…")` (comiida is static; there is no Astro SSR). This is why the public tier is origin/rate-limited rather than server-secret-authenticated. + +--- + +## Architecture + +``` +Browser (static Astro page) + │ fetch("/api/health") same-origin + ▼ +Apache :443 ──Alias /api──▶ api/index.php ──▶ core/Bootstrap ──▶ Router + │ │ + │ (php-fpm 8.3 via /tmp/php-cgi-83.sock) ┌────────────────┴───────────────┐ + │ ▼ ▼ + │ PublicController ApiController + │ (origin + rate limit) (bearer / api_auth + │ │ + plan limits + logging) + │ ▼ ▼ + │ App logic ───────▶ Db helper ───▶ MariaDB (comiida) + ▼ +JSON envelope { ok, data, error } +``` + +The Foundation adds **five units**, each independently understandable/testable: + +1. **Installer** — provisions DB + config (CLI + locked web wizard share one service). +2. **Migrations runner** — versioned schema evolution. +3. **Two-tier auth** — `PublicController` + `ApiController` base classes. +4. **Health round-trip** — a real end-to-end proof endpoint. +5. **Security hardening** — Apache denies, install lock, crypto keys. + +--- + +## 1. Install (CLI-first) + +**Problem with the shipped installer** (`install/controllers/index.php`): it reads +`dump.sql`/writes `config.php` via `$_SERVER['DOCUMENT_ROOT']` (wrong under the +`/api` Alias — that's comiida's `public/`); it generates a config with `SITE_BASE '/'`, +`ASSETS '/public/assets/'`, `seedproject.com` (would clobber the correct `/api` +config); it is web-reachable and re-runnable; and `getName()` uses non-crypto +`rand()` over an alphabet containing `'`, `\`, `/` (a quote breaks the single-quoted +`config.php` → PHP syntax error). + +**Design:** + +- **`app/Services/Installer.php`** — one service both entrypoints call (DRY). Uses + `dirname(__DIR__, 2)` base paths, never `DOCUMENT_ROOT`. Responsibilities: + 1. Validate + test DB connection (PDO). + 2. Import `install/dump.sql` (the skeleton). + 3. Generate keys with `bin2hex(random_bytes(32))` (crypto-safe; hex only — no quote bug). + 4. Write `api/config.php` with the **correct `/api` constants** (`SITE_BASE=/api`, + `ASSETS=/api/public/assets/`, `URL`, `PROJECT_NAME` from args/env; DB creds). + 5. Record the baseline as applied, then run pending migrations. + 6. Write install lock `api/system/.installed`. +- **`commands/InstallCommand.php`** — `php console app:install` (Symfony Console, + registered in `console` next to `GreetCommand`). Reads creds from options + (`--db-host --db-name --db-user --db-pass --url --name`) or interactive prompt; + refuses to run if `.installed` exists unless `--force`. +- **Web wizard** (`install/`) — refactored to call `Installer` (fixes its path bugs); + refuses to run when `.installed` exists; denied at the Apache layer by default. + +Config stays as `config.php` (framework-native, already gitignored) — no `.env` +loader is introduced (YAGNI). The stray CreditPullEngine `.env` remains vestigial and +should be deleted/rotated by the operator. + +## 2. Schema & migrations + +- `install/dump.sql` = **baseline** (imported once). Tables already provided: + `users*`, `roles`/`permissions`/`role_perm` (RBAC), `org*` (orgs/profiles), + `api_auth`/`users_api` (API keys), `api_plans` (rate limits), `api_requests` + (request log), `api_usage` (daily/monthly/yearly counters), `config` (key/value). +- **`db/migrations/NNN_description.sql`** — ordered, forward-only SQL files. +- **`migrations`** tracking table (`id, filename, applied_at`) — created by the + installer; baseline recorded so it is never re-run. +- **`commands/MigrateCommand.php`** — `php console db:migrate` applies unapplied + files in filename order, each in a transaction where the DDL allows; records each. + `php console db:migrate --status` lists applied/pending. + +## 3. Two-tier auth + response contract + +Reuses the existing tables — no duplication. + +- **`ApiController`** (base, privileged) — verifies a bearer token: either the + `ADMIN_TOKEN` (config constant; same token pattern as `/devconsole` & `/admin`) + for first-party/admin calls, **or** an `api_auth.apikey` for programmatic + consumers. On an API key: check `active`, enforce `api_plans.limit_minute` / + `limit_monthly` against `api_usage`, and log to `api_requests`. Failures → + `401` (missing/invalid) or `429` (over limit). +- **`PublicController`** (base, public) — for browser beacons. Verifies + `Origin`/`Referer` against a config **`ALLOWED_ORIGINS`** allowlist, applies + **IP-based rate limiting**, requires no user key. Failures → `403` (bad origin) + or `429` (flood). Emits same-origin CORS headers. +- **Response envelope** — a shared `json($data, $status)` helper on the base + `Controller` returns `{ "ok": bool, "data": …, "error": { "code", "message" } }` + with the matching HTTP status. + +## 4. Health round-trip (the demonstrable slice) + +- **`GET /api/health`** (public) → runs a **real** DB query (`SELECT 1` + read one + `config` row) → `{ ok:true, data:{ db:"connected", app, version, time } }`. +- **`GET /api/admin/ping`** (privileged) → echoes authenticated context. +- A small client-side `fetch("/api/health")` on an Astro test page renders the + result — proving **browser → Apache → php-fpm → MariaDB → JSON** end to end. + +## 5. Security hardening + +- Apache `` denies extended to `install/` (once `.installed` exists), + `db/`, and the `.installed` lock. Existing denies (`vendor|core|app|system|commands| + .memory|.reference_files`, dotfiles, `config.php`, `composer.*`) stay. +- `config.php` gitignored, written with crypto-safe keys. +- Public endpoints origin-gated + rate-limited. +- Operator TODO: rotate/delete the vestigial `api/.env` (live CreditPullEngine + secrets + a GitHub PAT). + +--- + +## Data flow — a public request + +1. Browser `GET /api/health` (same-origin, no credentials). +2. Apache Alias → `api/index.php` → Bootstrap → Router → `Health` controller + (extends `PublicController`). +3. `PublicController` checks `Origin` ∈ `ALLOWED_ORIGINS`, checks IP rate limit. +4. Controller queries `Db` (lazy PDO connect to MariaDB), builds payload. +5. `json()` emits `{ ok, data }` + `200` (or `403`/`429`/`500`). + +## Error handling + +- All controller output goes through `json()`; no raw echo. HTTP status always set. +- DB/PDO exceptions caught → `{ ok:false, error:{ code:"db_error", … } }` + `500` + (detail hidden unless `DEBUG`). `system/ErrorHandler.php` remains the backstop. +- Auth failures return typed codes: `unauthorized` (401), `forbidden_origin` (403), + `rate_limited` (429). + +## File inventory + +**New** +- `app/Services/Installer.php` +- `commands/InstallCommand.php`, `commands/MigrateCommand.php` +- `app/Controllers/PublicController.php`, `app/Controllers/ApiController.php` +- `public/controllers/health.php` (public), `public/controllers/admin.php` (privileged) +- `db/migrations/` (+ a first example migration) +- `api/system/.installed` (generated at install) + +**Modified** +- `console` — register `InstallCommand`, `MigrateCommand` +- `install/controllers/index.php` — delegate to `Installer` (path-bug fix, lock check) +- `core/Controller.php` (or a base) — add `json()` helper +- `config.php` — add `ADMIN_TOKEN`, `ALLOWED_ORIGINS` constants (generated by installer) +- Apache vhost (`/www/server/panel/vhost/apache/comiida.com.conf`) — extend denies + +**Server (outside repo)** +- MariaDB: create `comiida` database + dedicated user. + +## Verification + +1. `php console app:install --db-… --url=https://www.comiida.com --name=Comiida` + → tables created, `config.php` written with crypto keys, `.installed` present. +2. `php console db:migrate --status` → baseline + migrations applied. +3. `curl https://www.comiida.com/api/health` → `200`, `db:"connected"`. +4. From a disallowed `Origin` → `403`; flood → `429`. +5. `curl /api/admin/ping` no token → `401`; with `ADMIN_TOKEN` → `200`. +6. `curl /api/install/` after lock → denied. +7. Main Astro site (`/`) unaffected. + +## Out of scope (future sub-projects, each its own spec) + +- **Metrics** — event tables + `collect` beacon + admin dashboard. +- **Runtime agents/skills** — LLM agent endpoints (builds on [llm.md](llm.md)). +- **Business CLI commands** — cron/process runners. + +The Foundation delivers the CLI *infrastructure* (`app:install`, `db:migrate`) and the +secure round-trip those three build on. + +## Open items for the operator + +- DB name/user/password for the `comiida` database (BT panel or root creds). +- Decision to rotate + delete `api/.env`. diff --git a/api/.memory/llm.md b/api/.memory/llm.md new file mode 100644 index 0000000..a37d31b --- /dev/null +++ b/api/.memory/llm.md @@ -0,0 +1,313 @@ +# LLM Integration Documentation + +## Architecture + +``` +app/LLM/ +├── LLMProvider.php — Abstract base class (contract for all providers) +├── LLMManager.php — Factory: loads org config from DB, returns provider instance +├── svcOpenAI.php — OpenAI (fully implemented) +├── svcAnthropic.php — Anthropic/Claude (fully implemented) +├── svcGemini.php — Google Gemini (fully implemented) +├── svcMistral.php — Mistral AI (fully implemented) +└── svcDeepSeek.php — DeepSeek (fully implemented) +``` + +## Database Storage — sp_orgs_meta + +LLM configs are stored per-org in `sp_orgs_meta`: + +| column | description | +|------------|------------------------------------| +| orgid | Foreign key to sp_orgs | +| keyval | Provider key (e.g. `llmOpenAI`) | +| metval | JSON config blob | +| active | 1 = enabled, 0 = disabled | + +### keyval names +| Provider | keyval | +|------------|----------------| +| OpenAI | `llmOpenAI` | +| Anthropic | `llmAnthropic` | +| Gemini | `llmGemini` | +| Mistral | `llmMistral` | +| DeepSeek | `llmDeepSeek` | + +### metval JSON structure (all providers) +```json +{ + "secretKey": "sk-...", + "model": "gpt-4o", + "max_tokens": 4096, + "temperature": 0.7 +} +``` + +## LLMManager — Factory Class + +```php +use App\LLM\LLMManager; + +// Get a configured, ready-to-use provider instance +$llm = LLMManager::forOrg($orgId, 'openai'); + +// List all configured providers for an org +$providers = LLMManager::getAvailableProviders($orgId); +// returns: ['openai', 'gemini'] + +// Save or update a provider config +LLMManager::saveOrgConfig($orgId, 'openai', [ + 'secretKey' => 'sk-...', + 'model' => 'gpt-4o', + 'max_tokens' => 4096, + 'temperature' => 0.7, +]); +``` + +## LLMProvider — Base Class Interface + +All providers expose the same methods: + +```php +// Send a prompt, get full response +$response = $llm->sendPrompt(array $messages, array $options = []); +// Returns: ['success' => bool, 'content' => string, 'usage' => array, 'error' => string] + +// Stream a prompt, callback per chunk +$result = $llm->streamPrompt(array $messages, array $options, callable $callback); +// Returns: ['success' => bool, 'error' => string] + +// Get available models +$models = $llm->getModels(); +// Returns: [['id' => 'gpt-4o', 'label' => 'GPT-4o'], ...] + +// Test API key + connectivity +$status = $llm->testConnection(); +// Returns: ['success' => bool, 'latency_ms' => int, 'error' => string] +``` + +### Fluent setters (chainable) +```php +$llm->setModel('gpt-4o') + ->setMaxTokens(2048) + ->setTemperature(0.5) + ->setSystemPrompt('You are a helpful assistant.') + ->setTimeout(60); +``` + +## Usage Examples (PHP) + +### Basic chat +```php +$llm = LLMManager::forOrg($orgId, 'openai'); +$response = $llm->sendPrompt([ + ['role' => 'user', 'content' => 'Summarize this contract: ...'] +]); + +if ($response['success']) { + echo $response['content']; + // $response['usage'] = ['prompt_tokens' => 120, 'completion_tokens' => 80, 'total_tokens' => 200] +} +``` + +### With system prompt and custom options +```php +$llm = LLMManager::forOrg($orgId, 'anthropic'); +$llm->setSystemPrompt('You are a legal document assistant.'); + +$response = $llm->sendPrompt( + [['role' => 'user', 'content' => 'Draft an NDA for two parties.']], + ['model' => 'claude-sonnet-4-6', 'max_tokens' => 2048, 'temperature' => 0.3] +); +``` + +### Streaming (PHP — CLI or long-running process) +```php +$llm = LLMManager::forOrg($orgId, 'gemini'); +$llm->streamPrompt( + [['role' => 'user', 'content' => 'Write a report on...']], + [], + function(string $chunk) { + echo $chunk; + flush(); + } +); +``` + +### Multi-turn conversation +```php +$messages = [ + ['role' => 'user', 'content' => 'My name is Carlos.'], + ['role' => 'assistant', 'content' => 'Nice to meet you, Carlos!'], + ['role' => 'user', 'content' => 'What is my name?'], +]; + +$response = $llm->sendPrompt($messages); +``` + +## API Endpoints — appi/controllers/llm.php + +All endpoints are authenticated via `Auth::API()`. +Internal AJAX requests (X-Requested-With: XMLHttpRequest) pass through automatically. +External requests require `?apikey=` param. + +### POST /appi/llm/chat +Full response. +```json +// Request +{ + "provider": "openai", + "org_id": 100, + "messages": [{"role": "user", "content": "Hello"}], + "options": {"model": "gpt-4o", "max_tokens": 1024, "system_prompt": "..."} +} + +// Response +{ + "success": true, + "provider": "openai", + "content": "Hello! How can I help you?", + "usage": {"prompt_tokens": 10, "completion_tokens": 8, "total_tokens": 18} +} +``` + +### POST /appi/llm/stream +SSE streaming. Each chunk sent as: +``` +event: chunk +data: {"chunk":"partial text here"} + +event: done +data: "[DONE]" + +event: error +data: {"error":"something went wrong"} +``` + +### GET /appi/llm/models?provider=openai&org_id=100 +```json +{ + "success": true, + "provider": "openai", + "models": [{"id": "gpt-4o", "label": "GPT-4o"}, ...] +} +``` + +### GET /appi/llm/providers?org_id=100 +```json +{ + "success": true, + "org_id": 100, + "providers": ["openai", "gemini"] +} +``` + +### POST /appi/llm/test +```json +// Request +{"provider": "openai", "org_id": 100} + +// Response +{"success": true, "provider": "openai", "latency_ms": 342, "error": ""} +``` + +## Frontend JS Client — public/assets/js/llm-client.js + +Load in any page that needs LLM functionality: +```php +$this->JavaScript[] = '/public/assets/js/llm-client.js'; +$this->view->JavaScript = $this->JavaScript; +``` + +### LLMClient.chat() +```js +const res = await LLMClient.chat({ + provider: 'openai', + orgId: 100, + messages: [{ role: 'user', content: 'Hello' }], + options: { model: 'gpt-4o', system_prompt: 'You are helpful.' } +}); +console.log(res.content); +``` + +### LLMClient.stream() +```js +const output = document.getElementById('output'); + +await LLMClient.stream({ + provider: 'anthropic', + orgId: 100, + messages: [{ role: 'user', content: 'Write a report on...' }], + options: { model: 'claude-sonnet-4-6' }, + onChunk: (chunk) => { output.innerHTML += chunk; }, + onDone: () => { console.log('Stream complete'); }, + onError: (err) => { console.error('Error:', err); } +}); +``` + +### LLMClient.getModels() +```js +const { models } = await LLMClient.getModels('openai', 100); +// models = [{ id: 'gpt-4o', label: 'GPT-4o' }, ...] +``` + +### LLMClient.getProviders() +```js +const { providers } = await LLMClient.getProviders(100); +// providers = ['openai', 'gemini'] +``` + +### LLMClient.test() +```js +const { success, latency_ms, error } = await LLMClient.test('openai', 100); +``` + +## Provider API Differences (internals) + +| Provider | Auth Header | System Prompt | Role names | Quirks | +|------------|-------------------------|-----------------------|-------------------------|---------------------------------| +| OpenAI | `Authorization: Bearer` | `role: system` msg | user / assistant | No temperature on o1/o3 models | +| Anthropic | `x-api-key` | Top-level `system` key| user / assistant | Requires `anthropic-version` header | +| Gemini | `?key=` query param | `systemInstruction` | user / model | Messages use `parts: [{text}]` | +| Mistral | `Authorization: Bearer` | `role: system` msg | user / assistant | None | +| DeepSeek | `Authorization: Bearer` | `role: system` msg | user / assistant | No temperature on R1 (reasoner) | + +## Available Models + +### OpenAI +- `gpt-4o` — GPT-4o (default) +- `gpt-4o-mini` — GPT-4o Mini +- `gpt-4-turbo` — GPT-4 Turbo +- `o1` — o1 +- `o1-mini` — o1 Mini +- `o3-mini` — o3 Mini + +### Anthropic +- `claude-sonnet-4-6` — Claude Sonnet 4.6 (default) +- `claude-opus-4-6` — Claude Opus 4.6 +- `claude-haiku-4-5-20251001` — Claude Haiku 4.5 + +### Gemini +- `gemini-2.5-flash` — Gemini 2.5 Flash (default) +- `gemini-2.5-flash-lite` — Gemini 2.5 Flash Lite +- `gemini-2.0-flash` — Gemini 2.0 Flash +- `gemini-1.5-pro` — Gemini 1.5 Pro + +### Mistral +- `mistral-large-latest` — Mistral Large (default) +- `mistral-small-latest` — Mistral Small +- `codestral-latest` — Codestral +- `open-mistral-nemo` — Mistral Nemo + +### DeepSeek +- `deepseek-chat` — DeepSeek Chat V3 (default) +- `deepseek-reasoner` — DeepSeek Reasoner R1 + +## Adding a New Provider + +1. Create `app/LLM/svcNewProvider.php` extending `LLMProvider` +2. Implement: `__construct(array $config)`, `sendPrompt()`, `streamPrompt()`, `getModels()`, `testConnection()` +3. Register in `LLMManager.php`: + - Add to `$providers` array: `'newprovider' => svcNewProvider::class` + - Add to `$metaKeys` array: `'newprovider' => 'llmNewProvider'` +4. Add JSON config row to `sp_orgs_meta` with `keyval = 'llmNewProvider'` diff --git a/api/app/Components/Role.php b/api/app/Components/Role.php new file mode 100644 index 0000000..6c2bdaa --- /dev/null +++ b/api/app/Components/Role.php @@ -0,0 +1,158 @@ + $PerMethod) { + if($permContrller == $controller) { + $AllowAccess = 1; + break; + } + } + + if($AllowAccess == 1) { + return true; + } else { + print_array('Deny Access ' . $controller); + } + + } + + + public static function AccessGranted($permission, $method) { + //print_array($permission); + + if($permission[0] == "*") { return true; } else { + if (in_array($method, $permission)) { + return true; + } else { + die('Access Denied - ' . $method); + } + } + } + + +//////////////////////// Manage Role Application //////////////////////////// + + public function Group() { + $Return = $this->db->select("SELECT COUNT(*) AS accounts, a.role_id, b.role_name AS RoleName, b.createdate AS CreateDate FROM usergen a JOIN roles b ON a.role_id=b.role_id GROUP BY a.role_id"); + return $Return; + } + + public function getPerms($data=""){ + if($data){ + $Search .= "WHERE 1"; + foreach($data as $key => $val) : + if(!$val) continue; + $string = strtolower($val); + $Search .= " AND {$key} LIKE '%{$string}%'"; + endforeach; + } + $Return = $this->db->select("SELECT * FROM permissions {$Search} ORDER BY perm_controller ASC"); + // sendlog("SELECT * FROM permissions {$Search} ORDER BY perm_controller ASC"); + return $Return; + } + + public function insertPerm($data){ + $data['perm_controller'] = strtolower($data['perm_controller']); + $data['perm_action'] = strtolower($data['perm_action']); + + $Return = $this->db->insert('permissions', $data); +// sendlog($Return); + return $Return; + } + + + public function deletePerm($id) { + $Return = $this->db->query("DELETE FROM permissions WHERE perm_id='{$id}'"); + return $Return; + } + + + public function updatePerm($data, $pid){ + $Return = $this->db->update('permissions', $data, "perm_id='{$pid}'"); + return $Return; + } + + + + // insert a new role + public function insertRole($data) { + $rdata['role_name'] = $data['groupname']; + $rdata['controller'] = $data['grouprole']; + $Return = $this->db->insert('roles', $rdata); + $getID = json_decode($Return); + + foreach($data['permission'] as $permid) : + $permData['role_id']=$getID->ID ; + $permData['perm_id']=$permid; + $RolePerms = $this->db->insert('role_perm', $permData); + endforeach; + + return $Return; + } + + // insert array of roles for specified user id + public function insertUserRoles($userid, $roles) { + $sql = "INSERT INTO user_role (userid, role_id) VALUES (:userid, :role_id)"; + $sth = $GLOBALS["DB"]->prepare($sql); + $sth->bindParam(":userid", $userid, PDO::PARAM_STR); + $sth->bindParam(":role_id", $role_id, PDO::PARAM_INT); + foreach ($roles as $role_id) { + $sth->execute(); + } + return true; + } + + // delete array of roles, and all associations + public function deleteRoles($roles) { + $sql = "DELETE t1, t2, t3 FROM roles as t1 + JOIN user_role as t2 on t1.role_id = t2.role_id + JOIN role_perm as t3 on t1.role_id = t3.role_id + WHERE t1.role_id = :role_id"; + $sth = $GLOBALS["DB"]->prepare($sql); + $sth->bindParam(":role_id", $role_id, PDO::PARAM_INT); + foreach ($roles as $role_id) { + $sth->execute(); + } + return true; + } + + + // delete ALL roles for specified user id + public function deleteUserRoles($userid) { + $sql = "DELETE FROM user_role WHERE userid = :userid"; + $sth = $GLOBALS["DB"]->prepare($sql); + return $sth->execute(array(":userid" => $userid)); + } + + // check if a user has a specific role + public function hasRole($role_name) { + return isset($this->roles[$role_name]); + } + + + + public static function createRole(){ + die('testing'); + } + + public function roleType(){ + $SQL = "SELECT role_id, role_name FROM roles"; + $Return = $this->db->select($SQL); + + return $Return; + } + + +} // End Class \ No newline at end of file diff --git a/api/app/Components/Template.php b/api/app/Components/Template.php new file mode 100644 index 0000000..4e07023 --- /dev/null +++ b/api/app/Components/Template.php @@ -0,0 +1,31 @@ +view->UserProfile = $profile; + + // Build tokenized slug for account settings link: "first-last.encryptedId" + if ($profile) { + $namePart = strtolower(trim(($profile['fname'] ?? '') . '-' . ($profile['lname'] ?? ''), '-')); + $namePart = preg_replace('/[^a-z0-9\-]/', '', $namePart); + $token = Functions::encryptData((string)$profile['userid'], HASH_PASSWORD_KEY); + $this->view->UserSlug = $namePart . '.' . $token; + } + } + } +} diff --git a/api/app/Core/Email.php b/api/app/Core/Email.php new file mode 100644 index 0000000..5d251cb --- /dev/null +++ b/api/app/Core/Email.php @@ -0,0 +1,82 @@ +SMTPDebug = 0; // Enable verbose debug output + $mail->isSMTP(); // Set mailer to use SMTP + $mail->Host = EMAILHOST; // Specify main and backup SMTP servers + $mail->SMTPAuth = true; // Enable SMTP authentication + $mail->Username = EMAILUSER; // SMTP username + $mail->Password = EMAILPASS; // SMTP password + $mail->SMTPSecure = 'tls'; // Enable TLS encryption, `ssl` also accepted + $mail->Port = 587; // TCP port to connect to + + //Recipients + $mail->setFrom('noreply@snoopi.io', 'Snoopi.io'); + //Set an alternative reply-to address + $mail->addReplyTo('support@snoopi.io', 'Snoopi.io'); + + #TODO: Need to have it loop multiple email addresses + $mail->AddBCC('carlosja80@gmail.com ', 'Carlos Arias'); // Add a recipient + + if($this->ccEmail) { + foreach($this->ccEmail as $ccdemail) { + $mail->AddCC(trim($ccdemail)); + } + } + $mail->addAddress(trim($this->SendToEmail)); // Name is optional + + //Attachments + if($this->Attachment) $mail->addAttachment($this->Attachment); + //$mail->addAttachment('/tmp/image.jpg', 'new.jpg'); // Optional name + + //Content + $mail->isHTML(true); // Set email format to HTML + $mail->Subject = $this->Subject; + $mail->Body = $this->BodyMessage; + $mail->AltBody = 'This is the body in plain text for non-HTML mail clients'; + + + $HTMLMessage = file_get_contents($_SERVER['DOCUMENT_ROOT'] . '/templates/emails/' . strtolower($this->Template) . '.html'); + foreach($this->EmailParam as $Key => $Value) { + $HTMLMessage = str_replace('%'. strtoupper($Key) .'%', $Value, $HTMLMessage); + } + + #Example: $HTMLMessage = str_replace('%ACTIVATION%', $this->EmailParam['activation'] , $HTMLMessage); + + + $mail->MsgHTML($HTMLMessage); + + $mail->send(); + return true; + } catch (Exception $e) { + echo 'Message could not be sent.'; + echo 'Mailer Error: ' . $mail->ErrorInfo; + } + } + + +} // End Class \ No newline at end of file diff --git a/api/app/Gateways/Stripe.php b/api/app/Gateways/Stripe.php new file mode 100644 index 0000000..418811d --- /dev/null +++ b/api/app/Gateways/Stripe.php @@ -0,0 +1,152 @@ + $description, + "name" => $name, + "source" => $token, + "email" => $email + )); + return ($Customer); + } catch (\Stripe\Error\Card $e){ + return ($e->getMessage()); + } + } + + + # Retrieve Subscription Information + public function RetrieveSubscription($str){ + try { + $subscription = \Stripe\Subscription::retrieve($str); + return($subscription); + } catch (\Stripe\Error\Base $e) { + return ($e->getMessage()); + } + + + } + + + #Create Subscription + # https://stripe.com/docs/api/subscriptions/create + // a Customer ID is required - this is retrieve from the CreateAccount method. + // a PlanID is required. This is grabbed from stripe plans (https://dashboard.stripe.com/subscriptions/products) + // My Recommendation is to use Method ($this->getPlans) to get the plans. Stripe doesn't make it easy to get the planid + + public function CreateSubscription($CustID, $PlanID){ + $CustomerSubscription = \Stripe\Subscription::create(array( + "customer" => $CustID, + "items" => array( + array( + "plan" => $PlanID, + "quantity" => 1, + ) + ) + )); + + return ($CustomerSubscription); + } + + + # Cancels Subscription + // Subscription ID is required + // https://stripe.com/docs/api/subscriptions/cancel + + public function CancelSubscription($str){ + try { + $subscription = \Stripe\Subscription::retrieve($str); + $subscription->cancel(); + return($subscription); + } catch (\Stripe\Error\Base $e) { + return ($e->getMessage()); + } + } + + + # Update Subscription. + # subscription code required from stripe. + // Provide with a new PlanID + // https://stripe.com/docs/api/subscriptions/update + + public function UpdateSubscription($SubID, $PlanID){ + $subscription = \Stripe\Subscription::retrieve($SubID); + + try { + $UpdateSubscription = \Stripe\Subscription::update($SubID, [ + 'cancel_at_period_end' => false, + 'proration_behavior' => 'always_invoice', + 'items' => [ + [ + 'id' => $subscription->items->data[0]->id, + 'plan' => $PlanID, + ], + ] + ]); + return($UpdateSubscription); + + } catch (\Stripe\Error\Base $e) { + return ($e->getMessage()); + } + } + + #This get customer information everything about the customer from Stripe + // Just Need Customer id which looks like cust_348398439 + // https://stripe.com/docs/api/customers/retrieve + public function getCustomerInfo($str){ + $Return = \Stripe\Customer::retrieve($str); + return $Return; + } + + + #Retrieve a list of plans + public function getPlans($limit = 10){ + $Return = \Stripe\Plan::all(["limit" => $limit]); + return $Return; + } + + + # Retrieves the Plans detail information + public function ProductInfo($planId){ + $Return = \Stripe\Plan::retrieve($planId); + return $Return; + } + + + # Update Credit Card Info + // Coming Soon + public function UpdateCard($token, $custID){ + try { + $cu = \Stripe\Customer::update( + $custID, // stored in your application + [ + 'source' => $token // obtained with Checkout + ] + ); + + return $cu; + //$Message['Success'] = "Your card details have been updated!"; + } + catch(\Stripe\Error\Card $e) { + return ($e->getMessage()); + // $e->getJsonBody(); // show fulll error message + } + } + +} // End Class diff --git a/api/app/Helpers/Auth.php b/api/app/Helpers/Auth.php new file mode 100644 index 0000000..d1e5887 --- /dev/null +++ b/api/app/Helpers/Auth.php @@ -0,0 +1,119 @@ + 0, + 'trigval' => 'security.api_auth_failed', + 'msgval' => 'Invalid API key presented', + 'status' => 1, + ]); + $Return = ['code' => '401', 'msg' => 'Unauthorized: Api Invalid']; + header('Content-Type: application/json;charset=UTF-8'); + echo json_encode($Return); + exit(); + } + + $Return['code'] = '1'; + $Return['userid'] = $ApiUser['userid']; + return $Return; + } + } + + // ─── Session Login Check ────────────────────────────────────────────────── + + public static function handleLogin() { + $logged = $_SESSION['login']; + if ($logged == false) { + session_destroy(); + header('location: /login'); + exit; + } + return $logged; + } + + // ─── Load Permissions into Session ─────────────────────────────────────── + // Call this once after successful login. + + public static function loadPermissions(int $roleId): void { + $rows = \Db::select(" + SELECT p.perm_controller, p.perm_action + FROM sp_role_perm rp + JOIN sp_permissions p ON rp.perm_id = p.perm_id + WHERE rp.role_id = ? + ", [$roleId]); + + $permissions = []; + foreach ($rows as $row) { + $permissions[] = $row['perm_controller'] . '.' . $row['perm_action']; + } + + $_SESSION['permissions'] = $permissions; + } + + // ─── Permission Checks ──────────────────────────────────────────────────── + + /** + * Returns true if the current user has the given permission. + * Permission format: 'controller.action' e.g. 'users.create' + */ + public static function can(string $permission): bool { + $permissions = $_SESSION['permissions'] ?? []; + return in_array($permission, $permissions); + } + + /** + * Halts execution with a 403 JSON response if the user lacks the permission. + * Use in API controllers. + */ + public static function requirePermission(string $permission): void { + if (!self::can($permission)) { + Logger::Entry([ + 'userid' => $_SESSION['login']['userid'] ?? 0, + 'trigval' => 'security.permission_denied', + 'msgval' => 'Permission denied: ' . $permission, + 'status' => 1, + ]); + http_response_code(403); + header('Content-Type: application/json'); + echo json_encode(['success' => false, 'error' => 'Permission denied']); + exit; + } + } + + /** + * Returns all permissions for the current user as an array. + */ + public static function getPermissions(): array { + return $_SESSION['permissions'] ?? []; + } + + /** + * Returns a JS-safe array of the current user's permissions. + * Use in header partial to inject into window.AppUser. + */ + public static function getPermissionsJson(): string { + return json_encode(self::getPermissions()); + } + +} diff --git a/api/app/Helpers/DB.php b/api/app/Helpers/DB.php new file mode 100644 index 0000000..b524e26 --- /dev/null +++ b/api/app/Helpers/DB.php @@ -0,0 +1,366 @@ + PDO::ERRMODE_EXCEPTION, + PDO::MYSQL_ATTR_INIT_COMMAND => "SET NAMES utf8; SET time_zone = 'America/New_York'" + ]; + + /** + * Set connection information + * + * @example Db::setConnectionInfo('mysql', 'dbname', 'username', 'password', 'hostname', 'connectionName'); + */ + public static function setConnectionInfo( + $driver, + $dbname, + $username, + $password, + $hostname = 'localhost', + $connectionName = 'default' + ) { + self::$_connections[$connectionName] = [ + 'driver' => $driver, + 'connectionString' => "{$driver}:dbname={$dbname};host={$hostname}", + 'username' => $username, + 'password' => $password, + ]; + + if (self::$_activeConnection === null) { + self::$_activeConnection = $connectionName; + } + } + + /** + * Switch to a different connection + */ + public static function useConnection($connectionName) + { + if (!isset(self::$_connections[$connectionName])) { + throw new \Exception("Connection '{$connectionName}' does not exist."); + } + self::$_activeConnection = $connectionName; + } + + /** + * Get the current PDO object + */ + public static function getPDOObject() + { + return self::_getConnection(); + } + + /** + * Execute a statement and returns the first row + */ + public static function getRow($sql, $params = []) + { + $statement = self::_query($sql, $params); + return $statement->fetch(self::$_fetchMode); + } + + /** + * Execute a statement and returns all rows + */ + public static function select($sql, $params = []) + { + $statement = self::_query($sql, $params); + return $statement->fetchAll(self::$_fetchMode); + } + + + /** + * Insert a new row into the database + */ + public static function insert($table, $data) + { + $pdo = self::_getConnection(); + + ksort($data); + $fieldNames = implode('`, `', array_keys($data)); + $fieldValues = ':' . implode(', :', array_keys($data)); + + try { + $sth = $pdo->prepare("INSERT INTO $table (`$fieldNames`) VALUES ($fieldValues)"); + + foreach ($data as $key => $value) { + $sth->bindValue(":$key", $value); + } + + $sth->execute(); + + return json_encode([ + 'Code' => 1, + 'Message' => 'Created', + 'ID' => $pdo->lastInsertId() + ]); + } catch (\PDOException $e) { + return json_encode([ + 'Code' => 0, + 'Message' => 'Error Creating Entry: ' . $e->getMessage() + ]); + } + } + + /** + * Update existing rows in the database + */ + public static function update($table, $data, $where, $whereParams = []) + { + $pdo = self::_getConnection(); + + ksort($data); + $usesPositionalWhere = str_contains($where, '?'); + + if ($usesPositionalWhere) { + $fieldDetails = implode(', ', array_map(function($key) { + return "`$key` = ?"; + }, array_keys($data))); + } else { + $fieldDetails = implode(', ', array_map(function($key) { + return "`$key` = :set_$key"; + }, array_keys($data))); + } + + try { + $sth = $pdo->prepare("UPDATE $table SET $fieldDetails WHERE $where"); + + if ($usesPositionalWhere) { + // Single positional style: SET values first, WHERE values after. + $params = array_values($data); + if (!empty($whereParams)) { + $params = array_merge($params, array_values($whereParams)); + } + $sth->execute($params); + } else { + // Single named style for SET; allow named WHERE params if supplied. + $params = []; + foreach ($data as $key => $value) { + $params["set_$key"] = $value; + } + foreach ($whereParams as $key => $value) { + $params[ltrim((string)$key, ':')] = $value; + } + $sth->execute($params); + } + + $count = $sth->rowCount(); + + return json_encode([ + 'Code' => ($count > 0) ? 1 : 0, + 'Rows' => $count, + 'Message' => ($count > 0) ? 'Updated' : 'No Records Updated' + ]); + } catch (\PDOException $e) { + return json_encode([ + 'Code' => $e->errorInfo[1], + 'Message' => 'Error Updating Database: ' . $e->getMessage() + ]); + } + } + + /** + * Execute a statement and returns number of affected rows + */ + public static function execute($sql, $params = []) + { + $statement = self::_query($sql, $params); + return $statement->rowCount(); + } + + + + /** + * Execute a statement and returns a single value + */ + public static function getValue($sql, $params = []) + { + $statement = self::_query($sql, $params); + return $statement->fetchColumn(0); + } + + /** + * Set PDO fetch mode + */ + public static function setFetchMode($fetchMode) + { + self::$_fetchMode = $fetchMode; + } + + /** + * Begin a transaction + */ + public static function beginTransaction() + { + self::_getConnection()->beginTransaction(); + } + + /** + * Commit a transaction + */ + public static function commitTransaction() + { + self::_getConnection()->commit(); + } + + /** + * Rollback a transaction + */ + public static function rollbackTransaction() + { + self::_getConnection()->rollBack(); + } + + /** + * Backward-compatible aliases used across controllers. + */ + public static function commit() + { + self::commitTransaction(); + } + + public static function rollback() + { + self::rollbackTransaction(); + } + + /** + * Set PDO driver options + */ + public static function setDriverOptions(array $options) + { + self::$_driverOptions = $options; + } + + /** + * Get or create the PDO connection + */ + private static function _getConnection() + { + $connectionInfo = self::$_connections[self::$_activeConnection]; + + if (!isset($connectionInfo['pdo'])) { + $connectionInfo['pdo'] = new \PDO( + $connectionInfo['connectionString'], + $connectionInfo['username'], + $connectionInfo['password'], + self::$_driverOptions + ); + self::$_connections[self::$_activeConnection] = $connectionInfo; + } + + return $connectionInfo['pdo']; + } + + /** + * Prepare and execute a PDO statement + */ + private static function _query($sql, $params = []) + { + $pdo = self::_getConnection(); + $statement = $pdo->prepare($sql); + + if (!$statement) { + $errorInfo = $pdo->errorInfo(); + throw new \PDOException("Database error [{$errorInfo[0]}]: {$errorInfo[2]}, driver error code is $errorInfo[1]"); + } + + if (!$statement->execute($params) || $statement->errorCode() != '00000') { + $errorInfo = $statement->errorInfo(); + throw new \PDOException("Database error [{$errorInfo[0]}]: {$errorInfo[2]}, driver error code is $errorInfo[1]"); + } + + return $statement; + } + + /** + * Delete rows from the database + */ + public static function delete($table, $where) + { + $pdo = self::_getConnection(); + + // Build the WHERE clause dynamically + $whereConditions = implode(' AND ', array_map(function($key) { + return "`$key` = :$key"; + }, array_keys($where))); + + try { + $sth = $pdo->prepare("DELETE FROM $table WHERE $whereConditions"); + + // Bind the parameters from the where array + foreach ($where as $key => $value) { + $sth->bindValue(":$key", $value); + } + + $sth->execute(); + $count = $sth->rowCount(); + + return json_encode([ + 'Code' => ($count > 0) ? 1 : 0, + 'Rows' => $count, + 'Message' => ($count > 0) ? 'Deleted' : 'No Records Deleted' + ]); + } catch (\PDOException $e) { + return json_encode([ + 'Code' => $e->errorInfo[1], + 'Message' => 'Error Deleting From Database: ' . $e->getMessage() + ]); + } + } + + public static function batchInsert($table, array $data) + { + if (empty($data)) { + return json_encode([ + 'Code' => 0, + 'Message' => 'No data provided for insertion' + ]); + } + + $pdo = self::_getConnection(); + + // Assume all rows have the same structure as the first row + $firstRow = reset($data); + $columns = array_keys($firstRow); + $columnString = '`' . implode('`, `', $columns) . '`'; + + // Create placeholders for each row + $rowPlaceholder = '(' . implode(', ', array_fill(0, count($columns), '?')) . ')'; + $valuePlaceholders = implode(', ', array_fill(0, count($data), $rowPlaceholder)); + + $sql = "INSERT INTO $table ($columnString) VALUES $valuePlaceholders"; + + try { + $stmt = $pdo->prepare($sql); + + // Flatten the data array and bind values + $values = []; + foreach ($data as $row) { + foreach ($columns as $column) { + $values[] = $row[$column]; + } + } + + $stmt->execute($values); + + $insertedCount = $stmt->rowCount(); + + return json_encode([ + 'Code' => 1, + 'Message' => 'Batch insert successful', + 'InsertedRows' => $insertedCount + ]); + } catch (\PDOException $e) { + return json_encode([ + 'Code' => 0, + 'Message' => 'Error performing batch insert: ' . $e->getMessage() + ]); + } + } +} diff --git a/api/app/Helpers/Debug.php b/api/app/Helpers/Debug.php new file mode 100644 index 0000000..6302952 --- /dev/null +++ b/api/app/Helpers/Debug.php @@ -0,0 +1,22 @@ +'; + print_r($f); + echo ''; + + + if($kill){ + die('-- Debugging --'); + } + } + + + public static function sendlog($string){ + error_log($string, 0); + } + +} // End Class \ No newline at end of file diff --git a/api/app/Helpers/Format.php b/api/app/Helpers/Format.php new file mode 100644 index 0000000..37f6af6 --- /dev/null +++ b/api/app/Helpers/Format.php @@ -0,0 +1,84 @@ + 10) { + $countryCode = substr($phoneNumber, 0, strlen($phoneNumber)-10); + $areaCode = substr($phoneNumber, -10, 3); + $nextThree = substr($phoneNumber, -7, 3); + $lastFour = substr($phoneNumber, -4, 4); + + $phoneNumber = '+'.$countryCode.' ('.$areaCode.') '.$nextThree.'-'.$lastFour; + } + else if(strlen($phoneNumber) == 10) { + $areaCode = substr($phoneNumber, 0, 3); + $nextThree = substr($phoneNumber, 3, 3); + $lastFour = substr($phoneNumber, 6, 4); + + $phoneNumber = '('.$areaCode.') '.$nextThree.'-'.$lastFour; + } + else if(strlen($phoneNumber) == 7) { + $nextThree = substr($phoneNumber, 0, 3); + $lastFour = substr($phoneNumber, 3, 4); + + $phoneNumber = $nextThree.'-'.$lastFour; + } + + return $phoneNumber; + } + + public static function formatCurrency($amount='', $thousands='') { + // Convert to float and divide by 100 + $formattedAmount = number_format($amount / 100, 2, '.', $thousands); + + return $formattedAmount; + } + + + /** + * Convert Equifax date strings to MM/DD/YYYY display format. + * Handles: YYYYMMDD, YYYY-MM-DD, MM/YYYY, MM/DD/YYYY passthrough. + * Returns null on empty/null input. + */ + public static function eqFormatDate($date) { + if (empty($date)) return null; + $date = trim((string)$date); + + // YYYYMMDD (8 digits) + if (preg_match('/^\d{8}$/', $date)) { + $d = \DateTime::createFromFormat('Ymd', $date); + return $d ? $d->format('m/d/Y') : null; + } + + // YYYY-MM-DD + if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $date)) { + $d = \DateTime::createFromFormat('Y-m-d', $date); + return $d ? $d->format('m/d/Y') : null; + } + + // MM/YYYY — partial date, use first of month + if (preg_match('/^\d{2}\/\d{4}$/', $date)) { + $d = \DateTime::createFromFormat('m/Y', $date); + return $d ? $d->format('m/01/Y') : null; + } + + // MM/DD/YYYY — already correct format, passthrough + if (preg_match('/^\d{2}\/\d{2}\/\d{4}$/', $date)) { + return $date; + } + + // Fallback: try strtotime + $ts = strtotime($date); + return $ts ? date('m/d/Y', $ts) : null; + } + +} // End Class \ No newline at end of file diff --git a/api/app/Helpers/Functions.php b/api/app/Helpers/Functions.php new file mode 100644 index 0000000..daebe19 --- /dev/null +++ b/api/app/Helpers/Functions.php @@ -0,0 +1,287 @@ + $fQuery) { + $SQLWhere .= "AND {$key} like '%{$fQuery}%' "; + } + $Return = \Db::getResult("select * from sp_roles WHERE 1 {$SQLWhere}"); + return $Return; + } + + public static function getUserInfo($userid=''){ + if(!$userid) die('UserID Required...'); + $Return = \Db::getRow("SELECT * FROM sp_users a JOIN sp_roles b ON a.role_id=b.role_id WHERE userid='{$userid}'"); + return $Return; + } + + public static function getStates(){ + + } + + public static function getCountries(){ + + } + + public static function isSysAdmin() { + // \Session::get('login'); + } + + /** + * Check if the user exist in the database by email. + * @param $f + * @return array|false + */ + public static function CheckUserExist($f){ + $j = \Db::getRow("select * from sp_users where email ='{$f}'"); + return (($j) ? $j : false); + } + + /** + * Get either a Gravatar URL or complete image tag for a specified email address. + * + * @param string $email The email address + * @param string $s Size in pixels, defaults to 80px [ 1 - 2048 ] + * @param string $d Default imageset to use [ 404 | mp | identicon | monsterid | wavatar ] + * @param string $r Maximum rating (inclusive) [ g | pg | r | x ] + * @param boole $img True to return a complete IMG tag False for just the URL + * @param array $atts Optional, additional key/value attributes to include in the IMG tag + * @return String containing either just a URL or a complete image tag + * @source https://gravatar.com/site/implement/images/php/ + */ + public static function getAvatar($email, $s = 80, $d = 'mp', $r = 'g', $img = false, $atts = array()) { + + $url = 'https://www.gravatar.com/avatar/'; + $url .= md5( strtolower( trim( $email ) ) ); + $url .= "?s=$s&d=$d&r=$r"; + if ( $img ) { + $url = ' $val ) + $url .= ' ' . $key . '="' . $val . '"'; + $url .= ' />'; + } + return $url; + + } + + /** Create Encryption for security in the application + * + * @param string $st The string that needs to be encrypted + * Usage: echo \Functions::Encrypt("Carlos"); + * + */ + public static function Encrypt($st){ + $encrypted = openssl_encrypt($st, 'AES-128-CTR', HASH_PASSWORD_KEY, OPENSSL_RAW_DATA, '1234567890123456'); + return base64_encode($encrypted); + } + + + /** Create Encryption for security in the application + * + * @param string $st The string that needs to be encrypted + * Usage: echo \Functions::Decrypt("a/pxWERALz2YMd4l32U3ew=="); + * + */ + public static function Decrypt($st){ + $decrypted = openssl_decrypt(base64_decode($st), 'AES-128-CTR', HASH_PASSWORD_KEY, OPENSSL_RAW_DATA, '1234567890123456'); + return $decrypted; + } + + public static function DefualtRole(){ + Return \Db::getRow("SELECT role_id, role_name FROM sp_roles WHERE defaultrole='1'"); + } + + + public static function encryptData($data, $key) { + $ivLength = openssl_cipher_iv_length($cipher = 'AES-256-CBC'); + $iv = openssl_random_pseudo_bytes($ivLength); + $encrypted = openssl_encrypt($data, $cipher, $key, OPENSSL_RAW_DATA, $iv); + // Convert to hex to ensure binary safety, then to base64 to make it URL-friendly + $encryptedBase64 = base64_encode($iv . $encrypted); + // Replace URL-unfriendly characters from base64 encoding + $urlSafeEncrypted = strtr($encryptedBase64, '+/', '-_'); + // Optionally remove '=' if present + $urlSafeEncrypted = rtrim($urlSafeEncrypted, '='); + return $urlSafeEncrypted; + } + + public static function decryptData($urlSafeEncrypted, $key) { + $ivLength = openssl_cipher_iv_length($cipher = 'AES-256-CBC'); + // Reverse the URL-safe transformations + $base64Encrypted = strtr($urlSafeEncrypted, '-_', '+/'); + // Decode from base64 to binary + $binaryData = base64_decode($base64Encrypted); + $iv = substr($binaryData, 0, $ivLength); + $encrypted = substr($binaryData, $ivLength); + $decrypted = openssl_decrypt($encrypted, $cipher, $key, OPENSSL_RAW_DATA, $iv); + return $decrypted; + } + + + /*// Usage + $key = 'your-256-bit-secret-key'; // Make sure to use a secure key + $originalData = "Your secret data"; + + $encryptedData = encryptData($originalData, $key); + echo "Encrypted: " . $encryptedData . "\n"; + + $decryptedData = decryptData($encryptedData, $key); + echo "Decrypted: " . $decryptedData . "\n"; + */ + + /** + * Resolve an org from a tokenized slug (e.g. "acme-corp.AbcToken"). + * Decrypts the token, validates it, and returns the active org row. + * Calls die() with an error message on any failure. + * + * @param string $slug + * @return array + */ + public static function resolveOrg($slug) { + if (!$slug || strpos($slug, '.') === false) die('Invalid link'); + [, $token] = explode('.', $slug, 2); + $orgid = self::decryptData($token, HASH_PASSWORD_KEY); + if (!$orgid || !is_numeric($orgid)) die('Invalid link'); + $org = \Db::getRow("SELECT * FROM sp_orgs WHERE orgid = ? AND active = 1", [$orgid]); + if (!$org) die('Organization not found'); + return $org; + } + + /** + * Resolve a user from a tokenized slug (e.g. "john-doe.AbcToken"). + * Decrypts the token, validates it, and returns the user row (with role). + * Calls die() with an error message on any failure. + * + * @param string $slug + * @return array + */ + public static function resolveUser($slug) { + if (!$slug || strpos($slug, '.') === false) die('Invalid link'); + [, $token] = explode('.', $slug, 2); + $userid = self::decryptData($token, HASH_PASSWORD_KEY); + if (!$userid || !is_numeric($userid)) die('Invalid link'); + $user = \Db::getRow("SELECT u.*, r.role_name FROM sp_users u LEFT JOIN sp_roles r ON u.role_id = r.role_id WHERE u.userid = ?", [$userid]); + if (!$user) die('User not found'); + return $user; + } + + // This gets the system default organization. + // Ideally for single organization applications with users going straight into the org or for api calls that have no business name assigned to the field. + public static function DefaultOrg(){ + Return \Db::getRow("SELECT orgid, catid, name FROM sp_orgs WHERE defaultorg='1'"); + } + + public static function randomPassword() { + $alphabet = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890'; + $pass = array(); //remember to declare $pass as an array + $alphaLength = strlen($alphabet) - 1; //put the length -1 in cache + for ($i = 0; $i < 8; $i++) { + $n = rand(0, $alphaLength); + $pass[] = $alphabet[$n]; + } + return implode($pass); //turn the array into a string + } + + + + public static function ip_in_range($ip, $range) { + if (strpos($range, '/') == false) + $range .= '/32'; + + // $range is in IP/CIDR format eg 127.0.0.1/24 + list($range, $netmask) = explode('/', $range, 2); + $range_decimal = ip2long($range); + $ip_decimal = ip2long($ip); + $wildcard_decimal = pow(2, (32 - $netmask)) - 1; + $netmask_decimal = ~ $wildcard_decimal; + return (($ip_decimal & $netmask_decimal) == ($range_decimal & $netmask_decimal)); + } + + public static function CheckCloudFlare($ip) { + $cf_ips = array( + '173.245.48.0/20', + '103.21.244.0/22', + '103.22.200.0/22', + '103.31.4.0/22', + '141.101.64.0/18', + '108.162.192.0/18', + '190.93.240.0/20', + '188.114.96.0/20', + '197.234.240.0/22', + '198.41.128.0/17', + '162.158.0.0/15', + '104.16.0.0/13', + '104.24.0.0/14', + '172.64.0.0/13', + '131.0.72.0/22' + ); + $is_cf_ip = false; + foreach ($cf_ips as $cf_ip) { + if (self::ip_in_range($ip, $cf_ip)) { + + $is_cf_ip = true; + break; + } + } return $is_cf_ip; + } + + public static function in_array_all($needles, $haystack) { + return empty(array_diff($needles, $haystack)); + } + + public static function hasOrgAdminAccess($userArray) { + foreach ($userArray as $org) { + if ($org['OrgAdmin'] == 1) { + return true; + } + } + return false; + } + + + static function getDomainIP($domain) { + // Validate domain format + if (!filter_var($domain, FILTER_VALIDATE_DOMAIN)) { + return "Invalid domain format"; + } + + $result = []; + + try { + // Method 1: Get single IP using gethostbyname() + $ip = gethostbyname($domain); + if ($ip !== $domain) { + $result['primary_ip'] = $ip; + } + + // Method 2: Get all DNS A records + $dns_records = dns_get_record($domain, DNS_A); + if (!empty($dns_records)) { + $result['dns_records'] = array_column($dns_records, 'ip'); + } + + // Method 3: Get all IPv4 addresses + if (checkdnsrr($domain, 'A')) { + $ip_array = gethostbynamel($domain); + if ($ip_array) { + $result['all_ipv4'] = $ip_array; + } + } + + if (empty($result)) { + return "Could not resolve domain"; + } + + return $result; + + } catch (Exception $e) { + return "Error: " . $e->getMessage(); + } + } + + + +} // End Class + diff --git a/api/app/Helpers/GlobalConst.php b/api/app/Helpers/GlobalConst.php new file mode 100644 index 0000000..869b79b --- /dev/null +++ b/api/app/Helpers/GlobalConst.php @@ -0,0 +1,16 @@ +format('Ymd') : $dob; + } + + // Fallback + $ts = strtotime($dob); + return $ts ? date('Ymd', $ts) : $dob; + } + + /** + * Create a stable consumer profile ID from DOB + SSN. + * Returns first 32 chars of HMAC-SHA256. + */ + public static function createConsumerProfileId($dob, $ssn, $salt) { + $normalized = self::normalizeDob($dob) . preg_replace('/[^0-9]/', '', $ssn); + return substr(hash_hmac('sha256', $normalized, $salt), 0, 32); + } +} \ No newline at end of file diff --git a/api/app/Helpers/Logger.php b/api/app/Helpers/Logger.php new file mode 100644 index 0000000..6f28f0d --- /dev/null +++ b/api/app/Helpers/Logger.php @@ -0,0 +1,72 @@ + 10) { + $countryCode = substr($phoneNumber, 0, strlen($phoneNumber)-10); + $areaCode = substr($phoneNumber, -10, 3); + $nextThree = substr($phoneNumber, -7, 3); + $lastFour = substr($phoneNumber, -4, 4); + + $phoneNumber = '+'.$countryCode.' ('.$areaCode.') '.$nextThree.'-'.$lastFour; + } + else if(strlen($phoneNumber) == 10) { + $areaCode = substr($phoneNumber, 0, 3); + $nextThree = substr($phoneNumber, 3, 3); + $lastFour = substr($phoneNumber, 6, 4); + + $phoneNumber = '('.$areaCode.') '.$nextThree.'-'.$lastFour; + } + else if(strlen($phoneNumber) == 7) { + $nextThree = substr($phoneNumber, 0, 3); + $lastFour = substr($phoneNumber, 3, 4); + + $phoneNumber = $nextThree.'-'.$lastFour; + } + + return $phoneNumber; + } + +} // End Class \ No newline at end of file diff --git a/api/app/Helpers/Pagination.php b/api/app/Helpers/Pagination.php new file mode 100644 index 0000000..c0ba21f --- /dev/null +++ b/api/app/Helpers/Pagination.php @@ -0,0 +1,177 @@ +_instance = $instance; + $this->_perPage = $perPage; + $this->set_instance(); + } + /** + * get_start + * + * creates the starting point for limiting the dataset + * @return numeric + */ + public function get_start(){ + return ($this->_page * $this->_perPage) - $this->_perPage; + } + /** + * set_instance + * + * sets the instance parameter, if numeric value is 0 then set to 1 + * + * @var numeric + */ + private function set_instance(){ + $this->_page = (int) (!isset($_GET[$this->_instance]) ? 1 : $_GET[$this->_instance]); + $this->_page = ($this->_page == 0 ? 1 : $this->_page); + } + /** + * set_total + * + * collect a numberic value and assigns it to the totalRows + * + * @var numeric + */ + public function set_total($_totalRows){ + $this->_totalRows = $_totalRows; + } + /** + * get_limit + * + * returns the limit for the data source, calling the get_start method and passing in the number of items perp page + * + * @return string + */ + public function get_limit(){ + return "LIMIT ".$this->get_start().",$this->_perPage"; + } + /** + * page_links + * + * create the html links for navigating through the dataset + * + * @var sting $path optionally set the path for the link + * @var sting $ext optionally pass in extra parameters to the GET + * @return string returns the html menu + */ + public function page_links($path='?',$ext=null) + { + $adjacents = "2"; + $prev = $this->_page - 1; + $next = $this->_page + 1; + $lastpage = ceil($this->_totalRows/$this->_perPage); + $lpm1 = $lastpage - 1; + $pagination = ""; + if($lastpage > 1) + { + $pagination .= "
    "; + if ($this->_page > 1) + $pagination.= "
  • Previous
  • "; + else + $pagination.= "Previous"; + if ($lastpage < 7 + ($adjacents * 2)) + { + for ($counter = 1; $counter <= $lastpage; $counter++) + { + if ($counter == $this->_page) + $pagination.= "
  • $counter
  • "; + else + $pagination.= "
  • $counter
  • "; + } + } + elseif($lastpage > 5 + ($adjacents * 2)) + { + if($this->_page < 1 + ($adjacents * 2)) + { + for ($counter = 1; $counter < 4 + ($adjacents * 2); $counter++) + { + if ($counter == $this->_page) + $pagination.= "
  • $counter
  • "; + else + $pagination.= "
  • $counter
  • "; + } + $pagination.= "..."; + $pagination.= "
  • $lpm1
  • "; + $pagination.= "
  • $lastpage
  • "; + } + elseif($lastpage - ($adjacents * 2) > $this->_page && $this->_page > ($adjacents * 2)) + { + $pagination.= "
  • 1
  • "; + $pagination.= "
  • 2
  • "; + $pagination.= "..."; + for ($counter = $this->_page - $adjacents; $counter <= $this->_page + $adjacents; $counter++) + { + if ($counter == $this->_page) + $pagination.= "$counter"; + else + $pagination.= "
  • $counter
  • "; + } + $pagination.= ".."; + $pagination.= "
  • $lpm1
  • "; + $pagination.= "
  • $lastpage
  • "; + } + else + { + $pagination.= "
  • 1
  • "; + $pagination.= "
  • 2
  • "; + $pagination.= ".."; + for ($counter = $lastpage - (2 + ($adjacents * 2)); $counter <= $lastpage; $counter++) + { + if ($counter == $this->_page) + $pagination.= "$counter"; + else + $pagination.= "
  • $counter
  • "; + } + } + } + if ($this->_page < $counter - 1) + $pagination.= "
  • Next
  • "; + else + $pagination.= "
  • Next
  • "; + $pagination.= "
\n"; + } + return $pagination; + } +} \ No newline at end of file diff --git a/api/app/Helpers/PluginManager.php b/api/app/Helpers/PluginManager.php new file mode 100644 index 0000000..4d06470 --- /dev/null +++ b/api/app/Helpers/PluginManager.php @@ -0,0 +1,353 @@ +match(). + */ + public static function boot(AltoRouter $router): void { + self::init(); + + $plugins = Db::select( + "SELECT slug FROM sp_plugins WHERE enabled = 1", + [] + ); + + if (!$plugins) return; + + foreach ($plugins as $row) { + $slug = $row['slug']; + $manifestPath = self::$pluginsDir . '/' . $slug . '/plugin.php'; + if (!file_exists($manifestPath)) continue; + + $manifest = require $manifestPath; + foreach ($manifest['routes'] ?? [] as $rule) { + // Each rule: [method, pattern, target, name] + if (count($rule) >= 3) { + $router->map($rule[0], $rule[1], $rule[2], $rule[3] ?? null); + } + } + } + } + + // ─── Install ────────────────────────────────────────────────────────────── + + /** + * Install a plugin: create symlinks, run install.php, upsert DB row (enabled=0). + */ + public static function install(string $slug): array { + self::init(); + + $slug = self::sanitizeSlug($slug); + if (!$slug) return ['success' => false, 'error' => 'Invalid slug']; + + $pluginDir = self::$pluginsDir . '/' . $slug; + if (!is_dir($pluginDir)) { + return ['success' => false, 'error' => "Plugin directory not found: {$slug}"]; + } + + $manifestPath = $pluginDir . '/plugin.php'; + if (!file_exists($manifestPath)) { + return ['success' => false, 'error' => "plugin.php manifest missing for: {$slug}"]; + } + + $manifest = require $manifestPath; + + // Create symlinks + $symlinkResult = self::createSymlinks($slug, $manifest); + if (!$symlinkResult['success']) return $symlinkResult; + + // Run install.php + $installScript = $pluginDir . '/install.php'; + if (file_exists($installScript)) { + try { + require $installScript; + } catch (Throwable $e) { + return ['success' => false, 'error' => 'install.php failed: ' . $e->getMessage()]; + } + } + + // Upsert sp_plugins row (enabled=0 — admin must explicitly enable) + $existing = Db::getRow("SELECT plugin_id FROM sp_plugins WHERE slug = ?", [$slug]); + if ($existing) { + Db::update('sp_plugins', [ + 'name' => $manifest['name'] ?? $slug, + 'version' => $manifest['version'] ?? '1.0.0', + 'description' => $manifest['description'] ?? '', + 'author' => $manifest['author'] ?? '', + 'updated_at' => date('Y-m-d H:i:s'), + ], 'slug = ?', [$slug]); + } else { + Db::insert('sp_plugins', [ + 'slug' => $slug, + 'name' => $manifest['name'] ?? $slug, + 'version' => $manifest['version'] ?? '1.0.0', + 'description' => $manifest['description'] ?? '', + 'author' => $manifest['author'] ?? '', + 'enabled' => 0, + 'installed_at' => date('Y-m-d H:i:s'), + 'updated_at' => date('Y-m-d H:i:s'), + ]); + } + + return ['success' => true, 'message' => "Plugin '{$slug}' installed. Enable it to activate."]; + } + + // ─── Enable ─────────────────────────────────────────────────────────────── + + /** + * Enable a plugin: ensure symlinks exist, set enabled=1. + */ + public static function enable(string $slug): array { + self::init(); + + $slug = self::sanitizeSlug($slug); + $row = Db::getRow("SELECT plugin_id FROM sp_plugins WHERE slug = ?", [$slug]); + if (!$row) return ['success' => false, 'error' => "Plugin '{$slug}' is not installed"]; + + $manifest = self::loadManifest($slug); + if (!$manifest) return ['success' => false, 'error' => "Cannot load manifest for '{$slug}'"]; + + $symlinkResult = self::createSymlinks($slug, $manifest); + if (!$symlinkResult['success']) return $symlinkResult; + + Db::update('sp_plugins', ['enabled' => 1, 'updated_at' => date('Y-m-d H:i:s')], 'slug = ?', [$slug]); + + return ['success' => true, 'message' => "Plugin '{$slug}' enabled"]; + } + + // ─── Disable ────────────────────────────────────────────────────────────── + + /** + * Disable a plugin: remove symlinks, set enabled=0. + * Symlink removal causes natural 404 without any guard code. + */ + public static function disable(string $slug): array { + self::init(); + + $slug = self::sanitizeSlug($slug); + $row = Db::getRow("SELECT plugin_id FROM sp_plugins WHERE slug = ?", [$slug]); + if (!$row) return ['success' => false, 'error' => "Plugin '{$slug}' is not installed"]; + + self::removeSymlinks($slug); + + Db::update('sp_plugins', ['enabled' => 0, 'updated_at' => date('Y-m-d H:i:s')], 'slug = ?', [$slug]); + + return ['success' => true, 'message' => "Plugin '{$slug}' disabled"]; + } + + // ─── Uninstall ──────────────────────────────────────────────────────────── + + /** + * Uninstall a plugin: remove symlinks, run uninstall.php, delete DB row. + */ + public static function uninstall(string $slug): array { + self::init(); + + $slug = self::sanitizeSlug($slug); + + self::removeSymlinks($slug); + + $uninstallScript = self::$pluginsDir . '/' . $slug . '/uninstall.php'; + if (file_exists($uninstallScript)) { + try { + require $uninstallScript; + } catch (Throwable $e) { + // Log but don't abort — clean up DB anyway + } + } + + Db::delete('sp_plugins', ['slug' => $slug]); + + return ['success' => true, 'message' => "Plugin '{$slug}' uninstalled"]; + } + + // ─── Discover ───────────────────────────────────────────────────────────── + + /** + * Scan plugins/ directory and merge with DB rows. + * Returns array of plugin info for the admin UI. + */ + public static function discover(): array { + self::init(); + + // Get all DB rows keyed by slug + $dbRows = []; + $rows = Db::select("SELECT * FROM sp_plugins", []); + foreach ($rows as $row) { + $dbRows[$row['slug']] = $row; + } + + $plugins = []; + + if (!is_dir(self::$pluginsDir)) return $plugins; + + $dirs = glob(self::$pluginsDir . '/*/plugin.php'); + if (!$dirs) return $plugins; + + foreach ($dirs as $manifestPath) { + $slug = basename(dirname($manifestPath)); + $manifest = require $manifestPath; + $dbRow = $dbRows[$slug] ?? null; + + $plugins[] = [ + 'slug' => $slug, + 'name' => $manifest['name'] ?? $slug, + 'version' => $manifest['version'] ?? '1.0.0', + 'description' => $manifest['description'] ?? '', + 'author' => $manifest['author'] ?? '', + 'installed' => $dbRow !== null, + 'enabled' => (bool)($dbRow['enabled'] ?? false), + 'installed_at' => $dbRow['installed_at'] ?? null, + 'plugin_id' => $dbRow['plugin_id'] ?? null, + ]; + } + + return $plugins; + } + + // ─── Helpers ────────────────────────────────────────────────────────────── + + public static function pluginPath(string $slug): string { + self::init(); + return self::$pluginsDir . '/' . $slug; + } + + private static function sanitizeSlug(string $slug): string { + return preg_replace('/[^a-z0-9_-]/', '', strtolower(trim($slug))); + } + + private static function loadManifest(string $slug): ?array { + self::init(); + $path = self::$pluginsDir . '/' . $slug . '/plugin.php'; + return file_exists($path) ? require $path : null; + } + + /** + * Copy plugin files into the framework directories. + * public/controllers/{slug}.php ← plugins/{slug}/{Slug}Controller.php + * public/views/plugins/{slug}/ ← plugins/{slug}/views/{slug}/ + * public/assets/plugins/{slug}/ ← plugins/{slug}/assets/ + */ + private static function createSymlinks(string $slug, array $manifest): array { + self::init(); + + $pluginDir = self::$pluginsDir . '/' . $slug; + $controllerClass = ucfirst($slug) . 'Controller'; + + // Ensure parent dirs exist + foreach ([self::$viewsDir, self::$assetsDir] as $dir) { + if (!is_dir($dir)) { + mkdir($dir, 0755, true); + } + } + + // Copy controller file + $controllerSrc = $pluginDir . '/' . $controllerClass . '.php'; + $controllerDest = self::$controllersDir . '/' . $slug . '.php'; + if (!file_exists($controllerSrc)) { + return ['success' => false, 'error' => "Controller not found: {$controllerClass}.php"]; + } + if (!copy($controllerSrc, $controllerDest)) { + return ['success' => false, 'error' => "Failed to copy controller to: {$controllerDest}"]; + } + + // Copy views directory (optional) + $viewsSrc = $pluginDir . '/views/' . $slug; + $viewsDest = self::$viewsDir . '/' . $slug; + if (is_dir($viewsSrc)) { + $result = self::copyDir($viewsSrc, $viewsDest); + if (!$result) { + return ['success' => false, 'error' => "Failed to copy views to: {$viewsDest}"]; + } + } + + // Copy assets directory (optional) + $assetsSrc = $pluginDir . '/assets'; + $assetsDest = self::$assetsDir . '/' . $slug; + if (is_dir($assetsSrc)) { + $result = self::copyDir($assetsSrc, $assetsDest); + if (!$result) { + return ['success' => false, 'error' => "Failed to copy assets to: {$assetsDest}"]; + } + } + + return ['success' => true]; + } + + /** + * Remove copied plugin files from framework directories. + */ + private static function removeSymlinks(string $slug): void { + self::init(); + + $controllerFile = self::$controllersDir . '/' . $slug . '.php'; + if (file_exists($controllerFile)) { + unlink($controllerFile); + } + + foreach ([self::$viewsDir . '/' . $slug, self::$assetsDir . '/' . $slug] as $dir) { + if (is_dir($dir)) { + self::removeDir($dir); + } + } + } + + /** + * Recursively copy a directory. + */ + private static function copyDir(string $src, string $dest): bool { + if (!is_dir($dest)) { + mkdir($dest, 0755, true); + } + foreach (scandir($src) as $item) { + if ($item === '.' || $item === '..') continue; + $s = $src . '/' . $item; + $d = $dest . '/' . $item; + if (is_dir($s)) { + if (!self::copyDir($s, $d)) return false; + } else { + if (!copy($s, $d)) return false; + } + } + return true; + } + + /** + * Recursively delete a directory. + */ + private static function removeDir(string $dir): void { + foreach (scandir($dir) as $item) { + if ($item === '.' || $item === '..') continue; + $path = $dir . '/' . $item; + is_dir($path) ? self::removeDir($path) : unlink($path); + } + rmdir($dir); + } +} diff --git a/api/app/Helpers/Router.php b/api/app/Helpers/Router.php new file mode 100644 index 0000000..1af81d6 --- /dev/null +++ b/api/app/Helpers/Router.php @@ -0,0 +1,31 @@ +setBasePath(''); + + // Rules Set Here + // $router->map('GET|POST', '/orgs/[*:orgid]', array('c' => 'orgs', 'a' => 'index')); + + PluginManager::boot($router); + + return $router->match(); + + } +} \ No newline at end of file diff --git a/api/app/Helpers/Validate.php b/api/app/Helpers/Validate.php new file mode 100644 index 0000000..c0f2cc0 --- /dev/null +++ b/api/app/Helpers/Validate.php @@ -0,0 +1,26 @@ + 1]); + +// SELECT single row +$user = Db::getRow("SELECT * FROM sp_users WHERE id = :id", [':id' => $id]); +if (!$user) { die('Not found'); } + +// INSERT +$userId = Db::insert('sp_users', [ + 'name' => 'Jane Doe', + 'email' => 'jane@example.com', + 'created_at' => date('Y-m-d H:i:s') +]); + +// UPDATE +Db::update('sp_users', + ['name' => 'Jane Smith', 'updated_at' => date('Y-m-d H:i:s')], + 'id = :id', + [':id' => $userId] +); + +// DELETE +Db::delete('sp_users', 'id = :id', [':id' => $userId]); + +// Soft delete (preferred) +Db::update('sp_users', ['deleted_at' => date('Y-m-d H:i:s')], 'id = :id', [':id' => $userId]); + +// Custom query +Db::execute("UPDATE sp_users SET last_login = NOW() WHERE id = :id", [':id' => $id]); + +// Transaction +Db::beginTransaction(); +try { + $orderId = Db::insert('sp_orders', ['user_id' => $userId, 'total' => 100]); + Db::execute("UPDATE sp_users SET balance = balance - 100 WHERE id = :id", [':id' => $userId]); + Db::commit(); +} catch (Exception $e) { + Db::rollback(); + throw $e; +} +``` + +### Complex Query Patterns + +```php +// Aggregation +$stats = Db::getRow("SELECT COUNT(*) as total, SUM(amount) as revenue FROM sp_orders WHERE status = 'completed'"); +echo $stats['total']; + +// Subquery +$users = Db::select(" + SELECT u.*, + (SELECT COUNT(*) FROM sp_orders WHERE user_id = u.id) as order_count + FROM sp_users u WHERE u.active = 1 +"); + +// Dynamic filter building +$query = "SELECT * FROM sp_clients WHERE 1=1"; +$params = []; +if (!empty($_GET['q'])) { + $query .= " AND (name LIKE :q OR email LIKE :q)"; + $params[':q'] = '%' . $_GET['q'] . '%'; +} +if (!empty($_GET['status'])) { + $query .= " AND status = :status"; + $params[':status'] = $_GET['status']; +} +$clients = Db::select($query, $params); + +// Pagination +$page = max(1, (int)($_GET['page'] ?? 1)); +$perPage = 20; +$offset = ($page - 1) * $perPage; +$total = Db::getRow("SELECT COUNT(*) as count FROM sp_clients"); +$rows = Db::select("SELECT * FROM sp_clients LIMIT :limit OFFSET :offset", + [':limit' => $perPage, ':offset' => $offset] +); + +// Use EXISTS instead of COUNT for existence checks (faster) +$exists = Db::getRow("SELECT 1 FROM sp_users WHERE email = :email LIMIT 1", [':email' => $email]); +if ($exists) { ... } +``` + +--- + +## `Auth.php` — Authentication + +```php +Auth::handleLogin() // redirect to /login if not authenticated +Auth::can('users.create') // returns bool — check permission +Auth::requirePermission('users.delete') // halts with 403 JSON if denied +Auth::isLoggedIn() // returns bool +Auth::getUserId() // returns current user ID from session +Auth::getUser() // returns current user row +Auth::login($userId) // set session login state +Auth::logout() // destroy session +``` + +--- + +## `Session.php` — Session Management + +```php +Session::start() +Session::set($key, $value) +Session::get($key, $default = null) +Session::has($key) +Session::delete($key) +Session::destroy() +Session::flash($key, $value) // set one-time flash message +Session::getFlash($key) // get and delete flash message +``` + +--- + +## `Validator.php` — Input Validation + +```php +Validator::required($value) // not empty +Validator::email($value) // valid email format +Validator::minLength($value, $min) +Validator::maxLength($value, $max) +Validator::numeric($value) +Validator::alpha($value) +Validator::alphanumeric($value) +Validator::url($value) +Validator::date($value) +``` + +--- + +## `Functions.php` — General Purpose + +Key reusable helpers: + +```php +// Decrypt tokenized org slug → fetch active org row (dies on invalid) +Functions::resolveOrg($slug) + +// Decrypt tokenized user slug → fetch user+role row (dies on invalid) +Functions::resolveUser($slug) + +// AES-256-CBC URL-safe encrypt / decrypt +Functions::encryptData($data, $key) +Functions::decryptData($token, $key) +``` + +--- + +## `Hash.php` — Cryptography + +Crypto utilities (hashing, token generation, etc.). + +--- + +## Adding New Helpers + +1. Find the most semantically appropriate existing class. +2. Add a `public static function` to it. +3. If nothing fits, create `app/Helpers/NewHelper.php` as a static class. +4. Call from any controller or other helper — no instantiation needed. diff --git a/api/app/Helpers/helpers.php b/api/app/Helpers/helpers.php new file mode 100644 index 0000000..3796c29 --- /dev/null +++ b/api/app/Helpers/helpers.php @@ -0,0 +1,8 @@ + 'claude-opus-4-20250514', + 'sonnet' => 'claude-sonnet-4-5-20250929', + 'haiku' => 'claude-haiku-4-5-20251001', + ]; + + /** + * Constructor + */ + public function __construct(?string $apiKey = null, ?string $templatePath = null, int $timeout = 300) { + $this->apiKey = $apiKey ?? ANTHROPIC_API_KEY; + $this->templatePath = $templatePath ?? dirname(__DIR__, 2) . '/templates/prompts/'; + + + if (empty($this->apiKey)) { + throw new Exception('Anthropic API key is required.'); + } + + if (!is_dir($this->templatePath)) { + throw new Exception("Template directory not found: {$this->templatePath}"); + } + + if (!is_readable($this->templatePath)) { + throw new Exception("Template directory is not readable: {$this->templatePath}"); + } + + set_time_limit($timeout); + ini_set('default_socket_timeout', (string) $timeout); + + $this->client = new Client(apiKey: $this->apiKey); + } + + + // ========================================================================= + // CONNECTION & HEALTH + // ========================================================================= + + public function testConnection(): array + { + $startTime = microtime(true); + + try { + $response = $this->client->messages->create( + model: $this->defaultModel, + maxTokens: 10, + messages: [ + MessageParam::with(role: 'user', content: 'Reply with only: OK') + ] + ); + + $latency = round((microtime(true) - $startTime) * 1000); + + return [ + 'success' => true, + 'message' => 'Connected to Anthropic API', + 'model' => $this->defaultModel, + 'response' => trim($response->content[0]->text), + 'latency_ms' => $latency, + 'usage' => [ + 'input_tokens' => $response->usage->inputTokens, + 'output_tokens' => $response->usage->outputTokens, + ] + ]; + + } catch (Exception $e) { + return [ + 'success' => false, + 'message' => 'Connection failed', + 'error' => $e->getMessage(), + 'error_type' => get_class($e) + ]; + } + } + + public function isConnected(): bool + { + return $this->testConnection()['success']; + } + + public function getStatus(): array + { + $connection = $this->testConnection(); + + return [ + 'connected' => $connection['success'], + 'model' => $this->defaultModel, + 'max_tokens' => $this->defaultMaxTokens, + 'api_key_preview' => substr($this->apiKey, 0, 10) . '...' . substr($this->apiKey, -4), + 'template_path' => $this->templatePath, + 'connection_details' => $connection + ]; + } + + // ========================================================================= + // TEMPLATE LOADING + // ========================================================================= + + public function loadTemplate(string $filename): string + { + $path = $this->templatePath . '/' . $filename; + + if (!file_exists($path)) { + throw new Exception("Template not found: {$path}"); + } + + $content = file_get_contents($path); + + if ($content === false) { + throw new Exception("Failed to read template: {$path}"); + } + + return $content; + } + + public function setTemplatePath(string $path): self + { + $this->templatePath = rtrim($path, '/'); + return $this; + } + + public function getTemplatePath(): string + { + return $this->templatePath; + } + + // ========================================================================= + // BASIC MESSAGING + // ========================================================================= + + public function message(string $prompt, ?string $systemPrompt = null, array $options = []): string + { + $response = $this->messageWithMeta($prompt, $systemPrompt, $options); + return $response['content']; + } + + public function messageWithMeta(string $prompt, ?string $systemPrompt = null, array $options = []): array + { + $startTime = microtime(true); + + $messages = $this->buildMessages($prompt, $options['conversation'] ?? []); + + $params = [ + 'model' => $options['model'] ?? $this->defaultModel, + 'maxTokens' => $options['max_tokens'] ?? $this->defaultMaxTokens, + 'messages' => $messages, + ]; + + if ($systemPrompt) { + $params['system'] = $systemPrompt; + } + + if (isset($options['temperature'])) { + $params['temperature'] = $options['temperature']; + } + + if (isset($options['stop_sequences'])) { + $params['stopSequences'] = $options['stop_sequences']; + } + + $response = $this->client->messages->create(...$params); + + return [ + 'content' => $response->content[0]->text, + 'model' => $response->model, + 'stop_reason' => $response->stopReason, + 'latency_ms' => round((microtime(true) - $startTime) * 1000), + 'usage' => [ + 'input_tokens' => $response->usage->inputTokens, + 'output_tokens' => $response->usage->outputTokens, + 'total_tokens' => $response->usage->inputTokens + $response->usage->outputTokens, + ] + ]; + } + + public function conversation(array $messages, ?string $systemPrompt = null, array $options = []): array + { + $messageParams = []; + foreach ($messages as $msg) { + $messageParams[] = MessageParam::with(role: $msg['role'], content: $msg['content']); + } + + $params = [ + 'model' => $options['model'] ?? $this->defaultModel, + 'maxTokens' => $options['max_tokens'] ?? $this->defaultMaxTokens, + 'messages' => $messageParams, + ]; + + if ($systemPrompt) { + $params['system'] = $systemPrompt; + } + + $response = $this->client->messages->create(...$params); + + return [ + 'content' => $response->content[0]->text, + 'usage' => [ + 'input_tokens' => $response->usage->inputTokens, + 'output_tokens' => $response->usage->outputTokens, + ] + ]; + } + + + // ========================================================================= + // CACHED MESSAGING + // ========================================================================= + + /** + * Message with prompt caching - template is cached, variables are not + */ + public function messageWithCache( + string $prompt, + string $cachedContext, + ?string $dynamicContext = null, + array $options = [] + ): array { + $startTime = microtime(true); + + $systemBlocks = [ + [ + 'type' => 'text', + 'text' => $cachedContext, + 'cache_control' => ['type' => 'ephemeral'] + ] + ]; + + if ($dynamicContext) { + $systemBlocks[] = [ + 'type' => 'text', + 'text' => $dynamicContext + ]; + } + + $response = $this->client->messages->create( + model: $options['model'] ?? $this->defaultModel, + maxTokens: $options['max_tokens'] ?? $this->defaultMaxTokens, + system: $systemBlocks, + messages: [ + MessageParam::with(role: 'user', content: $prompt) + ] + ); + + $cacheCreation = $response->usage->cacheCreationInputTokens ?? 0; + $cacheRead = $response->usage->cacheReadInputTokens ?? 0; + + return [ + 'content' => $response->content[0]->text, + 'model' => $response->model, + 'latency_ms' => round((microtime(true) - $startTime) * 1000), + 'usage' => [ + 'input_tokens' => $response->usage->inputTokens, + 'output_tokens' => $response->usage->outputTokens, + 'cache_creation_tokens' => $cacheCreation, + 'cache_read_tokens' => $cacheRead, + ], + 'cache_hit' => $cacheRead > 0, + 'cache_status' => $this->getCacheStatus($cacheCreation, $cacheRead) + ]; + } + + private function getCacheStatus(int $created, int $read): string + { + if ($read > 0) return 'hit'; + if ($created > 0) return 'created'; + return 'none'; + } + + // ========================================================================= + // ARTICLE GENERATION + // ========================================================================= + + /** + * Build the dynamic variables block (topic, word count, etc.) + * This is NOT cached - changes per article + */ + public function buildArticleVariables(array $config): string + { + $lines = []; + + if (!empty($config['topic'])) { + $lines[] = "TOPIC: {$config['topic']}"; + } + if (!empty($config['domain'])) { + $lines[] = "DOMAIN: {$config['domain']}"; + } + if (!empty($config['industry'])) { + $lines[] = "INDUSTRY: {$config['industry']}"; + } + if (!empty($config['target_audience'])) { + $lines[] = "AUDIENCE: {$config['target_audience']}"; + } + if (!empty($config['knowledge_level'])) { + $lines[] = "KNOWLEDGE_LEVEL: {$config['knowledge_level']}"; + } + if (!empty($config['tone'])) { + $lines[] = "TONE: {$config['tone']}"; + } + if (!empty($config['word_count'])) { + $lines[] = "WORD_COUNT: {$config['word_count']}"; + } + if (!empty($config['location'])) { + $lines[] = "LOCATION: {$config['location']}"; + } + + $lines[] = "CURRENT_DATE: " . date('Y-m-d'); + + return implode("\n", $lines); + } + + /** + * Generate article using template + variables (legacy single-file method) + * Template (with business profile) is cached + * Variables (topic, word count, etc.) are dynamic + */ + public function generateArticle(string $templateFile, array $config): array + { + // Load template - this includes the master prompt + business profile + // This part gets cached + $template = $this->loadTemplate($templateFile); + + // Build dynamic variables - this changes per article + $variables = $this->buildArticleVariables($config); + + return $this->messageWithCache( + prompt: $config['prompt'] ?? 'BEGIN GENERATION NOW.', + cachedContext: $template, + dynamicContext: $variables, + options: [ + 'max_tokens' => $config['max_tokens'] ?? 8192, + 'model' => $config['model'] ?? $this->defaultModel + ] + ); + } + + /** + * Generate article using 3-block structure with prompt caching + * + * Block 1: Business Profile (cached - client specific) + * Block 2: Article Instructions (cached - shared across all clients) + * Block 3: Config Variables (dynamic - not cached) + * + * Cache behavior: + * - Blocks 1 & 2 are cached together as a prefix + * - When processing multiple articles for same client, cache hits on blocks 1+2 + * - Block 3 varies per article, never cached + * + * @param string $businessProfile Client-specific business profile content + * @param string $articleInstructions Shared writing instructions content + * @param array $config Article configuration (topic, word_count, etc.) + * - 'enable_web_search' => bool (default: false) + * - 'reference_urls' => array of URLs to research + * @return array Response with content, usage, and cache status + */ +// ============================================================================= +// UPDATED Anthropic::generateArticleWithBlocks() +// ============================================================================= + public function generateArticleWithBlocks( + string $businessProfile, + string $articleInstructions, + array $projectConfig, + string $referenceContent = '', + bool $useWebSearch = false + ): array { + $startTime = microtime(true); + + // Build article variables (Block 3) + $articleVariables = $this->buildArticleVariables($projectConfig); + + // DEEPCRAWL MODE: Inject pre-fetched content into Block 3 + if (!empty($referenceContent)) { + $articleVariables .= "\n\n" . $referenceContent; + } + + // WEBSEARCH MODE: Add URL hints for Claude to search + if ($useWebSearch && !empty($projectConfig['reference_urls'])) { + $urlHints = "\n\n\n"; + $urlHints .= "Consider searching these sources for authoritative information:\n"; + foreach ($projectConfig['reference_urls'] as $ref) { + $url = is_array($ref) ? $ref['url'] : $ref; + $notes = is_array($ref) ? ($ref['notes'] ?? '') : ''; + $urlHints .= "- {$url}"; + if ($notes) $urlHints .= " ({$notes})"; + $urlHints .= "\n"; + } + $urlHints .= ""; + $articleVariables .= $urlHints; + } + + // ========================================================================= + // BUILD THE FULL PROMPT (for logging) + // ========================================================================= + $block1 = "\n{$businessProfile}\n"; + $block2 = "\n{$articleInstructions}\n"; + $block3 = "\n{$articleVariables}\n"; + + $fullPrompt = $block1 . "\n\n" . $block2 . "\n\n" . $block3; + + // Build messages + $messages = [ + [ + 'role' => 'user', + 'content' => [ + // Block 1: Business Profile (cached) + [ + 'type' => 'text', + 'text' => $block1, + 'cache_control' => ['type' => 'ephemeral'] + ], + // Block 2: Article Instructions (cached) + [ + 'type' => 'text', + 'text' => $block2, + 'cache_control' => ['type' => 'ephemeral'] + ], + // Block 3: Article Variables + References (dynamic) + [ + 'type' => 'text', + 'text' => $block3 + ] + ] + ] + ]; + + // Call API + if ($useWebSearch) { + $response = $this->client->messages->create( + model: 'claude-sonnet-4-5-20250929', + maxTokens: 8192, + messages: $messages, + tools: [ + [ + 'type' => 'web_search_20250305', + 'name' => 'web_search', + ] + ] + ); + } else { + $response = $this->client->messages->create( + model: 'claude-sonnet-4-5-20250929', + maxTokens: 8192, + messages: $messages + ); + } + + // Parse response + $cacheCreation = $response->usage->cacheCreationInputTokens ?? 0; + $cacheRead = $response->usage->cacheReadInputTokens ?? 0; + + return [ + 'content' => $response->content[0]->text, + 'model' => $response->model, + 'stop_reason' => $response->stopReason, + 'latency_ms' => round((microtime(true) - $startTime) * 1000), + 'usage' => [ + 'input_tokens' => $response->usage->inputTokens, + 'output_tokens' => $response->usage->outputTokens, + 'cache_creation_tokens' => $cacheCreation, + 'cache_read_tokens' => $cacheRead, + ], + 'cache_status' => $this->getCacheStatus($cacheCreation, $cacheRead), + 'prompt_sent' => $fullPrompt, // NEW: Full prompt for logging + ]; + } + + + /** + * Build instruction for reference URLs + */ + private function buildReferenceUrlsInstruction(array $urls): string + { + $instruction = "REFERENCE URLS FOR RESEARCH:\n"; + $instruction .= "Search and reference the following URLs for accurate, current information:\n\n"; + + foreach ($urls as $index => $url) { + $num = $index + 1; + if (is_array($url)) { + // URL with description: ['url' => '...', 'description' => '...'] + $instruction .= "{$num}. {$url['url']}\n"; + if (!empty($url['description'])) { + $instruction .= " Purpose: {$url['description']}\n"; + } + } else { + // Simple URL string + $instruction .= "{$num}. {$url}\n"; + } + } + + $instruction .= "\nUse web search to fetch current information from these sources. "; + $instruction .= "Cite specific facts, statistics, or requirements found on these pages. "; + $instruction .= "You may also search for additional supporting information as needed."; + + return $instruction; + } + + /** + * Extract text content from response blocks (handles mixed content with tool use) + */ + private function extractTextContent(array $contentBlocks): string + { + $textParts = []; + + foreach ($contentBlocks as $block) { + if (isset($block->type) && $block->type === 'text') { + $textParts[] = $block->text; + } + } + + return implode("\n", $textParts); + } + + /** + * Generate article using multi-turn conversation style + * + * This approach chunks the prompt into conversational turns: + * - Turn 1: User sends business profile + * - Turn 2: Assistant acknowledges persona + * - Turn 3: User sends article instructions + * - Turn 4: Assistant confirms understanding + * - Turn 5: User sends config and triggers generation + * + * Benefits: May improve instruction following for complex prompts + * Tradeoff: Extra tokens for assistant acknowledgments + * + * @param string $businessProfile Client-specific business profile content + * @param string $articleInstructions Shared writing instructions content + * @param array $config Article configuration (topic, word_count, etc.) + * @return array Response with content, usage, and cache status + */ + public function generateArticleMultiTurn( + string $businessProfile, + string $articleInstructions, + array $config + ): array { + $startTime = microtime(true); + + // Build dynamic config variables + $configVariables = $this->buildArticleVariables($config); + + $response = $this->client->messages->create( + model: $config['model'] ?? $this->defaultModel, + maxTokens: $config['max_tokens'] ?? 8192, + messages: [ + // Turn 1: Business Profile + [ + 'role' => 'user', + 'content' => [ + [ + 'type' => 'text', + 'text' => "Here is the business profile you will write as:\n\n" . $businessProfile, + 'cache_control' => ['type' => 'ephemeral'], + ], + ], + ], + // Turn 2: Assistant acknowledges persona + [ + 'role' => 'assistant', + 'content' => [ + [ + 'type' => 'text', + 'text' => "I understand. I am now embodying this business identity and will write from their perspective, using their voice, expertise, and experience as defined in the profile.", + 'cache_control' => ['type' => 'ephemeral'], + ], + ], + ], + // Turn 3: Article Instructions + [ + 'role' => 'user', + 'content' => [ + [ + 'type' => 'text', + 'text' => "Here are the article writing instructions:\n\n" . $articleInstructions, + 'cache_control' => ['type' => 'ephemeral'], + ], + ], + ], + // Turn 4: Assistant confirms understanding + [ + 'role' => 'assistant', + 'content' => [ + [ + 'type' => 'text', + 'text' => "Ready. I will follow these writing rules: early answer with recommendation, 'This breaks down when...' statement, field notes from practitioner experience, no blacklisted phrases, boundary conditions section, and output valid JSON only.", + 'cache_control' => ['type' => 'ephemeral'], + ], + ], + ], + // Turn 5: Config Variables + Generate + [ + 'role' => 'user', + 'content' => $configVariables . "\n\nBEGIN GENERATION.", + ], + ], + ); + + $cacheCreation = $response->usage->cacheCreationInputTokens ?? 0; + $cacheRead = $response->usage->cacheReadInputTokens ?? 0; + + return [ + 'content' => $response->content[0]->text, + 'model' => $response->model, + 'stop_reason' => $response->stopReason, + 'latency_ms' => round((microtime(true) - $startTime) * 1000), + 'usage' => [ + 'input_tokens' => $response->usage->inputTokens, + 'output_tokens' => $response->usage->outputTokens, + 'cache_creation_tokens' => $cacheCreation, + 'cache_read_tokens' => $cacheRead, + ], + 'cache_hit' => $cacheRead > 0, + 'cache_status' => $this->getCacheStatus($cacheCreation, $cacheRead), + ]; + } + + /** + * Generate multiple articles (same template = cache hits after first) + */ + public function generateArticleBatch(string $templateFile, array $articles): array + { + $results = []; + + foreach ($articles as $index => $config) { + $results[] = [ + 'index' => $index, + 'topic' => $config['topic'] ?? 'Unknown', + 'result' => $this->generateArticle($templateFile, $config) + ]; + } + + return $results; + } + + // ========================================================================= + // UTILITY METHODS + // ========================================================================= + + private function buildMessages(string $prompt, array $conversation = []): array + { + $messages = []; + + foreach ($conversation as $msg) { + $messages[] = MessageParam::with(role: $msg['role'], content: $msg['content']); + } + + $messages[] = MessageParam::with(role: 'user', content: $prompt); + + return $messages; + } + + public function estimateTokens(string $text): int + { + return (int) ceil(strlen($text) / 4); + } + + /** + * Check if content meets minimum cache threshold (1024 tokens for Sonnet) + */ + public function meetsCacheMinimum(string $text): bool + { + return $this->estimateTokens($text) >= 1024; + } + + public function estimateCost(int $inputTokens, int $outputTokens, ?string $model = null): array + { + $model = $model ?? $this->defaultModel; + + $pricing = [ + 'claude-opus-4-20250514' => ['input' => 15.00, 'output' => 75.00], + 'claude-sonnet-4-5-20250929' => ['input' => 3.00, 'output' => 15.00], + 'claude-haiku-4-5-20251001' => ['input' => 0.80, 'output' => 4.00], + ]; + + $rates = $pricing[$model] ?? $pricing['claude-sonnet-4-5-20250929']; + + $inputCost = ($inputTokens / 1_000_000) * $rates['input']; + $outputCost = ($outputTokens / 1_000_000) * $rates['output']; + + return [ + 'input_cost' => round($inputCost, 6), + 'output_cost' => round($outputCost, 6), + 'total_cost' => round($inputCost + $outputCost, 6), + 'model' => $model + ]; + } + + public function formatCost(array $usage): string { + $cost = $this->estimateCostWithCache($usage); + + $output = "Cost Breakdown:\n"; + $output .= " Input: $" . number_format($cost['input_cost'], 4) . "\n"; + $output .= " Output: $" . number_format($cost['output_cost'], 4) . "\n"; + + if ($cost['cache_write_cost'] > 0) { + $output .= " Cache Write: $" . number_format($cost['cache_write_cost'], 4) . "\n"; + } + if ($cost['cache_read_cost'] > 0) { + $output .= " Cache Read: $" . number_format($cost['cache_read_cost'], 4) . "\n"; + } + + $output .= " ─────────────\n"; + $output .= " Total: $" . number_format($cost['total_cost'], 4) . "\n"; + + if ($cost['savings'] > 0) { + $output .= " Saved: $" . number_format($cost['savings'], 4) . " ({$cost['savings_percent']}%)\n"; + } + + return $output; + } + + public function estimateCostWithCache(array $usage, ?string $model = null): array + { + $model = $model ?? $this->defaultModel; + + $pricing = [ + 'claude-opus-4-20250514' => ['input' => 15.00, 'output' => 75.00, 'cache_write' => 18.75, 'cache_read' => 1.50], + 'claude-sonnet-4-5-20250929' => ['input' => 3.00, 'output' => 15.00, 'cache_write' => 3.75, 'cache_read' => 0.30], + 'claude-haiku-4-5-20251001' => ['input' => 0.80, 'output' => 4.00, 'cache_write' => 1.00, 'cache_read' => 0.08], + ]; + + $rates = $pricing[$model] ?? $pricing['claude-sonnet-4-5-20250929']; + + $inputCost = (($usage['input_tokens'] ?? 0) / 1_000_000) * $rates['input']; + $outputCost = (($usage['output_tokens'] ?? 0) / 1_000_000) * $rates['output']; + $cacheWriteCost = (($usage['cache_creation_tokens'] ?? 0) / 1_000_000) * $rates['cache_write']; + $cacheReadCost = (($usage['cache_read_tokens'] ?? 0) / 1_000_000) * $rates['cache_read']; + + $totalCost = $inputCost + $outputCost + $cacheWriteCost + $cacheReadCost; + + $cacheTokens = ($usage['cache_creation_tokens'] ?? 0) + ($usage['cache_read_tokens'] ?? 0); + $costWithoutCache = (($usage['input_tokens'] + $cacheTokens) / 1_000_000) * $rates['input'] + $outputCost; + $savings = $costWithoutCache - $totalCost; + + return [ + 'input_cost' => round($inputCost, 6), + 'output_cost' => round($outputCost, 6), + 'cache_write_cost' => round($cacheWriteCost, 6), + 'cache_read_cost' => round($cacheReadCost, 6), + 'total_cost' => round($totalCost, 6), + 'cost_without_cache' => round($costWithoutCache, 6), + 'savings' => round($savings, 6), + 'savings_percent' => $costWithoutCache > 0 ? round(($savings / $costWithoutCache) * 100, 2) : 0, + 'model' => $model + ]; + } + + // ========================================================================= + // CONFIGURATION + // ========================================================================= + + public function setModel(string $model): self + { + $this->defaultModel = $model; + return $this; + } + + public function useModel(string $preset): self + { + if (isset(self::MODELS[$preset])) { + $this->defaultModel = self::MODELS[$preset]; + } + return $this; + } + + public function setMaxTokens(int $tokens): self + { + $this->defaultMaxTokens = $tokens; + return $this; + } + + public function getModel(): string + { + return $this->defaultModel; + } + + public function getClient(): Client + { + return $this->client; + } + + public function getAvailableModels(): array + { + return self::MODELS; + } +} \ No newline at end of file diff --git a/api/app/LLM/GeminiService.php b/api/app/LLM/GeminiService.php new file mode 100644 index 0000000..af48ce3 --- /dev/null +++ b/api/app/LLM/GeminiService.php @@ -0,0 +1,749 @@ + 'gemini-3-pro-preview', // Best for humanizing, SEO strategy + 'flash' => 'gemini-3-flash-preview', // Faster, good for SEO review pass + 'flash-lite' => 'gemini-2.5-flash-lite', // Cost saver for simple tasks + 'flash-2.5' => 'gemini-2.5-flash', // Legacy flash model + ]; + + // Human-readable labels for UI dropdowns + public const MODEL_LABELS = [ + 'gemini-3-pro-preview' => 'Gemini 3 Pro (Best for Humanizing)', + 'gemini-3-flash-preview' => 'Gemini 3 Flash (Fast & Smart)', + 'gemini-2.5-flash' => 'Gemini 2.5 Flash (Legacy)', + 'gemini-2.5-flash-lite' => 'Gemini 2.5 Flash Lite (Budget)', + ]; + + /** + * Constructor + */ + public function __construct(?string $apiKey = null, ?string $templatePath = null, int $timeout = 300) + { + $this->apiKey = $apiKey ?? (defined('GEMINI_API_KEY') ? GEMINI_API_KEY : null); + + if (empty($this->apiKey)) { + throw new Exception('Gemini API key is required.'); + } + + $this->templatePath = $templatePath ?? '/www/wwwroot/appCarlos/templates/prompts'; + + if (!is_dir($this->templatePath)) { + throw new Exception("Template directory not found: {$this->templatePath}"); + } + + if (!is_readable($this->templatePath)) { + throw new Exception("Template directory is not readable: {$this->templatePath}"); + } + + set_time_limit($timeout); + ini_set('default_socket_timeout', (string) $timeout); + + $this->client = GeminiClient::client($this->apiKey); + } + + // ========================================================================= + // CONNECTION & HEALTH + // ========================================================================= + + public function testConnection(): array + { + $startTime = microtime(true); + + try { + $response = $this->client + ->generativeModel(model: $this->defaultModel) + ->generateContent('Reply with only: OK'); + + $latency = round((microtime(true) - $startTime) * 1000); + + // Extract usage from response + $usage = $this->extractUsage($response); + + return [ + 'success' => true, + 'message' => 'Connected to Gemini API', + 'model' => $this->defaultModel, + 'response' => trim($response->text()), + 'latency_ms' => $latency, + 'usage' => $usage, + ]; + + } catch (Exception $e) { + return [ + 'success' => false, + 'message' => 'Connection failed', + 'error' => $e->getMessage(), + 'error_type' => get_class($e), + ]; + } + } + + public function isConnected(): bool + { + return $this->testConnection()['success']; + } + + public function getStatus(): array + { + $connection = $this->testConnection(); + + return [ + 'connected' => $connection['success'], + 'model' => $this->defaultModel, + 'max_tokens' => $this->defaultMaxTokens, + 'api_key_preview' => substr($this->apiKey, 0, 10) . '...' . substr($this->apiKey, -4), + 'template_path' => $this->templatePath, + 'connection_details' => $connection, + ]; + } + + // ========================================================================= + // TEMPLATE LOADING + // ========================================================================= + + public function loadTemplate(string $filename): string + { + $path = $this->templatePath . '/' . $filename; + + if (!file_exists($path)) { + throw new Exception("Template not found: {$path}"); + } + + $content = file_get_contents($path); + + if ($content === false) { + throw new Exception("Failed to read template: {$path}"); + } + + return $content; + } + + public function setTemplatePath(string $path): self + { + $this->templatePath = rtrim($path, '/'); + return $this; + } + + public function getTemplatePath(): string + { + return $this->templatePath; + } + + // ========================================================================= + // BASIC MESSAGING + // ========================================================================= + + public function message(string $prompt, ?string $systemPrompt = null, array $options = []): string + { + $response = $this->messageWithMeta($prompt, $systemPrompt, $options); + return $response['content']; + } + + /** + * GEMINI SERVICE UPDATE + * + * Replace the existing messageWithMeta() method with this updated version + * that supports JSON response mode via 'response_mime_type' option. + * + * Location: App\Components\GeminiService + * Method: messageWithMeta() + */ + + public function messageWithMeta(string $prompt, ?string $systemPrompt = null, array $options = []): array + { + $startTime = microtime(true); + + $model = $this->client->generativeModel( + model: $options['model'] ?? $this->defaultModel + ); + + // Add system instruction if provided + if ($systemPrompt) { + $model = $model->withSystemInstruction(Content::parse($systemPrompt)); + } + + // Build generation config + $configParams = [ + 'maxOutputTokens' => $options['max_tokens'] ?? $this->defaultMaxTokens, + ]; + + if (isset($options['response_mime_type']) && $options['response_mime_type'] === 'application/json') { + $configParams['responseMimeType'] = ResponseMimeType::APPLICATION_JSON; + } + + if (isset($options['temperature'])) { + $configParams['temperature'] = $options['temperature']; + } + + if (isset($options['stop_sequences'])) { + $configParams['stopSequences'] = $options['stop_sequences']; + } + + // Support JSON response mode + if (isset($options['response_mime_type']) && $options['response_mime_type'] === 'application/json') { + $configParams['responseMimeType'] = ResponseMimeType::APPLICATION_JSON; + } + + $model = $model->withGenerationConfig(new GenerationConfig(...$configParams)); + + // Enable Google Search if requested + if ($options['use_search'] ?? false) { + $model = $model->withTool(new Tool(googleSearch: new GoogleSearch())); + } + + $response = $model->generateContent($prompt); + + $usage = $this->extractUsage($response); + + return [ + 'content' => $response->text(), + 'model' => $options['model'] ?? $this->defaultModel, + 'stop_reason' => $response->candidates[0]->finishReason ?? null, + 'latency_ms' => round((microtime(true) - $startTime) * 1000), + 'usage' => $usage, + ]; + } + + + // ========================================================================= + // ARTICLE GENERATION + // ========================================================================= + + /** + * Build the dynamic variables block (topic, word count, etc.) + */ + public function buildArticleVariables(array $config): string + { + $lines = []; + + if (!empty($config['topic'])) { + $lines[] = "TOPIC: {$config['topic']}"; + } + if (!empty($config['domain'])) { + $lines[] = "DOMAIN: {$config['domain']}"; + } + if (!empty($config['industry'])) { + $lines[] = "INDUSTRY: {$config['industry']}"; + } + if (!empty($config['target_audience'])) { + $lines[] = "AUDIENCE: {$config['target_audience']}"; + } + if (!empty($config['knowledge_level'])) { + $lines[] = "KNOWLEDGE_LEVEL: {$config['knowledge_level']}"; + } + if (!empty($config['tone'])) { + $lines[] = "TONE: {$config['tone']}"; + } + if (!empty($config['word_count'])) { + $lines[] = "WORD_COUNT: {$config['word_count']}"; + } + if (!empty($config['location'])) { + $lines[] = "LOCATION: {$config['location']}"; + } + + return implode("\n", $lines); + } + + /** + * Generate article using template + variables + * Uses Google Search for fact verification + */ + public function generateArticle(string $templateFile, array $config): array + { + $startTime = microtime(true); + + // Load template - this includes the master prompt + business profile + $template = $this->loadTemplate($templateFile); + + // Build dynamic variables + $variables = $this->buildArticleVariables($config); + + // Combine template with variables + $systemPrompt = $template . "\n\n" . $variables; + + // Build the model with configuration + $model = $this->client->generativeModel( + model: $config['model'] ?? $this->defaultModel + ); + + // Add system instruction (template + variables) + $model = $model->withSystemInstruction(Content::parse($systemPrompt)); + + // Add generation config + $model = $model->withGenerationConfig(new GenerationConfig( + maxOutputTokens: $config['max_tokens'] ?? $this->defaultMaxTokens, + )); + + // Enable Google Search for fact verification + if ($config['use_search'] ?? true) { + $model = $model->withTool(new Tool(googleSearch: new GoogleSearch())); + } + + // Generate content + $userPrompt = $config['prompt'] ?? 'BEGIN GENERATION NOW.'; + $response = $model->generateContent($userPrompt); + + $usage = $this->extractUsage($response); + $latency = round((microtime(true) - $startTime) * 1000); + + // Check if grounding was used + $groundingUsed = $this->checkGroundingUsed($response); + + return [ + 'content' => $response->text(), + 'model' => $config['model'] ?? $this->defaultModel, + 'latency_ms' => $latency, + 'grounding_used' => $groundingUsed, + 'usage' => $usage, + ]; + } + + /** + * Generate article with structured JSON output + */ + public function generateArticleStructured(string $templateFile, array $config): array + { + $startTime = microtime(true); + + // Load template + $template = $this->loadTemplate($templateFile); + + // Build dynamic variables + $variables = $this->buildArticleVariables($config); + + // Define JSON structure for article + $jsonSchema = $config['json_schema'] ?? [ + 'title' => 'Article title', + 'meta_description' => 'SEO meta description', + 'content' => 'Full article content in HTML format', + 'sections' => [ + ['heading' => 'Section heading', 'content' => 'Section content'] + ], + 'tags' => ['relevant', 'tags'], + 'sources' => [ + ['title' => 'Source title', 'url' => 'Source URL'] + ], + ]; + + $schemaJson = json_encode($jsonSchema, JSON_PRETTY_PRINT); + + // Combine template with variables and JSON instruction + $systemPrompt = $template . "\n\n" . $variables . "\n\n" . + "IMPORTANT: Respond ONLY with valid JSON using this structure:\n" . $schemaJson; + + // Build the model + $model = $this->client->generativeModel( + model: $config['model'] ?? $this->defaultModel + ); + + $model = $model->withSystemInstruction(Content::parse($systemPrompt)); + + // Enable JSON output + $model = $model->withGenerationConfig(new GenerationConfig( + maxOutputTokens: $config['max_tokens'] ?? $this->defaultMaxTokens, + responseMimeType: ResponseMimeType::APPLICATION_JSON, + )); + + // Enable Google Search + if ($config['use_search'] ?? true) { + $model = $model->withTool(new Tool(googleSearch: new GoogleSearch())); + } + + $userPrompt = $config['prompt'] ?? 'BEGIN GENERATION NOW.'; + $response = $model->generateContent($userPrompt); + + $usage = $this->extractUsage($response); + $latency = round((microtime(true) - $startTime) * 1000); + $groundingUsed = $this->checkGroundingUsed($response); + + // Parse JSON response + $articleData = null; + try { + $articleData = $response->json(); + } catch (Exception $e) { + // Fall back to text if JSON parsing fails + $articleData = ['content' => $response->text(), 'parse_error' => $e->getMessage()]; + } + + return [ + 'content' => $response->text(), + 'article' => $articleData, + 'model' => $config['model'] ?? $this->defaultModel, + 'latency_ms' => $latency, + 'grounding_used' => $groundingUsed, + 'usage' => $usage, + ]; + } + + /** + * Generate multiple articles + */ + public function generateArticleBatch(string $templateFile, array $articles): array + { + $results = []; + + foreach ($articles as $index => $config) { + $results[] = [ + 'index' => $index, + 'topic' => $config['topic'] ?? 'Unknown', + 'result' => $this->generateArticle($templateFile, $config), + ]; + } + + return $results; + } + + // ========================================================================= + // ARTICLE REVIEW + // ========================================================================= + + /** + * Review an article for quality and factual accuracy + */ + public function reviewArticle(string $articleContent, array $options = []): array + { + $startTime = microtime(true); + + $criteria = $options['criteria'] ?? [ + 'grammar_spelling', + 'factual_accuracy', + 'readability', + 'seo_optimization', + 'engagement', + 'structure', + 'tone_consistency', + ]; + + $criteriaList = implode(', ', $criteria); + + $systemPrompt = << 85, + 'summary' => 'Brief overall assessment', + 'criteria_scores' => [ + 'example_criterion' => [ + 'score' => 90, + 'feedback' => 'Detailed feedback', + ], + ], + 'strengths' => ['List of article strengths'], + 'improvements' => [ + [ + 'priority' => 'high|medium|low', + 'issue' => 'Issue description', + 'suggestion' => 'How to fix it', + ], + ], + 'fact_check' => [ + [ + 'claim' => 'Claim from the article', + 'verified' => true, + 'source' => 'Source or note', + ], + ], + ]; + + $schemaJson = json_encode($jsonSchema, JSON_PRETTY_PRINT); + + $userPrompt = <<client->generativeModel( + model: $options['model'] ?? $this->defaultModel + ); + + $model = $model->withSystemInstruction(Content::parse($systemPrompt)); + + $model = $model->withGenerationConfig(new GenerationConfig( + maxOutputTokens: $options['max_tokens'] ?? $this->defaultMaxTokens, + responseMimeType: ResponseMimeType::APPLICATION_JSON, + )); + + // Enable Google Search for fact-checking + $model = $model->withTool(new Tool(googleSearch: new GoogleSearch())); + + $response = $model->generateContent($userPrompt); + + $usage = $this->extractUsage($response); + $latency = round((microtime(true) - $startTime) * 1000); + + $reviewData = null; + try { + $reviewData = $response->json(); + } catch (Exception $e) { + $reviewData = ['content' => $response->text(), 'parse_error' => $e->getMessage()]; + } + + return [ + 'content' => $response->text(), + 'review' => $reviewData, + 'model' => $options['model'] ?? $this->defaultModel, + 'latency_ms' => $latency, + 'usage' => $usage, + ]; + } + + // ========================================================================= + // RESEARCH + // ========================================================================= + + /** + * Research a topic using Google Search + */ + public function research(string $query, array $options = []): array + { + $startTime = microtime(true); + + $depth = $options['depth'] ?? 2; + + $depthInstructions = match ($depth) { + 1 => 'Provide a quick overview with 3-5 key points.', + 2 => 'Provide a comprehensive overview with detailed findings.', + 3 => 'Provide an exhaustive analysis covering all aspects.', + default => 'Provide a comprehensive overview.', + }; + + $systemPrompt = << 'The research topic', + 'summary' => 'Executive summary', + 'key_findings' => [ + ['finding' => 'Key finding', 'confidence' => 'high|medium|low'], + ], + 'statistics' => [ + ['stat' => 'Statistic', 'source' => 'Source'], + ], + 'sources' => [ + ['title' => 'Source title', 'url' => 'URL', 'credibility' => 'high|medium|low'], + ], + ]; + + $schemaJson = json_encode($jsonSchema, JSON_PRETTY_PRINT); + + $userPrompt = <<client->generativeModel( + model: $options['model'] ?? $this->defaultModel + ); + + $model = $model->withSystemInstruction(Content::parse($systemPrompt)); + + $model = $model->withGenerationConfig(new GenerationConfig( + maxOutputTokens: $options['max_tokens'] ?? $this->defaultMaxTokens, + responseMimeType: ResponseMimeType::APPLICATION_JSON, + )); + + $model = $model->withTool(new Tool(googleSearch: new GoogleSearch())); + + $response = $model->generateContent($userPrompt); + + $usage = $this->extractUsage($response); + $latency = round((microtime(true) - $startTime) * 1000); + + $researchData = null; + try { + $researchData = $response->json(); + } catch (Exception $e) { + $researchData = ['content' => $response->text(), 'parse_error' => $e->getMessage()]; + } + + return [ + 'content' => $response->text(), + 'research' => $researchData, + 'model' => $options['model'] ?? $this->defaultModel, + 'latency_ms' => $latency, + 'usage' => $usage, + ]; + } + + // ========================================================================= + // UTILITY METHODS + // ========================================================================= + + /** + * Extract usage information from response + */ + private function extractUsage($response): array + { + $usage = [ + 'input_tokens' => 0, + 'output_tokens' => 0, + 'total_tokens' => 0, + ]; + + // Try to get usage metadata from response + if (isset($response->usageMetadata)) { + $usage['input_tokens'] = $response->usageMetadata->promptTokenCount ?? 0; + $usage['output_tokens'] = $response->usageMetadata->candidatesTokenCount ?? 0; + $usage['total_tokens'] = $response->usageMetadata->totalTokenCount ?? + ($usage['input_tokens'] + $usage['output_tokens']); + } + + return $usage; + } + + /** + * Check if grounding/search was used in the response + */ + private function checkGroundingUsed($response): bool + { + // Check for grounding metadata in response + if (isset($response->candidates[0]->groundingMetadata)) { + return true; + } + + return false; + } + + public function estimateTokens(string $text): int + { + return (int) ceil(strlen($text) / 4); + } + + public function estimateCost(array $usage, ?string $model = null): array + { + $model = $model ?? $this->defaultModel; + + // Gemini pricing per 1M tokens (as of January 2026) + $pricing = [ + 'gemini-3-pro-preview' => ['input' => 2.00, 'output' => 12.00], + 'gemini-3-flash-preview' => ['input' => 0.20, 'output' => 0.80], + 'gemini-2.5-flash' => ['input' => 0.15, 'output' => 0.60], + 'gemini-2.5-flash-lite' => ['input' => 0.075, 'output' => 0.30], + ]; + + $rates = $pricing[$model] ?? $pricing['gemini-3-pro-preview']; + + $inputCost = (($usage['input_tokens'] ?? 0) / 1_000_000) * $rates['input']; + $outputCost = (($usage['output_tokens'] ?? 0) / 1_000_000) * $rates['output']; + + return [ + 'input_cost' => round($inputCost, 6), + 'output_cost' => round($outputCost, 6), + 'total_cost' => round($inputCost + $outputCost, 6), + 'model' => $model, + ]; + } + + public function formatCost(array $usage): string + { + $cost = $this->estimateCost($usage); + + $output = "Cost Breakdown:\n"; + $output .= " Input: $" . number_format($cost['input_cost'], 4) . "\n"; + $output .= " Output: $" . number_format($cost['output_cost'], 4) . "\n"; + $output .= " ─────────────\n"; + $output .= " Total: $" . number_format($cost['total_cost'], 4) . "\n"; + + return $output; + } + + + + + // ========================================================================= + // CONFIGURATION + // ========================================================================= + + public function setModel(string $model): self + { + $this->defaultModel = $model; + return $this; + } + + public function useModel(string $preset): self + { + if (isset(self::MODELS[$preset])) { + $this->defaultModel = self::MODELS[$preset]; + } + return $this; + } + + public function setMaxTokens(int $tokens): self + { + $this->defaultMaxTokens = $tokens; + return $this; + } + + public function getModel(): string + { + return $this->defaultModel; + } + + public function getClient() + { + return $this->client; + } + + public function getAvailableModels(): array + { + return self::MODELS; + } + + /** + * Get model labels for UI dropdowns + */ + public static function getModelLabels(): array + { + return self::MODEL_LABELS; + } + + /** + * Get model ID from preset name + */ + public static function getModelId(string $preset): string + { + return self::MODELS[$preset] ?? $preset; + } +} \ No newline at end of file diff --git a/api/app/LLM/LLMManager.php b/api/app/LLM/LLMManager.php new file mode 100644 index 0000000..e4da024 --- /dev/null +++ b/api/app/LLM/LLMManager.php @@ -0,0 +1,254 @@ + svcOpenAI::class, + 'anthropic' => svcAnthropic::class, + 'gemini' => svcGemini::class, + 'mistral' => svcMistral::class, + 'deepseek' => svcDeepSeek::class, + ]; + + // Maps provider key to sp_settings.keyval (application-level) + private static array $settingsKeys = [ + 'openai' => 'llm_openai', + 'anthropic' => 'llm_anthropic', + 'gemini' => 'llm_gemini', + 'mistral' => 'llm_mistral', + 'deepseek' => 'llm_deepseek', + ]; + + // Maps provider key to sp_orgs_meta.keyval (org-level overrides) + private static array $metaKeys = [ + 'openai' => 'llmOpenAI', + 'anthropic' => 'llmAnthropic', + 'gemini' => 'llmGemini', + 'mistral' => 'llmMistral', + 'deepseek' => 'llmDeepSeek', + ]; + + // Provider preference order for auto-selection + public static array $priority = ['anthropic', 'openai', 'gemini', 'mistral', 'deepseek']; + + /** + * Instantiate a provider directly from a pre-loaded config array. + * Use this when you already have the config and don't need another DB lookup. + * + * @throws \Exception if provider unknown + */ + public static function make(string $provider, array $config): LLMProvider + { + $provider = strtolower($provider); + if (!isset(self::$providers[$provider])) { + throw new \Exception("Unknown LLM provider: '$provider'"); + } + $class = self::$providers[$provider]; + return new $class($config); + } + + // ─── Application-level (sp_settings) ──────────────────────────────────── + + /** + * Return a configured provider instance from the application-level sp_settings table. + * + * @throws \Exception if provider unknown or not configured + */ + public static function forApp(string $provider): LLMProvider + { + $provider = strtolower($provider); + + if (!isset(self::$providers[$provider])) { + throw new \Exception("Unknown LLM provider: '$provider'"); + } + + $config = self::getAppConfig($provider); + + if (empty($config)) { + throw new \Exception("No '$provider' configuration found. Add one under Admin → Integrations."); + } + + $class = self::$providers[$provider]; + return new $class($config); + } + + /** + * Read and normalize a provider's config from sp_settings. + * Returns [] if not found or api_key is empty. + */ + public static function getAppConfig(string $provider): array + { + $provider = strtolower($provider); + $settingsKey = self::$settingsKeys[$provider] ?? null; + if (!$settingsKey) return []; + + $row = \Db::getRow( + "SELECT metval FROM sp_settings WHERE `group` = 'llm' AND keyval = ? LIMIT 1", + [$settingsKey] + ); + + if (!$row || empty($row['metval'])) return []; + + $raw = json_decode($row['metval'], true); + if (!is_array($raw) || empty($raw['api_key'])) return []; + + // Normalize sp_settings field names to LLMProvider expected names + return array_filter([ + 'secretKey' => $raw['api_key'], + 'model' => $raw['model'] ?? null, + 'max_tokens' => $raw['max_tokens'] ?? null, + 'temperature' => $raw['temperature'] ?? null, + ]); + } + + /** + * Return the list of application-level providers that have a non-empty api_key. + * + * @return array e.g. ['anthropic', 'openai'] + */ + public static function getAppProviders(): array + { + $available = []; + + foreach (self::$settingsKeys as $provider => $keyval) { + $row = \Db::getRow( + "SELECT metval FROM sp_settings WHERE `group` = 'llm' AND keyval = ? LIMIT 1", + [$keyval] + ); + if (!$row || empty($row['metval'])) continue; + $cfg = json_decode($row['metval'], true); + if (!empty($cfg['api_key'])) { + $available[] = $provider; + } + } + + return $available; + } + + /** + * Return the first configured application-level provider (by priority), or null. + */ + public static function getDefaultAppProvider(): ?string + { + $available = self::getAppProviders(); + foreach (self::$priority as $p) { + if (in_array($p, $available)) return $p; + } + return null; + } + + // ─── Org-level (sp_orgs_meta) ──────────────────────────────────────────── + + /** + * Return a configured provider instance from sp_orgs_meta (org-level override). + * + * @throws \Exception if provider unknown or not configured for the org + */ + public static function forOrg(int $orgId, string $provider): LLMProvider + { + $provider = strtolower($provider); + + if (!isset(self::$providers[$provider])) { + throw new \Exception("Unknown LLM provider: '$provider'"); + } + + $config = self::getOrgConfig($orgId, $provider); + + if (empty($config)) { + throw new \Exception("No '$provider' config found for org $orgId"); + } + + $class = self::$providers[$provider]; + return new $class($config); + } + + /** + * Read and decode a provider config from sp_orgs_meta for the given org. + */ + public static function getOrgConfig(int $orgId, string $provider): array + { + $provider = strtolower($provider); + $metaKey = self::$metaKeys[$provider] ?? null; + if (!$metaKey) return []; + + $row = \Db::getRow( + "SELECT metval FROM sp_orgs_meta WHERE orgid = ? AND keyval = ? AND active = 1 LIMIT 1", + [$orgId, $metaKey] + ); + + if (!$row || empty($row['metval'])) return []; + + $config = json_decode($row['metval'], true); + return is_array($config) ? $config : []; + } + + /** + * Return which providers are configured for the given org (sp_orgs_meta only). + */ + public static function getAvailableProviders(int $orgId): array + { + $metaKeys = array_values(self::$metaKeys); + $rows = \Db::select( + "SELECT keyval FROM sp_orgs_meta WHERE orgid = ? AND keyval IN ('" . implode("','", $metaKeys) . "') AND active = 1", + [$orgId] + ); + + $flip = array_flip(self::$metaKeys); + $available = []; + + foreach ($rows as $row) { + if (isset($flip[$row['keyval']])) { + $available[] = $flip[$row['keyval']]; + } + } + + return $available; + } + + /** + * Save or update a provider config for an org in sp_orgs_meta. + */ + public static function saveOrgConfig(int $orgId, string $provider, array $config): void + { + $provider = strtolower($provider); + $metaKey = self::$metaKeys[$provider] ?? null; + + if (!$metaKey) { + throw new \Exception("Unknown LLM provider: '$provider'"); + } + + $existing = \Db::getRow( + "SELECT orgmetaid FROM sp_orgs_meta WHERE orgid = ? AND keyval = ? LIMIT 1", + [$orgId, $metaKey] + ); + + if ($existing) { + \Db::update('sp_orgs_meta', + ['metval' => json_encode($config)], + 'orgmetaid = ?', + [$existing['orgmetaid']] + ); + } else { + \Db::insert('sp_orgs_meta', [ + 'orgid' => $orgId, + 'keyval' => $metaKey, + 'metval' => json_encode($config), + 'active' => 1, + ]); + } + } +} diff --git a/api/app/LLM/LLMProvider.php b/api/app/LLM/LLMProvider.php new file mode 100644 index 0000000..d35c46e --- /dev/null +++ b/api/app/LLM/LLMProvider.php @@ -0,0 +1,191 @@ + 'user', 'content' => '...']] + * @param array $options Override defaults (model, max_tokens, temperature, etc.) + * @return array ['success' => bool, 'content' => string, 'usage' => array, 'error' => string] + */ + abstract public function sendPrompt(array $messages, array $options = []): array; + + /** + * Stream a prompt response chunk by chunk. + * + * @param array $messages + * @param array $options + * @param callable $callback Called with each chunk: function(string $chunk) + * @return array ['success' => bool, 'error' => string] + */ + abstract public function streamPrompt(array $messages, array $options, callable $callback): array; + + /** + * Return available models for this provider. + * + * @return array [['id' => '...', 'label' => '...']] + */ + abstract public function getModels(): array; + + /** + * Test the API key is valid and the provider is reachable. + * + * @return array ['success' => bool, 'latency_ms' => int, 'error' => string] + */ + abstract public function testConnection(): array; + + // ─── Fluent Setters ────────────────────────────────────────────────────── + + public function getModel(): string + { + return $this->model; + } + + public function setModel(string $model): static + { + $this->model = $model; + return $this; + } + + public function setMaxTokens(int $tokens): static + { + $this->maxTokens = $tokens; + return $this; + } + + public function setSystemPrompt(string $prompt): static + { + $this->systemPrompt = $prompt; + return $this; + } + + public function setTemperature(float $temp): static + { + $this->temperature = $temp; + return $this; + } + + public function setTimeout(int $seconds): static + { + $this->timeout = $seconds; + return $this; + } + + // ─── Shared Helpers ────────────────────────────────────────────────────── + + /** + * Build a standard error response. + */ + protected function errorResponse(string $message): array + { + return ['success' => false, 'content' => '', 'usage' => [], 'error' => $message]; + } + + /** + * Build a standard success response. + */ + protected function successResponse(string $content, array $usage = []): array + { + return ['success' => true, 'content' => $content, 'usage' => $usage, 'error' => '']; + } + + /** + * Execute a cURL request and return the decoded JSON response. + */ + protected function curlPost(string $url, array $headers, array $payload): array + { + $ch = curl_init($url); + curl_setopt_array($ch, [ + CURLOPT_RETURNTRANSFER => true, + CURLOPT_POST => true, + CURLOPT_POSTFIELDS => json_encode($payload), + CURLOPT_HTTPHEADER => $headers, + CURLOPT_TIMEOUT => $this->timeout, + ]); + + $body = curl_exec($ch); + $error = curl_error($ch); + $code = curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); + + if ($error) { + throw new \Exception("cURL error: $error"); + } + + $decoded = json_decode($body, true); + + if ($code >= 400) { + $msg = $decoded['error']['message'] ?? $decoded['message'] ?? "HTTP $code error"; + throw new \Exception($msg); + } + + return $decoded; + } + + /** + * Execute a streaming cURL request, calling $callback for each SSE data chunk. + */ + protected function curlStream(string $url, array $headers, array $payload, callable $callback): void + { + $payload['stream'] = true; + + $errorBody = ''; + + $ch = curl_init($url); + curl_setopt_array($ch, [ + CURLOPT_RETURNTRANSFER => false, + CURLOPT_POST => true, + CURLOPT_POSTFIELDS => json_encode($payload), + CURLOPT_HTTPHEADER => $headers, + CURLOPT_TIMEOUT => $this->timeout, + CURLOPT_WRITEFUNCTION => function($ch, $data) use ($callback, &$errorBody) { + $lines = explode("\n", $data); + $hasSseData = false; + foreach ($lines as $line) { + $line = trim($line); + if (str_starts_with($line, 'data: ')) { + $hasSseData = true; + $json = substr($line, 6); + if ($json === '[DONE]') break; + $chunk = json_decode($json, true); + if ($chunk) $callback($chunk); + } + } + // If no SSE data lines were found this chunk may be an error body + if (!$hasSseData) { + $errorBody .= $data; + } + return strlen($data); + }, + ]); + + curl_exec($ch); + $curlError = curl_error($ch); + $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); + + if ($curlError) { + throw new \Exception("cURL stream error: $curlError"); + } + + if ($httpCode >= 400) { + $decoded = json_decode(trim($errorBody), true); + $msg = $decoded['error']['message'] ?? $decoded['message'] ?? "API error (HTTP $httpCode)"; + throw new \Exception($msg); + } + } +} diff --git a/api/app/LLM/OpenAIsvc.php b/api/app/LLM/OpenAIsvc.php new file mode 100644 index 0000000..678c850 --- /dev/null +++ b/api/app/LLM/OpenAIsvc.php @@ -0,0 +1,11 @@ +sendPrompt([['role' => 'user', 'content' => 'Hello']]); + * echo $res['content']; + */ +class svcAnthropic extends LLMProvider +{ + private const API_BASE = 'https://api.anthropic.com/v1'; + private const API_CHAT = self::API_BASE . '/messages'; + private const API_VERSION = '2023-06-01'; + + private static array $availableModels = [ + ['id' => 'claude-opus-4-6', 'label' => 'Claude Opus 4.6'], + ['id' => 'claude-sonnet-4-6', 'label' => 'Claude Sonnet 4.6'], + ['id' => 'claude-haiku-4-5-20251001', 'label' => 'Claude Haiku 4.5'], + ]; + + public function __construct(array $config) + { + if (empty($config['secretKey'])) { + throw new \Exception('Anthropic secretKey is required'); + } + + $this->apiKey = $config['secretKey']; + $this->model = $config['model'] ?? 'claude-sonnet-4-6'; + $this->maxTokens = (int)($config['max_tokens'] ?? 4096); + $this->temperature = (float)($config['temperature'] ?? 0.7); + } + + /** + * Send a chat prompt and return the full response. + */ + public function sendPrompt(array $messages, array $options = []): array + { + try { + $payload = $this->buildPayload($messages, $options); + $response = $this->curlPost(self::API_CHAT, $this->headers(!empty($options['cache_system'])), $payload); + + $content = $response['content'][0]['text'] ?? ''; + $usage = $response['usage'] ?? []; + + return $this->successResponse($content, [ + 'prompt_tokens' => $usage['input_tokens'] ?? 0, + 'completion_tokens' => $usage['output_tokens'] ?? 0, + 'total_tokens' => ($usage['input_tokens'] ?? 0) + ($usage['output_tokens'] ?? 0), + ]); + + } catch (\Exception $e) { + return $this->errorResponse($e->getMessage()); + } + } + + /** + * Stream a chat prompt, calling $callback with each text chunk. + * + * @param callable $callback function(string $chunk) — called with each partial text + */ + public function streamPrompt(array $messages, array $options, callable $callback): array + { + try { + $payload = $this->buildPayload($messages, $options); + $streamError = null; + + $this->curlStream(self::API_CHAT, $this->headers(!empty($options['cache_system'])), $payload, function(array $chunk) use ($callback, &$streamError) { + $type = $chunk['type'] ?? ''; + + if ($type === 'content_block_delta') { + $delta = $chunk['delta']['text'] ?? ''; + if ($delta !== '') { + $callback($delta); + } + } elseif ($type === 'error') { + // Anthropic sends inline error events during streaming + $streamError = $chunk['error']['message'] ?? 'Anthropic streaming error'; + } + }); + + if ($streamError) { + return ['success' => false, 'error' => $streamError]; + } + + return ['success' => true, 'error' => '']; + + } catch (\Exception $e) { + return ['success' => false, 'error' => $e->getMessage()]; + } + } + + /** + * Return supported Claude models. + */ + public function getModels(): array + { + return self::$availableModels; + } + + /** + * Validate the API key with a minimal single-token request. + */ + public function testConnection(): array + { + try { + $start = microtime(true); + + $payload = [ + 'model' => $this->model, + 'max_tokens' => 1, + 'messages' => [['role' => 'user', 'content' => 'hi']], + ]; + + $this->curlPost(self::API_CHAT, $this->headers(), $payload); + + $latency = (int)((microtime(true) - $start) * 1000); + + return ['success' => true, 'latency_ms' => $latency, 'error' => '']; + + } catch (\Exception $e) { + return ['success' => false, 'latency_ms' => 0, 'error' => $e->getMessage()]; + } + } + + // ─── Private Helpers ───────────────────────────────────────────────────── + + private function headers(bool $withCache = false): array + { + $h = [ + 'Content-Type: application/json', + 'x-api-key: ' . $this->apiKey, + 'anthropic-version: ' . self::API_VERSION, + ]; + if ($withCache) { + $h[] = 'anthropic-beta: prompt-caching-2024-07-31'; + } + return $h; + } + + private function buildPayload(array $messages, array $options): array + { + $model = $options['model'] ?? $this->model; + $maxTokens = (int)($options['max_tokens'] ?? $this->maxTokens); + $temperature = (float)($options['temperature'] ?? $this->temperature); + $cacheSystem = !empty($options['cache_system']); + + $payload = [ + 'model' => $model, + 'max_tokens' => $maxTokens, + 'temperature' => $temperature, + 'messages' => $messages, + ]; + + // Anthropic uses a top-level 'system' key. + // When cache_system is set, use block format with cache_control so + // the large static report payload is cached between turns. + if ($this->systemPrompt !== '') { + if ($cacheSystem) { + $payload['system'] = [[ + 'type' => 'text', + 'text' => $this->systemPrompt, + 'cache_control' => ['type' => 'ephemeral'], + ]]; + } else { + $payload['system'] = $this->systemPrompt; + } + } + + return $payload; + } +} diff --git a/api/app/LLM/svcDeepSeek.php b/api/app/LLM/svcDeepSeek.php new file mode 100644 index 0000000..977708b --- /dev/null +++ b/api/app/LLM/svcDeepSeek.php @@ -0,0 +1,177 @@ +sendPrompt([['role' => 'user', 'content' => 'Hello']]); + * echo $res['content']; + */ +class svcDeepSeek extends LLMProvider +{ + private const API_BASE = 'https://api.deepseek.com/v1'; + private const API_CHAT = self::API_BASE . '/chat/completions'; + private const API_MODELS = self::API_BASE . '/models'; + + private static array $availableModels = [ + ['id' => 'deepseek-chat', 'label' => 'DeepSeek Chat (V3)'], + ['id' => 'deepseek-reasoner', 'label' => 'DeepSeek Reasoner (R1)'], + ]; + + public function __construct(array $config) + { + if (empty($config['secretKey'])) { + throw new \Exception('DeepSeek secretKey is required'); + } + + $this->apiKey = $config['secretKey']; + $this->model = $config['model'] ?? 'deepseek-chat'; + $this->maxTokens = (int)($config['max_tokens'] ?? 4096); + $this->temperature = (float)($config['temperature'] ?? 0.7); + } + + /** + * Send a chat prompt and return the full response. + */ + public function sendPrompt(array $messages, array $options = []): array + { + try { + $payload = $this->buildPayload($messages, $options); + $response = $this->curlPost(self::API_CHAT, $this->headers(), $payload); + + $content = $response['choices'][0]['message']['content'] ?? ''; + $usage = $response['usage'] ?? []; + + // DeepSeek Reasoner includes reasoning_content separately + $reasoning = $response['choices'][0]['message']['reasoning_content'] ?? ''; + + return $this->successResponse($content, [ + 'prompt_tokens' => $usage['prompt_tokens'] ?? 0, + 'completion_tokens' => $usage['completion_tokens'] ?? 0, + 'total_tokens' => $usage['total_tokens'] ?? 0, + 'reasoning_tokens' => $usage['completion_tokens_details']['reasoning_tokens'] ?? 0, + 'reasoning_content' => $reasoning, + ]); + + } catch (\Exception $e) { + return $this->errorResponse($e->getMessage()); + } + } + + /** + * Stream a chat prompt, calling $callback with each text chunk. + * + * @param callable $callback function(string $chunk) + */ + public function streamPrompt(array $messages, array $options, callable $callback): array + { + try { + $payload = $this->buildPayload($messages, $options); + + $this->curlStream(self::API_CHAT, $this->headers(), $payload, function(array $chunk) use ($callback) { + $delta = $chunk['choices'][0]['delta']['content'] ?? ''; + if ($delta !== '') { + $callback($delta); + } + }); + + return ['success' => true, 'error' => '']; + + } catch (\Exception $e) { + return ['success' => false, 'error' => $e->getMessage()]; + } + } + + /** + * Return supported DeepSeek models. + */ + public function getModels(): array + { + return self::$availableModels; + } + + /** + * Validate the API key via the models endpoint. + */ + public function testConnection(): array + { + try { + $start = microtime(true); + + $ch = curl_init(self::API_MODELS); + curl_setopt_array($ch, [ + CURLOPT_RETURNTRANSFER => true, + CURLOPT_HTTPHEADER => $this->headers(), + CURLOPT_TIMEOUT => $this->timeout, + ]); + + $body = curl_exec($ch); + $error = curl_error($ch); + $code = curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); + + $latency = (int)((microtime(true) - $start) * 1000); + + if ($error) throw new \Exception("cURL error: $error"); + + $decoded = json_decode($body, true); + + if ($code !== 200) { + $msg = $decoded['error']['message'] ?? "HTTP $code"; + throw new \Exception($msg); + } + + return ['success' => true, 'latency_ms' => $latency, 'error' => '']; + + } catch (\Exception $e) { + return ['success' => false, 'latency_ms' => 0, 'error' => $e->getMessage()]; + } + } + + // ─── Private Helpers ───────────────────────────────────────────────────── + + private function headers(): array + { + return [ + 'Content-Type: application/json', + 'Authorization: Bearer ' . $this->apiKey, + ]; + } + + private function buildPayload(array $messages, array $options): array + { + $model = $options['model'] ?? $this->model; + $maxTokens = (int)($options['max_tokens'] ?? $this->maxTokens); + $temperature = (float)($options['temperature'] ?? $this->temperature); + + if ($this->systemPrompt !== '') { + array_unshift($messages, ['role' => 'system', 'content' => $this->systemPrompt]); + } + + $payload = [ + 'model' => $model, + 'messages' => $messages, + 'max_tokens' => $maxTokens, + 'temperature' => $temperature, + ]; + + // deepseek-reasoner does not support temperature + if ($model === 'deepseek-reasoner') { + unset($payload['temperature']); + } + + return $payload; + } +} diff --git a/api/app/LLM/svcGemini.php b/api/app/LLM/svcGemini.php new file mode 100644 index 0000000..6d67d96 --- /dev/null +++ b/api/app/LLM/svcGemini.php @@ -0,0 +1,172 @@ +sendPrompt([['role' => 'user', 'content' => 'Hello']]); + * echo $res['content']; + */ +class svcGemini extends LLMProvider +{ + private const API_BASE = 'https://generativelanguage.googleapis.com/v1beta/models'; + private const API_CHAT = ':generateContent'; + private const API_STREAM = ':streamGenerateContent'; + + private static array $availableModels = [ + ['id' => 'gemini-2.5-flash', 'label' => 'Gemini 2.5 Flash'], + ['id' => 'gemini-2.5-flash-lite', 'label' => 'Gemini 2.5 Flash Lite'], + ['id' => 'gemini-2.0-flash', 'label' => 'Gemini 2.0 Flash'], + ['id' => 'gemini-1.5-pro', 'label' => 'Gemini 1.5 Pro'], + ['id' => 'gemini-1.5-flash', 'label' => 'Gemini 1.5 Flash'], + ]; + + public function __construct(array $config) + { + if (empty($config['secretKey'])) { + throw new \Exception('Gemini secretKey is required'); + } + + $this->apiKey = $config['secretKey']; + $this->model = $config['model'] ?? 'gemini-2.5-flash'; + $this->maxTokens = (int)($config['max_tokens'] ?? 4096); + $this->temperature = (float)($config['temperature'] ?? 0.7); + } + + /** + * Send a chat prompt and return the full response. + */ + public function sendPrompt(array $messages, array $options = []): array + { + try { + $model = $options['model'] ?? $this->model; + $url = self::API_BASE . '/' . $model . self::API_CHAT . '?key=' . $this->apiKey; + $payload = $this->buildPayload($messages, $options); + + $response = $this->curlPost($url, $this->headers(), $payload); + + $content = $response['candidates'][0]['content']['parts'][0]['text'] ?? ''; + $usage = $response['usageMetadata'] ?? []; + + return $this->successResponse($content, [ + 'prompt_tokens' => $usage['promptTokenCount'] ?? 0, + 'completion_tokens' => $usage['candidatesTokenCount'] ?? 0, + 'total_tokens' => $usage['totalTokenCount'] ?? 0, + ]); + + } catch (\Exception $e) { + return $this->errorResponse($e->getMessage()); + } + } + + /** + * Stream a chat prompt, calling $callback with each text chunk. + * + * @param callable $callback function(string $chunk) + */ + public function streamPrompt(array $messages, array $options, callable $callback): array + { + try { + $model = $options['model'] ?? $this->model; + $url = self::API_BASE . '/' . $model . self::API_STREAM . '?key=' . $this->apiKey . '&alt=sse'; + $payload = $this->buildPayload($messages, $options); + + $this->curlStream($url, $this->headers(), $payload, function(array $chunk) use ($callback) { + $text = $chunk['candidates'][0]['content']['parts'][0]['text'] ?? ''; + if ($text !== '') { + $callback($text); + } + }); + + return ['success' => true, 'error' => '']; + + } catch (\Exception $e) { + return ['success' => false, 'error' => $e->getMessage()]; + } + } + + /** + * Return supported Gemini models. + */ + public function getModels(): array + { + return self::$availableModels; + } + + /** + * Validate the API key with a minimal request. + */ + public function testConnection(): array + { + try { + $start = microtime(true); + $model = $this->model; + $url = self::API_BASE . '/' . $model . self::API_CHAT . '?key=' . $this->apiKey; + $payload = [ + 'contents' => [['role' => 'user', 'parts' => [['text' => 'hi']]]], + 'generationConfig' => ['maxOutputTokens' => 1], + ]; + + $this->curlPost($url, $this->headers(), $payload); + + $latency = (int)((microtime(true) - $start) * 1000); + + return ['success' => true, 'latency_ms' => $latency, 'error' => '']; + + } catch (\Exception $e) { + return ['success' => false, 'latency_ms' => 0, 'error' => $e->getMessage()]; + } + } + + // ─── Private Helpers ───────────────────────────────────────────────────── + + private function headers(): array + { + return ['Content-Type: application/json']; + } + + private function buildPayload(array $messages, array $options): array + { + $maxTokens = (int)($options['max_tokens'] ?? $this->maxTokens); + $temperature = (float)($options['temperature'] ?? $this->temperature); + + // Convert OpenAI-style messages to Gemini format + // Roles: 'user' stays 'user', 'assistant' becomes 'model' + $contents = []; + foreach ($messages as $msg) { + $contents[] = [ + 'role' => $msg['role'] === 'assistant' ? 'model' : 'user', + 'parts' => [['text' => $msg['content']]], + ]; + } + + $payload = [ + 'contents' => $contents, + 'generationConfig' => [ + 'maxOutputTokens' => $maxTokens, + 'temperature' => $temperature, + ], + ]; + + // Gemini uses a top-level 'systemInstruction' for system prompts + if ($this->systemPrompt !== '') { + $payload['systemInstruction'] = [ + 'parts' => [['text' => $this->systemPrompt]], + ]; + } + + return $payload; + } +} diff --git a/api/app/LLM/svcMistral.php b/api/app/LLM/svcMistral.php new file mode 100644 index 0000000..a6a9586 --- /dev/null +++ b/api/app/LLM/svcMistral.php @@ -0,0 +1,169 @@ +sendPrompt([['role' => 'user', 'content' => 'Hello']]); + * echo $res['content']; + */ +class svcMistral extends LLMProvider +{ + private const API_BASE = 'https://api.mistral.ai/v1'; + private const API_CHAT = self::API_BASE . '/chat/completions'; + private const API_MODELS = self::API_BASE . '/models'; + + private static array $availableModels = [ + ['id' => 'mistral-large-latest', 'label' => 'Mistral Large'], + ['id' => 'mistral-medium-latest', 'label' => 'Mistral Medium'], + ['id' => 'mistral-small-latest', 'label' => 'Mistral Small'], + ['id' => 'codestral-latest', 'label' => 'Codestral'], + ['id' => 'open-mistral-nemo', 'label' => 'Mistral Nemo'], + ['id' => 'open-mixtral-8x22b', 'label' => 'Mixtral 8x22B'], + ]; + + public function __construct(array $config) + { + if (empty($config['secretKey'])) { + throw new \Exception('Mistral secretKey is required'); + } + + $this->apiKey = $config['secretKey']; + $this->model = $config['model'] ?? 'mistral-large-latest'; + $this->maxTokens = (int)($config['max_tokens'] ?? 4096); + $this->temperature = (float)($config['temperature'] ?? 0.7); + } + + /** + * Send a chat prompt and return the full response. + */ + public function sendPrompt(array $messages, array $options = []): array + { + try { + $payload = $this->buildPayload($messages, $options); + $response = $this->curlPost(self::API_CHAT, $this->headers(), $payload); + + $content = $response['choices'][0]['message']['content'] ?? ''; + $usage = $response['usage'] ?? []; + + return $this->successResponse($content, [ + 'prompt_tokens' => $usage['prompt_tokens'] ?? 0, + 'completion_tokens' => $usage['completion_tokens'] ?? 0, + 'total_tokens' => $usage['total_tokens'] ?? 0, + ]); + + } catch (\Exception $e) { + return $this->errorResponse($e->getMessage()); + } + } + + /** + * Stream a chat prompt, calling $callback with each text chunk. + * + * @param callable $callback function(string $chunk) + */ + public function streamPrompt(array $messages, array $options, callable $callback): array + { + try { + $payload = $this->buildPayload($messages, $options); + + $this->curlStream(self::API_CHAT, $this->headers(), $payload, function(array $chunk) use ($callback) { + $delta = $chunk['choices'][0]['delta']['content'] ?? ''; + if ($delta !== '') { + $callback($delta); + } + }); + + return ['success' => true, 'error' => '']; + + } catch (\Exception $e) { + return ['success' => false, 'error' => $e->getMessage()]; + } + } + + /** + * Return supported Mistral models. + */ + public function getModels(): array + { + return self::$availableModels; + } + + /** + * Validate the API key via the models endpoint. + */ + public function testConnection(): array + { + try { + $start = microtime(true); + + $ch = curl_init(self::API_MODELS); + curl_setopt_array($ch, [ + CURLOPT_RETURNTRANSFER => true, + CURLOPT_HTTPHEADER => $this->headers(), + CURLOPT_TIMEOUT => $this->timeout, + ]); + + $body = curl_exec($ch); + $error = curl_error($ch); + $code = curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); + + $latency = (int)((microtime(true) - $start) * 1000); + + if ($error) throw new \Exception("cURL error: $error"); + + $decoded = json_decode($body, true); + + if ($code !== 200) { + $msg = $decoded['message'] ?? "HTTP $code"; + throw new \Exception($msg); + } + + return ['success' => true, 'latency_ms' => $latency, 'error' => '']; + + } catch (\Exception $e) { + return ['success' => false, 'latency_ms' => 0, 'error' => $e->getMessage()]; + } + } + + // ─── Private Helpers ───────────────────────────────────────────────────── + + private function headers(): array + { + return [ + 'Content-Type: application/json', + 'Authorization: Bearer ' . $this->apiKey, + ]; + } + + private function buildPayload(array $messages, array $options): array + { + $model = $options['model'] ?? $this->model; + $maxTokens = (int)($options['max_tokens'] ?? $this->maxTokens); + $temperature = (float)($options['temperature'] ?? $this->temperature); + + if ($this->systemPrompt !== '') { + array_unshift($messages, ['role' => 'system', 'content' => $this->systemPrompt]); + } + + return [ + 'model' => $model, + 'messages' => $messages, + 'max_tokens' => $maxTokens, + 'temperature' => $temperature, + ]; + } +} diff --git a/api/app/LLM/svcOpenAI.php b/api/app/LLM/svcOpenAI.php new file mode 100644 index 0000000..3ac932f --- /dev/null +++ b/api/app/LLM/svcOpenAI.php @@ -0,0 +1,175 @@ +sendPrompt([['role' => 'user', 'content' => 'Hello']]); + * echo $res['content']; + */ +class svcOpenAI extends LLMProvider +{ + private const API_BASE = 'https://api.openai.com/v1'; + private const API_CHAT = self::API_BASE . '/chat/completions'; + private const API_MODELS = self::API_BASE . '/models'; + + private static array $availableModels = [ + ['id' => 'gpt-4o', 'label' => 'GPT-4o'], + ['id' => 'gpt-4o-mini', 'label' => 'GPT-4o Mini'], + ['id' => 'gpt-4-turbo', 'label' => 'GPT-4 Turbo'], + ['id' => 'gpt-4', 'label' => 'GPT-4'], + ['id' => 'gpt-3.5-turbo', 'label' => 'GPT-3.5 Turbo'], + ['id' => 'o1', 'label' => 'o1'], + ['id' => 'o1-mini', 'label' => 'o1 Mini'], + ['id' => 'o3-mini', 'label' => 'o3 Mini'], + ]; + + public function __construct(array $config) + { + if (empty($config['secretKey'])) { + throw new \Exception('OpenAI secretKey is required'); + } + + $this->apiKey = $config['secretKey']; + $this->model = $config['model'] ?? 'gpt-4o'; + $this->maxTokens = (int)($config['max_tokens'] ?? 4096); + $this->temperature = (float)($config['temperature'] ?? 0.7); + } + + /** + * Send a chat prompt and return the full response. + */ + public function sendPrompt(array $messages, array $options = []): array + { + try { + $payload = $this->buildPayload($messages, $options); + $response = $this->curlPost(self::API_CHAT, $this->headers(), $payload); + + $content = $response['choices'][0]['message']['content'] ?? ''; + $usage = $response['usage'] ?? []; + + return $this->successResponse($content, [ + 'prompt_tokens' => $usage['prompt_tokens'] ?? 0, + 'completion_tokens' => $usage['completion_tokens'] ?? 0, + 'total_tokens' => $usage['total_tokens'] ?? 0, + ]); + + } catch (\Exception $e) { + return $this->errorResponse($e->getMessage()); + } + } + + /** + * Stream a chat prompt, calling $callback with each text chunk. + * + * @param callable $callback function(string $chunk) — called with each partial text + */ + public function streamPrompt(array $messages, array $options, callable $callback): array + { + try { + $payload = $this->buildPayload($messages, $options); + + $this->curlStream(self::API_CHAT, $this->headers(), $payload, function(array $chunk) use ($callback) { + $delta = $chunk['choices'][0]['delta']['content'] ?? ''; + if ($delta !== '') { + $callback($delta); + } + }); + + return ['success' => true, 'error' => '']; + + } catch (\Exception $e) { + return ['success' => false, 'error' => $e->getMessage()]; + } + } + + /** + * Return the static list of supported OpenAI models. + */ + public function getModels(): array + { + return self::$availableModels; + } + + /** + * Validate the API key by calling the models endpoint. + */ + public function testConnection(): array + { + try { + $start = microtime(true); + + $ch = curl_init(self::API_MODELS); + curl_setopt_array($ch, [ + CURLOPT_RETURNTRANSFER => true, + CURLOPT_HTTPHEADER => $this->headers(), + CURLOPT_TIMEOUT => $this->timeout, + ]); + + $body = curl_exec($ch); + $error = curl_error($ch); + $code = curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); + + $latency = (int)((microtime(true) - $start) * 1000); + + if ($error) throw new \Exception("cURL error: $error"); + + $decoded = json_decode($body, true); + + if ($code !== 200) { + $msg = $decoded['error']['message'] ?? "HTTP $code"; + throw new \Exception($msg); + } + + return ['success' => true, 'latency_ms' => $latency, 'error' => '']; + + } catch (\Exception $e) { + return ['success' => false, 'latency_ms' => 0, 'error' => $e->getMessage()]; + } + } + + // ─── Private Helpers ───────────────────────────────────────────────────── + + private function headers(): array + { + return [ + 'Content-Type: application/json', + 'Authorization: Bearer ' . $this->apiKey, + ]; + } + + private function buildPayload(array $messages, array $options): array + { + $model = $options['model'] ?? $this->model; + $maxTokens = (int)($options['max_tokens'] ?? $this->maxTokens); + $temperature = (float)($options['temperature'] ?? $this->temperature); + + // Prepend system prompt if set + if ($this->systemPrompt !== '') { + array_unshift($messages, ['role' => 'system', 'content' => $this->systemPrompt]); + } + + $payload = [ + 'model' => $model, + 'messages' => $messages, + 'max_tokens' => $maxTokens, + ]; + + // o1/o3 models don't support temperature + if (!str_starts_with($model, 'o1') && !str_starts_with($model, 'o3')) { + $payload['temperature'] = $temperature; + } + + return $payload; + } +} diff --git a/api/commands/GreetCommand.php b/api/commands/GreetCommand.php new file mode 100644 index 0000000..372a54c --- /dev/null +++ b/api/commands/GreetCommand.php @@ -0,0 +1,55 @@ +setName($this->commandName) + ->setDescription($this->commandDescription) + ->addArgument( + $this->commandArgumentName, + InputArgument::OPTIONAL, + $this->commandArgumentDescription + ) + ->addOption( + $this->commandOptionName, + null, + InputOption::VALUE_NONE, + $this->commandOptionDescription + ) + ; + } + + protected function execute(InputInterface $input, OutputInterface $output) + { + $name = $input->getArgument($this->commandArgumentName); + + if ($name) { + $text = 'Hello '.$name; + } else { + $text = 'Hello'; + } + + if ($input->getOption($this->commandOptionName)) { + $text = strtoupper($text); + } + + $output->writeln($text); + return Command::SUCCESS; + } +} \ No newline at end of file diff --git a/api/commands/SeedMenuCommand.php b/api/commands/SeedMenuCommand.php new file mode 100644 index 0000000..bde1de1 --- /dev/null +++ b/api/commands/SeedMenuCommand.php @@ -0,0 +1,134 @@ +setName($this->commandName) + ->setDescription($this->commandDescription); + } + + protected function execute(InputInterface $input, OutputInterface $output) + { + $output->writeln('Creating tables...'); + + Db::execute(" + CREATE TABLE IF NOT EXISTS sp_menus ( + menu_id int unsigned NOT NULL AUTO_INCREMENT, + slug varchar(50) NOT NULL, + name varchar(100) NOT NULL, + active tinyint DEFAULT 1, + PRIMARY KEY (menu_id), + UNIQUE KEY (slug) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 + ", []); + + Db::execute(" + CREATE TABLE IF NOT EXISTS sp_menu_items ( + item_id int unsigned NOT NULL AUTO_INCREMENT, + menu_id int unsigned NOT NULL, + parent_id int unsigned DEFAULT NULL, + label varchar(100) NOT NULL, + icon varchar(100) DEFAULT NULL, + type enum('link','header','divider') DEFAULT 'link', + url varchar(255) DEFAULT NULL, + perm_id int unsigned DEFAULT NULL, + match_prefix tinyint DEFAULT 0, + sort_order int DEFAULT 0, + active tinyint DEFAULT 1, + PRIMARY KEY (item_id), + KEY (menu_id), + KEY (parent_id), + CONSTRAINT FOREIGN KEY (perm_id) REFERENCES sp_permissions(perm_id) ON DELETE SET NULL + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 + ", []); + + $output->writeln('Tables ready. Seeding data...'); + + // Idempotent: skip if already seeded + $existing = Db::getRow("SELECT menu_id FROM sp_menus WHERE slug = 'admin_sidebar' LIMIT 1"); + if ($existing) { + $output->writeln('admin_sidebar already exists — skipping seed. Run with --force to re-seed (delete rows manually first).'); + return Command::SUCCESS; + } + + // ── Insert menu ────────────────────────────────────────────────────────── + $menuResult = json_decode(Db::insert('sp_menus', [ + 'slug' => 'admin_sidebar', + 'name' => 'Admin Sidebar', + 'active' => 1, + ]), true); + + $menuId = (int)$menuResult['ID']; + $output->writeln("Menu ID: {$menuId}"); + + // Helper: fetch perm_id by controller.action, return null if not found + $perm = function(string $controller, string $action) use ($output): ?int { + $row = Db::getRow( + "SELECT perm_id FROM sp_permissions WHERE perm_controller = ? AND perm_action = ? LIMIT 1", + [$controller, $action] + ); + if (!$row) { + $output->writeln(" Permission not found: {$controller}.{$action} — using NULL"); + return null; + } + return (int)$row['perm_id']; + }; + + // Helper: insert item, return item_id + $insertItem = function(array $data) use ($menuId): int { + $data['menu_id'] = $menuId; + $result = json_decode(Db::insert('sp_menu_items', $data), true); + return (int)$result['ID']; + }; + + $sort = 10; + + // ── Parent: Manage Account ─────────────────────────────────────────────── + $acctId = $insertItem([ + 'parent_id' => null, + 'label' => 'Manage Account', + 'icon' => 'ki-duotone ki-address-book', + 'type' => 'link', + 'url' => null, + 'perm_id' => null, + 'sort_order' => $sort += 10, + ]); + + $subSort = 0; + $insertItem(['parent_id' => $acctId, 'label' => 'Account Settings', 'type' => 'link', 'url' => '/account', 'perm_id' => null, 'sort_order' => $subSort += 10]); + $insertItem(['parent_id' => $acctId, 'label' => 'Billing', 'type' => 'link', 'url' => '/account/billing', 'perm_id' => null, 'sort_order' => $subSort += 10]); + $insertItem(['parent_id' => $acctId, 'label' => 'Invoices', 'type' => 'link', 'url' => '/account/invoices', 'perm_id' => null, 'sort_order' => $subSort += 10]); + $insertItem(['parent_id' => $acctId, 'label' => 'Integrations & API', 'type' => 'link', 'url' => '/account/integrations', 'perm_id' => null, 'sort_order' => $subSort += 10]); + $insertItem(['parent_id' => $acctId, 'label' => 'Activity Logs', 'type' => 'link', 'url' => '/account/activity', 'perm_id' => null, 'sort_order' => $subSort += 10]); + + // ── Parent: Administrator ──────────────────────────────────────────────── + $adminId = $insertItem([ + 'parent_id' => null, + 'label' => 'Administrator', + 'icon' => 'ki-duotone ki-shield-tick', + 'type' => 'link', + 'url' => null, + 'perm_id' => null, + 'sort_order' => $sort += 10, + ]); + + $subSort = 0; + $insertItem(['parent_id' => $adminId, 'label' => 'Overview', 'type' => 'link', 'url' => '/admin', 'perm_id' => $perm('admin', 'index'), 'sort_order' => $subSort += 10, 'match_prefix' => 0]); + $insertItem(['parent_id' => $adminId, 'label' => 'Manage Orgs', 'type' => 'link', 'url' => '/admin/orgs', 'perm_id' => $perm('admin', 'orgs'), 'sort_order' => $subSort += 10, 'match_prefix' => 1]); + $insertItem(['parent_id' => $adminId, 'label' => 'Manage Users', 'type' => 'link', 'url' => '/admin/users', 'perm_id' => $perm('admin', 'users'), 'sort_order' => $subSort += 10, 'match_prefix' => 1]); + $insertItem(['parent_id' => $adminId, 'label' => 'Integrations', 'type' => 'link', 'url' => '/admin/integrations', 'perm_id' => $perm('admin', 'integrations'), 'sort_order' => $subSort += 10]); + $insertItem(['parent_id' => $adminId, 'label' => 'User Roles', 'type' => 'link', 'url' => '/admin/roles', 'perm_id' => $perm('admin', 'roles'), 'sort_order' => $subSort += 10, 'match_prefix' => 1]); + $insertItem(['parent_id' => $adminId, 'label' => 'Activity Logs', 'type' => 'link', 'url' => '/admin/activity', 'perm_id' => $perm('admin', 'activity'), 'sort_order' => $subSort += 10]); + $insertItem(['parent_id' => $adminId, 'label' => 'Menu Builder', 'type' => 'link', 'url' => '/admin/menu', 'perm_id' => $perm('admin', 'menu'), 'sort_order' => $subSort += 10, 'match_prefix' => 1]); + + $output->writeln('Seeded successfully.'); + return Command::SUCCESS; + } +} diff --git a/api/commands/commands.md b/api/commands/commands.md new file mode 100644 index 0000000..0f75817 --- /dev/null +++ b/api/commands/commands.md @@ -0,0 +1,119 @@ +# CLI Commands — `commands/` + +Scripts run via `php console `. No HTTP context — output goes to stdout. + +## File Naming + +`PascalCaseCommand.php` — e.g. `SyncDataCommand.php` + +## Running Commands + +```bash +php console SyncDataCommand +php console SyncDataCommand --date="2024-10-28" --force +php console CleanupDatabaseCommand --days=30 --dry-run +php console ProcessEmailQueueCommand --limit=50 +``` + +## Command Structure + +```php + $record['external_id']] + ); + if ($exists) { echo "Skipping {$record['external_id']}\n"; continue; } + } + + Db::insert('sp_data', [ + 'external_id' => $record['external_id'], + 'payload' => $record['payload'], + 'synced_at' => date('Y-m-d H:i:s') + ]); + echo "Inserted {$record['external_id']}\n"; + } + + echo "Done.\n"; + +} catch (Exception $e) { + echo "ERROR: " . $e->getMessage() . "\n"; + exit(1); +} +``` + +## Error Handling Pattern + +Log individual failures and keep processing — exit with code 1 only on fatal errors: + +```php +$errors = []; +$processed = 0; + +foreach ($items as $item) { + try { + processItem($item); + $processed++; + echo "."; + } catch (Exception $e) { + $errors[] = ['id' => $item['id'], 'error' => $e->getMessage()]; + echo "E"; + } +} + +echo "\n\nProcessed: $processed | Errors: " . count($errors) . "\n"; + +if (!empty($errors)) { + foreach ($errors as $err) { + echo "Item #{$err['id']}: {$err['error']}\n"; + } + exit(1); +} +``` + +## Dry-Run Pattern + +```php +$dryRun = isset($options['dry-run']); +if ($dryRun) { echo "DRY RUN — no changes will be made\n"; } + +$count = Db::getRow("SELECT COUNT(*) as n FROM sp_logs WHERE created_at < :date", + [':date' => $cutoffDate] +); +echo "Rows to delete: {$count['n']}\n"; + +if (!$dryRun) { + Db::delete('sp_logs', 'created_at < :date', [':date' => $cutoffDate]); +} +``` + +## Cron Setup + +```bash +crontab -e +``` + +```cron +# Every 5 minutes +*/5 * * * * cd /www/wwwroot/appSeedProject && php console ProcessEmailQueueCommand >> /var/log/email-queue.log 2>&1 + +# Every hour +0 * * * * cd /www/wwwroot/appSeedProject && php console SyncDataCommand >> /var/log/sync.log 2>&1 + +# Nightly cleanup at 2 AM +0 2 * * * cd /www/wwwroot/appSeedProject && php console CleanupDatabaseCommand --days=30 >> /var/log/cleanup.log 2>&1 +``` diff --git a/api/composer.json b/api/composer.json new file mode 100644 index 0000000..cc94fe8 --- /dev/null +++ b/api/composer.json @@ -0,0 +1,49 @@ +{ + "name": "seedproject/seedproject", + "require": { + "anthropic-ai/sdk": "^0.3.0", + "carbonphp/carbon-doctrine-types": "^2.1.0", + "getbrevo/brevo-php": "^1.0.2", + "guzzlehttp/promises": "^2.3.0", + "guzzlehttp/psr7": "^2.8.0", + "mashape/unirest-php": "^3.0.4", + "nyholm/psr7": "^1.8.2", + "php-http/discovery": "^1.20.0", + "php-http/multipart-stream-builder": "^1.4.2", + "phpmailer/phpmailer": "^6.12.0", + "psr/container": "^2.0.2", + "psr/http-client": "^1.0.3", + "psr/http-factory": "^1.1.0", + "psr/http-message": "^2.0", + "symfony/console": "^5.4.47", + "symfony/deprecation-contracts": "^3.6.0", + "symfony/polyfill-ctype": "^1.33.0", + "symfony/polyfill-intl-grapheme": "^1.33.0", + "symfony/polyfill-intl-normalizer": "^1.33.0", + "symfony/polyfill-mbstring": "^1.33.0", + "symfony/polyfill-php73": "^1.33.0", + "symfony/polyfill-php80": "^1.33.0", + "symfony/service-contracts": "^3.6.1", + "symfony/string": "^6.4.30" + }, + "autoload": { + "classmap": [ + "core/", + "app/Helpers", + "app/Controllers", + "app/Components", + "app/Gateways", + "system/" + ], + "psr-4": { + "App\\": "app", + "Models\\": "models" + } + }, + "config": { + "platform-check": false, + "allow-plugins": { + "php-http/discovery": true + } + } +} diff --git a/api/composer.lock b/api/composer.lock new file mode 100644 index 0000000..10643f8 --- /dev/null +++ b/api/composer.lock @@ -0,0 +1,1970 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "74a4cde47eddbeff4dbeda5cea0ae363", + "packages": [ + { + "name": "anthropic-ai/sdk", + "version": "v0.3.0", + "source": { + "type": "git", + "url": "https://github.com/anthropics/anthropic-sdk-php.git", + "reference": "e41ffa21bc156e795b79abca7bfcc88686a12e3c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/anthropics/anthropic-sdk-php/zipball/e41ffa21bc156e795b79abca7bfcc88686a12e3c", + "reference": "e41ffa21bc156e795b79abca7bfcc88686a12e3c", + "shasum": "" + }, + "require": { + "php": "^8.1", + "php-http/discovery": "^1", + "psr/http-client": "^1", + "psr/http-client-implementation": "^1", + "psr/http-factory-implementation": "^1", + "psr/http-message": "^1|^2" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3", + "nyholm/psr7": "^1", + "pestphp/pest": "^3", + "phpstan/extension-installer": "^1", + "phpstan/phpstan": "^2", + "phpstan/phpstan-phpunit": "^2", + "phpunit/phpunit": "^11", + "symfony/http-client": "^7" + }, + "type": "library", + "autoload": { + "files": [ + "src/Core.php", + "src/Client.php" + ], + "psr-4": { + "Anthropic\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "description": "Anthropic PHP SDK", + "support": { + "issues": "https://github.com/anthropics/anthropic-sdk-php/issues", + "source": "https://github.com/anthropics/anthropic-sdk-php/tree/v0.3.0" + }, + "time": "2025-09-02T16:25:58+00:00" + }, + { + "name": "carbonphp/carbon-doctrine-types", + "version": "2.1.0", + "source": { + "type": "git", + "url": "https://github.com/CarbonPHP/carbon-doctrine-types.git", + "reference": "99f76ffa36cce3b70a4a6abce41dba15ca2e84cb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/CarbonPHP/carbon-doctrine-types/zipball/99f76ffa36cce3b70a4a6abce41dba15ca2e84cb", + "reference": "99f76ffa36cce3b70a4a6abce41dba15ca2e84cb", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0" + }, + "conflict": { + "doctrine/dbal": "<3.7.0 || >=4.0.0" + }, + "require-dev": { + "doctrine/dbal": "^3.7.0", + "nesbot/carbon": "^2.71.0 || ^3.0.0", + "phpunit/phpunit": "^10.3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Carbon\\Doctrine\\": "src/Carbon/Doctrine/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "KyleKatarn", + "email": "kylekatarnls@gmail.com" + } + ], + "description": "Types to use Carbon in Doctrine", + "keywords": [ + "carbon", + "date", + "datetime", + "doctrine", + "time" + ], + "support": { + "issues": "https://github.com/CarbonPHP/carbon-doctrine-types/issues", + "source": "https://github.com/CarbonPHP/carbon-doctrine-types/tree/2.1.0" + }, + "funding": [ + { + "url": "https://github.com/kylekatarnls", + "type": "github" + }, + { + "url": "https://opencollective.com/Carbon", + "type": "open_collective" + }, + { + "url": "https://tidelift.com/funding/github/packagist/nesbot/carbon", + "type": "tidelift" + } + ], + "time": "2023-12-11T17:09:12+00:00" + }, + { + "name": "getbrevo/brevo-php", + "version": "v1.0.2", + "source": { + "type": "git", + "url": "https://github.com/getbrevo/brevo-php.git", + "reference": "6c3286e62327277fd8445cddb057d44e850722c0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/getbrevo/brevo-php/zipball/6c3286e62327277fd8445cddb057d44e850722c0", + "reference": "6c3286e62327277fd8445cddb057d44e850722c0", + "shasum": "" + }, + "require": { + "ext-curl": "*", + "ext-json": "*", + "ext-mbstring": "*", + "guzzlehttp/guzzle": "^7.4.0", + "php": ">=5.6" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "~1.12", + "phpunit/phpunit": "^4.8", + "squizlabs/php_codesniffer": "~2.6" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x.x-dev" + } + }, + "autoload": { + "psr-4": { + "Brevo\\Client\\": "lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Brevo Developers", + "email": "contact@brevo.com", + "homepage": "https://www.brevo.com/" + } + ], + "description": "Official Brevo provided RESTFul API V3 php library", + "homepage": "https://github.com/getbrevo/brevo-php", + "keywords": [ + "api", + "brevo", + "php", + "sdk", + "swagger" + ], + "support": { + "issues": "https://github.com/getbrevo/brevo-php/issues", + "source": "https://github.com/getbrevo/brevo-php/tree/v1.0.2" + }, + "time": "2023-07-14T10:00:50+00:00" + }, + { + "name": "guzzlehttp/guzzle", + "version": "7.10.0", + "source": { + "type": "git", + "url": "https://github.com/guzzle/guzzle.git", + "reference": "b51ac707cfa420b7bfd4e4d5e510ba8008e822b4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/b51ac707cfa420b7bfd4e4d5e510ba8008e822b4", + "reference": "b51ac707cfa420b7bfd4e4d5e510ba8008e822b4", + "shasum": "" + }, + "require": { + "ext-json": "*", + "guzzlehttp/promises": "^2.3", + "guzzlehttp/psr7": "^2.8", + "php": "^7.2.5 || ^8.0", + "psr/http-client": "^1.0", + "symfony/deprecation-contracts": "^2.2 || ^3.0" + }, + "provide": { + "psr/http-client-implementation": "1.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "ext-curl": "*", + "guzzle/client-integration-tests": "3.0.2", + "php-http/message-factory": "^1.1", + "phpunit/phpunit": "^8.5.39 || ^9.6.20", + "psr/log": "^1.1 || ^2.0 || ^3.0" + }, + "suggest": { + "ext-curl": "Required for CURL handler support", + "ext-intl": "Required for Internationalized Domain Name (IDN) support", + "psr/log": "Required for using the Log middleware" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "files": [ + "src/functions_include.php" + ], + "psr-4": { + "GuzzleHttp\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Jeremy Lindblom", + "email": "jeremeamia@gmail.com", + "homepage": "https://github.com/jeremeamia" + }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://github.com/sagikazarmark" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + } + ], + "description": "Guzzle is a PHP HTTP client library", + "keywords": [ + "client", + "curl", + "framework", + "http", + "http client", + "psr-18", + "psr-7", + "rest", + "web service" + ], + "support": { + "issues": "https://github.com/guzzle/guzzle/issues", + "source": "https://github.com/guzzle/guzzle/tree/7.10.0" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/guzzle", + "type": "tidelift" + } + ], + "time": "2025-08-23T22:36:01+00:00" + }, + { + "name": "guzzlehttp/promises", + "version": "2.3.0", + "source": { + "type": "git", + "url": "https://github.com/guzzle/promises.git", + "reference": "481557b130ef3790cf82b713667b43030dc9c957" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/promises/zipball/481557b130ef3790cf82b713667b43030dc9c957", + "reference": "481557b130ef3790cf82b713667b43030dc9c957", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "phpunit/phpunit": "^8.5.44 || ^9.6.25" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Promise\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + } + ], + "description": "Guzzle promises library", + "keywords": [ + "promise" + ], + "support": { + "issues": "https://github.com/guzzle/promises/issues", + "source": "https://github.com/guzzle/promises/tree/2.3.0" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/promises", + "type": "tidelift" + } + ], + "time": "2025-08-22T14:34:08+00:00" + }, + { + "name": "guzzlehttp/psr7", + "version": "2.9.0", + "source": { + "type": "git", + "url": "https://github.com/guzzle/psr7.git", + "reference": "7d0ed42f28e42d61352a7a79de682e5e67fec884" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/7d0ed42f28e42d61352a7a79de682e5e67fec884", + "reference": "7d0ed42f28e42d61352a7a79de682e5e67fec884", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0", + "psr/http-factory": "^1.0", + "psr/http-message": "^1.1 || ^2.0", + "ralouphie/getallheaders": "^3.0" + }, + "provide": { + "psr/http-factory-implementation": "1.0", + "psr/http-message-implementation": "1.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "http-interop/http-factory-tests": "0.9.0", + "jshttp/mime-db": "1.54.0.1", + "phpunit/phpunit": "^8.5.44 || ^9.6.25" + }, + "suggest": { + "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Psr7\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://github.com/sagikazarmark" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://sagikazarmark.hu" + } + ], + "description": "PSR-7 message implementation that also provides common utility methods", + "keywords": [ + "http", + "message", + "psr-7", + "request", + "response", + "stream", + "uri", + "url" + ], + "support": { + "issues": "https://github.com/guzzle/psr7/issues", + "source": "https://github.com/guzzle/psr7/tree/2.9.0" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/psr7", + "type": "tidelift" + } + ], + "time": "2026-03-10T16:41:02+00:00" + }, + { + "name": "mashape/unirest-php", + "version": "v3.0.4", + "source": { + "type": "git", + "url": "https://github.com/Mashape/unirest-php.git", + "reference": "842c0f242dfaaf85f16b72e217bf7f7c19ab12cb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Mashape/unirest-php/zipball/842c0f242dfaaf85f16b72e217bf7f7c19ab12cb", + "reference": "842c0f242dfaaf85f16b72e217bf7f7c19ab12cb", + "shasum": "" + }, + "require": { + "ext-curl": "*", + "php": ">=5.4.0" + }, + "require-dev": { + "codeclimate/php-test-reporter": "0.1.*", + "phpunit/phpunit": "~4.4" + }, + "suggest": { + "ext-json": "Allows using JSON Bodies for sending and parsing requests" + }, + "type": "library", + "autoload": { + "psr-0": { + "Unirest\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Unirest PHP", + "homepage": "https://github.com/Mashape/unirest-php", + "keywords": [ + "client", + "curl", + "http", + "https", + "rest" + ], + "support": { + "email": "opensource@mashape.com", + "issues": "https://github.com/Mashape/unirest-php/issues", + "source": "https://github.com/Mashape/unirest-php/tree/master" + }, + "time": "2016-08-11T17:49:21+00:00" + }, + { + "name": "nyholm/psr7", + "version": "1.8.2", + "source": { + "type": "git", + "url": "https://github.com/Nyholm/psr7.git", + "reference": "a71f2b11690f4b24d099d6b16690a90ae14fc6f3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Nyholm/psr7/zipball/a71f2b11690f4b24d099d6b16690a90ae14fc6f3", + "reference": "a71f2b11690f4b24d099d6b16690a90ae14fc6f3", + "shasum": "" + }, + "require": { + "php": ">=7.2", + "psr/http-factory": "^1.0", + "psr/http-message": "^1.1 || ^2.0" + }, + "provide": { + "php-http/message-factory-implementation": "1.0", + "psr/http-factory-implementation": "1.0", + "psr/http-message-implementation": "1.0" + }, + "require-dev": { + "http-interop/http-factory-tests": "^0.9", + "php-http/message-factory": "^1.0", + "php-http/psr7-integration-tests": "^1.0", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.4", + "symfony/error-handler": "^4.4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.8-dev" + } + }, + "autoload": { + "psr-4": { + "Nyholm\\Psr7\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com" + }, + { + "name": "Martijn van der Ven", + "email": "martijn@vanderven.se" + } + ], + "description": "A fast PHP7 implementation of PSR-7", + "homepage": "https://tnyholm.se", + "keywords": [ + "psr-17", + "psr-7" + ], + "support": { + "issues": "https://github.com/Nyholm/psr7/issues", + "source": "https://github.com/Nyholm/psr7/tree/1.8.2" + }, + "funding": [ + { + "url": "https://github.com/Zegnat", + "type": "github" + }, + { + "url": "https://github.com/nyholm", + "type": "github" + } + ], + "time": "2024-09-09T07:06:30+00:00" + }, + { + "name": "php-http/discovery", + "version": "1.20.0", + "source": { + "type": "git", + "url": "https://github.com/php-http/discovery.git", + "reference": "82fe4c73ef3363caed49ff8dd1539ba06044910d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-http/discovery/zipball/82fe4c73ef3363caed49ff8dd1539ba06044910d", + "reference": "82fe4c73ef3363caed49ff8dd1539ba06044910d", + "shasum": "" + }, + "require": { + "composer-plugin-api": "^1.0|^2.0", + "php": "^7.1 || ^8.0" + }, + "conflict": { + "nyholm/psr7": "<1.0", + "zendframework/zend-diactoros": "*" + }, + "provide": { + "php-http/async-client-implementation": "*", + "php-http/client-implementation": "*", + "psr/http-client-implementation": "*", + "psr/http-factory-implementation": "*", + "psr/http-message-implementation": "*" + }, + "require-dev": { + "composer/composer": "^1.0.2|^2.0", + "graham-campbell/phpspec-skip-example-extension": "^5.0", + "php-http/httplug": "^1.0 || ^2.0", + "php-http/message-factory": "^1.0", + "phpspec/phpspec": "^5.1 || ^6.1 || ^7.3", + "sebastian/comparator": "^3.0.5 || ^4.0.8", + "symfony/phpunit-bridge": "^6.4.4 || ^7.0.1" + }, + "type": "composer-plugin", + "extra": { + "class": "Http\\Discovery\\Composer\\Plugin", + "plugin-optional": true + }, + "autoload": { + "psr-4": { + "Http\\Discovery\\": "src/" + }, + "exclude-from-classmap": [ + "src/Composer/Plugin.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com" + } + ], + "description": "Finds and installs PSR-7, PSR-17, PSR-18 and HTTPlug implementations", + "homepage": "http://php-http.org", + "keywords": [ + "adapter", + "client", + "discovery", + "factory", + "http", + "message", + "psr17", + "psr7" + ], + "support": { + "issues": "https://github.com/php-http/discovery/issues", + "source": "https://github.com/php-http/discovery/tree/1.20.0" + }, + "time": "2024-10-02T11:20:13+00:00" + }, + { + "name": "php-http/multipart-stream-builder", + "version": "1.4.2", + "source": { + "type": "git", + "url": "https://github.com/php-http/multipart-stream-builder.git", + "reference": "10086e6de6f53489cca5ecc45b6f468604d3460e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-http/multipart-stream-builder/zipball/10086e6de6f53489cca5ecc45b6f468604d3460e", + "reference": "10086e6de6f53489cca5ecc45b6f468604d3460e", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0", + "php-http/discovery": "^1.15", + "psr/http-factory-implementation": "^1.0" + }, + "require-dev": { + "nyholm/psr7": "^1.0", + "php-http/message": "^1.5", + "php-http/message-factory": "^1.0.2", + "phpunit/phpunit": "^7.5.15 || ^8.5 || ^9.3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Http\\Message\\MultipartStream\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com" + } + ], + "description": "A builder class that help you create a multipart stream", + "homepage": "http://php-http.org", + "keywords": [ + "factory", + "http", + "message", + "multipart stream", + "stream" + ], + "support": { + "issues": "https://github.com/php-http/multipart-stream-builder/issues", + "source": "https://github.com/php-http/multipart-stream-builder/tree/1.4.2" + }, + "time": "2024-09-04T13:22:54+00:00" + }, + { + "name": "phpmailer/phpmailer", + "version": "v6.12.0", + "source": { + "type": "git", + "url": "https://github.com/PHPMailer/PHPMailer.git", + "reference": "d1ac35d784bf9f5e61b424901d5a014967f15b12" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHPMailer/PHPMailer/zipball/d1ac35d784bf9f5e61b424901d5a014967f15b12", + "reference": "d1ac35d784bf9f5e61b424901d5a014967f15b12", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "ext-filter": "*", + "ext-hash": "*", + "php": ">=5.5.0" + }, + "require-dev": { + "dealerdirect/phpcodesniffer-composer-installer": "^1.0", + "doctrine/annotations": "^1.2.6 || ^1.13.3", + "php-parallel-lint/php-console-highlighter": "^1.0.0", + "php-parallel-lint/php-parallel-lint": "^1.3.2", + "phpcompatibility/php-compatibility": "^9.3.5", + "roave/security-advisories": "dev-latest", + "squizlabs/php_codesniffer": "^3.7.2", + "yoast/phpunit-polyfills": "^1.0.4" + }, + "suggest": { + "decomplexity/SendOauth2": "Adapter for using XOAUTH2 authentication", + "ext-mbstring": "Needed to send email in multibyte encoding charset or decode encoded addresses", + "ext-openssl": "Needed for secure SMTP sending and DKIM signing", + "greew/oauth2-azure-provider": "Needed for Microsoft Azure XOAUTH2 authentication", + "hayageek/oauth2-yahoo": "Needed for Yahoo XOAUTH2 authentication", + "league/oauth2-google": "Needed for Google XOAUTH2 authentication", + "psr/log": "For optional PSR-3 debug logging", + "symfony/polyfill-mbstring": "To support UTF-8 if the Mbstring PHP extension is not enabled (^1.2)", + "thenetworg/oauth2-azure": "Needed for Microsoft XOAUTH2 authentication" + }, + "type": "library", + "autoload": { + "psr-4": { + "PHPMailer\\PHPMailer\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-2.1-only" + ], + "authors": [ + { + "name": "Marcus Bointon", + "email": "phpmailer@synchromedia.co.uk" + }, + { + "name": "Jim Jagielski", + "email": "jimjag@gmail.com" + }, + { + "name": "Andy Prevost", + "email": "codeworxtech@users.sourceforge.net" + }, + { + "name": "Brent R. Matzelle" + } + ], + "description": "PHPMailer is a full-featured email creation and transfer class for PHP", + "support": { + "issues": "https://github.com/PHPMailer/PHPMailer/issues", + "source": "https://github.com/PHPMailer/PHPMailer/tree/v6.12.0" + }, + "funding": [ + { + "url": "https://github.com/Synchro", + "type": "github" + } + ], + "time": "2025-10-15T16:49:08+00:00" + }, + { + "name": "psr/container", + "version": "2.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/container.git", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "shasum": "" + }, + "require": { + "php": ">=7.4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Container\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common Container Interface (PHP FIG PSR-11)", + "homepage": "https://github.com/php-fig/container", + "keywords": [ + "PSR-11", + "container", + "container-interface", + "container-interop", + "psr" + ], + "support": { + "issues": "https://github.com/php-fig/container/issues", + "source": "https://github.com/php-fig/container/tree/2.0.2" + }, + "time": "2021-11-05T16:47:00+00:00" + }, + { + "name": "psr/http-client", + "version": "1.0.3", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-client.git", + "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-client/zipball/bb5906edc1c324c9a05aa0873d40117941e5fa90", + "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90", + "shasum": "" + }, + "require": { + "php": "^7.0 || ^8.0", + "psr/http-message": "^1.0 || ^2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Client\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP clients", + "homepage": "https://github.com/php-fig/http-client", + "keywords": [ + "http", + "http-client", + "psr", + "psr-18" + ], + "support": { + "source": "https://github.com/php-fig/http-client" + }, + "time": "2023-09-23T14:17:50+00:00" + }, + { + "name": "psr/http-factory", + "version": "1.1.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-factory.git", + "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-factory/zipball/2b4765fddfe3b508ac62f829e852b1501d3f6e8a", + "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a", + "shasum": "" + }, + "require": { + "php": ">=7.1", + "psr/http-message": "^1.0 || ^2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "PSR-17: Common interfaces for PSR-7 HTTP message factories", + "keywords": [ + "factory", + "http", + "message", + "psr", + "psr-17", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-factory" + }, + "time": "2024-04-15T12:06:14+00:00" + }, + { + "name": "psr/http-message", + "version": "2.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-message.git", + "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-message/zipball/402d35bcb92c70c026d1a6a9883f06b2ead23d71", + "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP messages", + "homepage": "https://github.com/php-fig/http-message", + "keywords": [ + "http", + "http-message", + "psr", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-message/tree/2.0" + }, + "time": "2023-04-04T09:54:51+00:00" + }, + { + "name": "ralouphie/getallheaders", + "version": "3.0.3", + "source": { + "type": "git", + "url": "https://github.com/ralouphie/getallheaders.git", + "reference": "120b605dfeb996808c31b6477290a714d356e822" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/120b605dfeb996808c31b6477290a714d356e822", + "reference": "120b605dfeb996808c31b6477290a714d356e822", + "shasum": "" + }, + "require": { + "php": ">=5.6" + }, + "require-dev": { + "php-coveralls/php-coveralls": "^2.1", + "phpunit/phpunit": "^5 || ^6.5" + }, + "type": "library", + "autoload": { + "files": [ + "src/getallheaders.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ralph Khattar", + "email": "ralph.khattar@gmail.com" + } + ], + "description": "A polyfill for getallheaders.", + "support": { + "issues": "https://github.com/ralouphie/getallheaders/issues", + "source": "https://github.com/ralouphie/getallheaders/tree/develop" + }, + "time": "2019-03-08T08:55:37+00:00" + }, + { + "name": "symfony/console", + "version": "v5.4.47", + "source": { + "type": "git", + "url": "https://github.com/symfony/console.git", + "reference": "c4ba980ca61a9eb18ee6bcc73f28e475852bb1ed" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/console/zipball/c4ba980ca61a9eb18ee6bcc73f28e475852bb1ed", + "reference": "c4ba980ca61a9eb18ee6bcc73f28e475852bb1ed", + "shasum": "" + }, + "require": { + "php": ">=7.2.5", + "symfony/deprecation-contracts": "^2.1|^3", + "symfony/polyfill-mbstring": "~1.0", + "symfony/polyfill-php73": "^1.9", + "symfony/polyfill-php80": "^1.16", + "symfony/service-contracts": "^1.1|^2|^3", + "symfony/string": "^5.1|^6.0" + }, + "conflict": { + "psr/log": ">=3", + "symfony/dependency-injection": "<4.4", + "symfony/dotenv": "<5.1", + "symfony/event-dispatcher": "<4.4", + "symfony/lock": "<4.4", + "symfony/process": "<4.4" + }, + "provide": { + "psr/log-implementation": "1.0|2.0" + }, + "require-dev": { + "psr/log": "^1|^2", + "symfony/config": "^4.4|^5.0|^6.0", + "symfony/dependency-injection": "^4.4|^5.0|^6.0", + "symfony/event-dispatcher": "^4.4|^5.0|^6.0", + "symfony/lock": "^4.4|^5.0|^6.0", + "symfony/process": "^4.4|^5.0|^6.0", + "symfony/var-dumper": "^4.4|^5.0|^6.0" + }, + "suggest": { + "psr/log": "For using the console logger", + "symfony/event-dispatcher": "", + "symfony/lock": "", + "symfony/process": "" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Console\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Eases the creation of beautiful and testable command line interfaces", + "homepage": "https://symfony.com", + "keywords": [ + "cli", + "command-line", + "console", + "terminal" + ], + "support": { + "source": "https://github.com/symfony/console/tree/v5.4.47" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-11-06T11:30:55+00:00" + }, + { + "name": "symfony/deprecation-contracts", + "version": "v3.6.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/deprecation-contracts.git", + "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/63afe740e99a13ba87ec199bb07bbdee937a5b62", + "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.6-dev" + } + }, + "autoload": { + "files": [ + "function.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "A generic function and convention to trigger deprecation notices", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.6.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-25T14:21:43+00:00" + }, + { + "name": "symfony/polyfill-ctype", + "version": "v1.37.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-ctype.git", + "reference": "141046a8f9477948ff284fa65be2095baafb94f2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/141046a8f9477948ff284fa65be2095baafb94f2", + "reference": "141046a8f9477948ff284fa65be2095baafb94f2", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "provide": { + "ext-ctype": "*" + }, + "suggest": { + "ext-ctype": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Ctype\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Gert de Pagter", + "email": "BackEndTea@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for ctype functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "ctype", + "polyfill", + "portable" + ], + "support": { + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.37.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-04-10T16:19:22+00:00" + }, + { + "name": "symfony/polyfill-intl-grapheme", + "version": "v1.37.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-grapheme.git", + "reference": "4864388bfbd3001ce88e234fab652acd91fdc57e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/4864388bfbd3001ce88e234fab652acd91fdc57e", + "reference": "4864388bfbd3001ce88e234fab652acd91fdc57e", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Grapheme\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's grapheme_* functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "grapheme", + "intl", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.37.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-04-26T13:13:48+00:00" + }, + { + "name": "symfony/polyfill-intl-normalizer", + "version": "v1.37.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-normalizer.git", + "reference": "3833d7255cc303546435cb650316bff708a1c75c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/3833d7255cc303546435cb650316bff708a1c75c", + "reference": "3833d7255cc303546435cb650316bff708a1c75c", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Normalizer\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's Normalizer class and related functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "intl", + "normalizer", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.37.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, + { + "name": "symfony/polyfill-mbstring", + "version": "v1.37.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "6a21eb99c6973357967f6ce3708cd55a6bec6315" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/6a21eb99c6973357967f6ce3708cd55a6bec6315", + "reference": "6a21eb99c6973357967f6ce3708cd55a6bec6315", + "shasum": "" + }, + "require": { + "ext-iconv": "*", + "php": ">=7.2" + }, + "provide": { + "ext-mbstring": "*" + }, + "suggest": { + "ext-mbstring": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Mbstring\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for the Mbstring extension", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "mbstring", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.37.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-04-10T17:25:58+00:00" + }, + { + "name": "symfony/polyfill-php73", + "version": "v1.37.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php73.git", + "reference": "0f68c03565dcaaf25a890667542e8bd75fe7e5bb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/0f68c03565dcaaf25a890667542e8bd75fe7e5bb", + "reference": "0f68c03565dcaaf25a890667542e8bd75fe7e5bb", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php73\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 7.3+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php73/tree/v1.37.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, + { + "name": "symfony/polyfill-php80", + "version": "v1.37.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php80.git", + "reference": "dfb55726c3a76ea3b6459fcfda1ec2d80a682411" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/dfb55726c3a76ea3b6459fcfda1ec2d80a682411", + "reference": "dfb55726c3a76ea3b6459fcfda1ec2d80a682411", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php80\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ion Bazan", + "email": "ion.bazan@gmail.com" + }, + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php80/tree/v1.37.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-04-10T16:19:22+00:00" + }, + { + "name": "symfony/service-contracts", + "version": "v3.6.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/service-contracts.git", + "reference": "45112560a3ba2d715666a509a0bc9521d10b6c43" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/45112560a3ba2d715666a509a0bc9521d10b6c43", + "reference": "45112560a3ba2d715666a509a0bc9521d10b6c43", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/container": "^1.1|^2.0", + "symfony/deprecation-contracts": "^2.5|^3" + }, + "conflict": { + "ext-psr": "<1.1|>=2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.6-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Service\\": "" + }, + "exclude-from-classmap": [ + "/Test/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to writing services", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/service-contracts/tree/v3.6.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-07-15T11:30:57+00:00" + }, + { + "name": "symfony/string", + "version": "v6.4.34", + "source": { + "type": "git", + "url": "https://github.com/symfony/string.git", + "reference": "2adaf4106f2ef4c67271971bde6d3fe0a6936432" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/string/zipball/2adaf4106f2ef4c67271971bde6d3fe0a6936432", + "reference": "2adaf4106f2ef4c67271971bde6d3fe0a6936432", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-intl-grapheme": "~1.0", + "symfony/polyfill-intl-normalizer": "~1.0", + "symfony/polyfill-mbstring": "~1.0" + }, + "conflict": { + "symfony/translation-contracts": "<2.5" + }, + "require-dev": { + "symfony/http-client": "^5.4|^6.0|^7.0", + "symfony/intl": "^6.2|^7.0", + "symfony/translation-contracts": "^2.5|^3.0", + "symfony/var-exporter": "^5.4|^6.0|^7.0" + }, + "type": "library", + "autoload": { + "files": [ + "Resources/functions.php" + ], + "psr-4": { + "Symfony\\Component\\String\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides an object-oriented API to strings and deals with bytes, UTF-8 code points and grapheme clusters in a unified way", + "homepage": "https://symfony.com", + "keywords": [ + "grapheme", + "i18n", + "string", + "unicode", + "utf-8", + "utf8" + ], + "support": { + "source": "https://github.com/symfony/string/tree/v6.4.34" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-02-08T20:44:54+00:00" + } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": {}, + "prefer-stable": false, + "prefer-lowest": false, + "platform": {}, + "platform-dev": {}, + "plugin-api-version": "2.9.0" +} diff --git a/api/console b/api/console new file mode 100644 index 0000000..ee7f910 --- /dev/null +++ b/api/console @@ -0,0 +1,17 @@ +#!/usr/bin/env php +add(new GreetCommand()); +//$application->add(new Sentinel()); +//$application->add(new Engine()); +$application->run(); \ No newline at end of file diff --git a/api/core/AltoRouter.php b/api/core/AltoRouter.php new file mode 100644 index 0000000..7f7c944 --- /dev/null +++ b/api/core/AltoRouter.php @@ -0,0 +1,253 @@ + '[0-9]++', + 'a' => '[0-9A-Za-z]++', + 'h' => '[0-9A-Fa-f]++', + '*' => '.+?', + '**' => '.++', + '' => '[^/\.]++' + ); + + /** + * Create router in one call from config. + * + * @param array $routes + * @param string $basePath + * @param array $matchTypes + */ + public function __construct($routes = array(), $basePath = '', $matchTypes = array()) { + $this->setBasePath($basePath); + $this->addMatchTypes($matchTypes); + + foreach ($routes as $route) { + call_user_func_array(array($this, 'map'), $route); + } + } + + /** + * Set the base path. + * Useful if you are running your application from a subdirectory. + */ + public function setBasePath($basePath) { + $this->basePath = $basePath; + } + + /** + * Add named match types. It uses array_merge so keys can be overwritten. + * + * @param array $matchTypes The key is the name and the value is the regex. + */ + public function addMatchTypes($matchTypes) { + $this->matchTypes = array_merge($this->matchTypes, $matchTypes); + } + + /** + * Map a route to a target + * + * @param string $method One of 4 HTTP Methods, or a pipe-separated list of multiple HTTP Methods (GET|POST|PUT|DELETE) + * @param string $route The route regex, custom regex must start with an @. You can use multiple pre-set regex filters, like [i:id] + * @param mixed $target The target where this route should point to. Can be anything. + * @param string $name Optional name of this route. Supply if you want to reverse route this url in your application. + * + */ + public function map($method, $route, $target, $name = null) { + + $this->routes[] = array($method, $route, $target, $name); + + if ($name) { + if (isset($this->namedRoutes[$name])) { + throw new \Exception("Can not redeclare route '{$name}'"); + } else { + $this->namedRoutes[$name] = $route; + } + } + + return; + } + + /** + * Reversed routing + * + * Generate the URL for a named route. Replace regexes with supplied parameters + * + * @param string $routeName The name of the route. + * @param array @params Associative array of parameters to replace placeholders with. + * @return string The URL of the route with named parameters in place. + */ + public function generate($routeName, array $params = array()) { + + // Check if named route exists + if (!isset($this->namedRoutes[$routeName])) { + throw new \Exception("Route '{$routeName}' does not exist."); + } + + // Replace named parameters + $route = $this->namedRoutes[$routeName]; + + // prepend base path to route url again + $url = $this->basePath . $route; + + if (preg_match_all('`(/|\.|)\[([^:\]]*+)(?::([^:\]]*+))?\](\?|)`', $route, $matches, PREG_SET_ORDER)) { + + foreach ($matches as $match) { + list($block, $pre, $type, $param, $optional) = $match; + + if ($pre) { + $block = substr($block, 1); + } + + if (isset($params[$param])) { + $url = str_replace($block, $params[$param], $url); + } elseif ($optional) { + $url = str_replace($pre . $block, '', $url); + } + } + } + + return $url; + } + + /** + * Match a given Request Url against stored routes + * @param string $requestUrl + * @param string $requestMethod + * @return array|boolean Array with route information on success, false on failure (no match). + */ + public function match($requestUrl = null, $requestMethod = null) { + + $params = array(); + $match = false; + + // set Request Url if it isn't passed as parameter + if ($requestUrl === null) { + $requestUrl = isset($_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : '/'; + } + + // strip base path from request url + $requestUrl = substr($requestUrl, strlen($this->basePath)); + + // Strip query string (?a=b) from Request Url + if (($strpos = strpos($requestUrl, '?')) !== false) { + $requestUrl = substr($requestUrl, 0, $strpos); + } + + // set Request Method if it isn't passed as a parameter + if ($requestMethod === null) { + $requestMethod = isset($_SERVER['REQUEST_METHOD']) ? $_SERVER['REQUEST_METHOD'] : 'GET'; + } + + // Force request_order to be GP + // http://www.mail-archive.com/internals@lists.php.net/msg33119.html + $_REQUEST = array_merge($_GET, $_POST); + + foreach ($this->routes as $handler) { + list($method, $_route, $target, $name) = $handler; + + $methods = explode('|', $method); + $method_match = false; + + // Check if request method matches. If not, abandon early. (CHEAP) + foreach ($methods as $method) { + if (strcasecmp($requestMethod, $method) === 0) { + $method_match = true; + break; + } + } + + // Method did not match, continue to next route. + if (!$method_match) + continue; + + // Check for a wildcard (matches all) + if ($_route === '*') { + $match = true; + } elseif (isset($_route[0]) && $_route[0] === '@') { + $match = preg_match('`' . substr($_route, 1) . '`', $requestUrl, $params); + } else { + $route = null; + $regex = false; + $j = 0; + $n = isset($_route[0]) ? $_route[0] : null; + $i = 0; + + // Find the longest non-regex substring and match it against the URI + while (true) { + if (!isset($_route[$i])) { + break; + } elseif (false === $regex) { + $c = $n; + $regex = $c === '[' || $c === '(' || $c === '.'; + if (false === $regex && false !== isset($_route[$i + 1])) { + $n = $_route[$i + 1]; + $regex = $n === '?' || $n === '+' || $n === '*' || $n === '{'; + } + if (false === $regex && $c !== '/' && (!isset($requestUrl[$j]) || $c !== $requestUrl[$j])) { + continue 2; + } + $j++; + } + $route .= $_route[$i++]; + } + + $regex = $this->compileRoute($route); + $match = preg_match($regex, $requestUrl, $params); + } + + if (($match == true || $match > 0)) { + + if ($params) { + foreach ($params as $key => $value) { + if (is_numeric($key)) + unset($params[$key]); + } + } + + return array( + 'target' => $target, + 'params' => $params, + 'name' => $name + ); + } + } + return false; + } + + /** + * Compile the regex for a given route (EXPENSIVE) + */ + private function compileRoute($route) { + if (preg_match_all('`(/|\.|)\[([^:\]]*+)(?::([^:\]]*+))?\](\?|)`', $route, $matches, PREG_SET_ORDER)) { + + $matchTypes = $this->matchTypes; + foreach ($matches as $match) { + list($block, $pre, $type, $param, $optional) = $match; + + if (isset($matchTypes[$type])) { + $type = $matchTypes[$type]; + } + if ($pre === '.') { + $pre = '\.'; + } + + //Older versions of PCRE require the 'P' in (?P) + $pattern = '(?:' + . ($pre !== '' ? $pre : null) + . '(' + . ($param !== '' ? "?P<$param>" : null) + . $type + . '))' + . ($optional !== '' ? '?' : null); + + $route = str_replace($block, $pattern, $route); + } + } + return "`^$route$`"; + } + +} diff --git a/api/core/Bootstrap.php b/api/core/Bootstrap.php new file mode 100644 index 0000000..221d480 --- /dev/null +++ b/api/core/Bootstrap.php @@ -0,0 +1,248 @@ +_getUrl(); + + + + // Load the default controller if no URL is set + // eg: Visit http://localhost it loads Default Controller + if (empty($this->_url[0])) { + $this->_loadDefaultController(); + return false; + } + + //Router + $this->match = \Router::Routing(); + + + + if(DEBUG == true) { + register_shutdown_function(function () { + $err = error_get_last(); + if (! is_null($err)) { + print 'Error#'.$err['message'].'
'; + print 'Line#'.$err['line'].'
'; + print 'File#'.$err['file'].'
'; + } + }); + } + + + // This check whether there is a match + if (empty($this->match)) { + $this->_loadExistingController(); + $this->_callControllerMethod(); + } else { + $this->_loadRouter(); + } + + //$this->_loadExistingController(); + //$this->_callControllerMethod(); + } + + /** + * (Optional) Set a custom path to controllers + * @param string $path + */ + public function setControllerPath($path ='') { + \Helper::print_array($path); + $this->_controllerPath = trim($path, '') . ''; + } + + /** + * (Optional) Set a custom path to models + * @param string $path + */ + public function setModelPath($path ='') { + $this->_modelPath = trim($path, '') . ''; + } + + /** + * (Optional) Set a custom path to the error file + * @param string $path Use the file name of your controller, eg: error.php + */ + public function setErrorFile($path ='') { + $this->_errorFile = trim($path, '/'); + } + + /** + * (Optional) Set a custom path to the error file + * @param string $path Use the file name of your controller, eg: index.php + */ + public function setDefaultFile($path = '') { + $this->_defaultFile = trim($path, '/'); + } + + + /** + * (Optional) Set a custom path to the error file + * @param string $path Use the file name of your controller, eg: index.php + */ + public function setDefaultPath($path = '') { + $this->_defaultPath = trim($path, ''); // Removed the trim for / + } + + + + /** + * Fetches the $_GET from 'url' + */ + private function _getUrl() { + $url = isset($_GET['url']) ? $_GET['url'] : null; + $url = rtrim($url, '/'); + $url = filter_var($url, FILTER_SANITIZE_URL); + $this->_url = explode('/', $url); + } + + /** + * This loads if there is no GET parameter passed + */ + private function _loadDefaultController() { + require $this->_defaultPath . '/' . $this->_controllerPath . $this->_defaultFile; + $this->_controller = new Index(); + $this->_controller->index(); + } + + /** + * Load an existing controller if there IS a GET parameter passed + * + * @return boolean|string + */ + private function _loadExistingController() { + $file = $this->_defaultPath . '/' . $this->_controllerPath . $this->_url[0] . '.php'; + + + if (file_exists($file)) { + require $file; + if($this->_url[1]) { $method = $this->_url[1]; } else { $method = "index"; } + $this->_controller = new $this->_url[0]($method); + $this->_controller->loadModel($this->_url[0], $this->_modelPath); + } else { + $this->_error(); + return false; + } + } + + /** + * Loads Router if there's a rule set for specific URL combinate + * @return boolean + */ + private function _loadRouter() { + /// Run the Router + + + $this->ControllerName = $this->match['target']['c']; + $this->MethodName = $this->match['target']['a']; + $this->URIParameters = $this->match['params']; + + + $file = $this->_defaultPath . '/' . $this->_controllerPath . $this->ControllerName . '.php'; + + if (file_exists($file)) { + require $file; + + $this->_controller = new $this->ControllerName($this->MethodName); + $this->_controller->loadModel($this->ControllerName, $this->_modelPath); + } else { + $this->_error(); + return false; + } + + // Load Controller + $this->_controller->{$this->MethodName}($this->URIParameters); + } + + /** + * If a method is passed in the GET url parameter + * + * http://localhost/controller/method/(param)/(param)/(param) + * url[0] = Controller + * url[1] = Method (falls back to 'index' if not a valid method) + * url[2] = Param + * url[3] = Param + * url[4] = Param + * + * Fallback: /controller/param → controller->index(param) + */ + private function _callControllerMethod() { + $length = count($this->_url); + + // If url[1] is not a valid method, treat it as a param to index() + if ($length > 1) { + if (!method_exists($this->_controller, $this->_url[1])) { + array_splice($this->_url, 1, 0, ['index']); + $length = count($this->_url); + } + } + + // Determine what to load + switch ($length) { + case 5: + //Controller->Method(Param1, Param2, Param3) + $this->_controller->{$this->_url[1]}($this->_url[2], $this->_url[3], $this->_url[4]); + break; + + case 4: + //Controller->Method(Param1, Param2) + $this->_controller->{$this->_url[1]}($this->_url[2], $this->_url[3]); + break; + + case 3: + //Controller->Method(Param1, Param2) + $this->_controller->{$this->_url[1]}($this->_url[2]); + break; + + case 2: + //Controller->Method(Param1, Param2) + $this->_controller->{$this->_url[1]}(); + break; + + default: + $this->_controller->index(); + break; + } + } + + /** + * Display an error page if nothing exists + * + * @return boolean + */ + private function _error() { + require dirname(__DIR__) . '/' . $this->_defaultPath . '/' . $this->_controllerPath . $this->_errorFile; + $this->_controller = new _Error(); + $this->_controller->index(); + exit; + } + +} \ No newline at end of file diff --git a/api/core/Controller.php b/api/core/Controller.php new file mode 100644 index 0000000..31affca --- /dev/null +++ b/api/core/Controller.php @@ -0,0 +1,29 @@ +view = new View(); + $this->view->_c = $this; + } + + /** + * + * @param string $name Name of the model + * @param string $path Location of the models + */ + public function loadModel($name, $modelPath = 'models/') { + + $path = $modelPath . $name.'_model.php'; + + if (file_exists($path)) { + require $modelPath .$name.'_model.php'; + + $modelName = $name . '_Model'; + $this->model = new $modelName(); + $this->view->_m = $this->model; // extends [model] access to views + + } + } + +} \ No newline at end of file diff --git a/api/core/Database.php b/api/core/Database.php new file mode 100644 index 0000000..0a5ceff --- /dev/null +++ b/api/core/Database.php @@ -0,0 +1,321 @@ +prepare($sql); + foreach ($array as $key => $value) { + $sth->bindValue("$key", $value); + } + + $sth->execute(); + return $sth->fetchAll($fetchMode); + + } catch (PDOException $e) { + $Response['Code']='0'; + $Response['Message']= 'Error Select Entry: ' . $e->getMessage(); + + + echo json_encode($Response); + return false; + } + + + } + + /** + * insert + * @param string $table A name of table to insert into + * @param string $data An associative array + */ + public function insert($table, $data) + { + ksort($data); + + $fieldNames = implode('`, `', array_keys($data)); + $fieldValues = ':' . implode(', :', array_keys($data)); + + + try { + $sth = $this->prepare("INSERT INTO $table (`$fieldNames`) VALUES ($fieldValues)"); + + + foreach ($data as $key => $value) { + $sth->bindValue(":$key", $value); + } + + $sth->execute(); + $msg = $sth->errorInfo(); + + + if(!$msg[1]) { $msgCode = 1; } else { $msgCode = $msg[1]; } + if(!$msg[2]) { $msgMessage = 'Created'; } else { $msgMessage = $msg[2]; } + + $Response['Code']=$msgCode; + $Response['Message']=$msgMessage; + $Response['ID']= $this->lastInsertId(); + + } catch (PDOException $e) { + $Response['Code']='0'; + $Response['Message']= 'Error Creating Entry: ' . $e->getMessage(); + + + } + + return json_encode($Response); + + + + } + + /** + * update + * @param string $table A name of table to insert into + * @param string $data An associative array + * @param string $where the WHERE query part + */ + public function update($table, $data, $where) + { + ksort($data); + + $fieldDetails = NULL; + foreach($data as $key=> $value) { + $fieldDetails .= "`$key`=:$key,"; + } + $fieldDetails = rtrim($fieldDetails, ','); + + try { + + $sth = $this->prepare("UPDATE $table SET $fieldDetails WHERE $where"); + + foreach ($data as $key => $value) { + $sth->bindValue(":$key", $value); + } + + + $sth->execute(); + $count = $sth->rowCount(); + if($count){ + $Response['Code']='1'; + $Response['Rows']= $count; + $Response['Message']='Updated'; + } else { + $Response['Code']='0'; + $Response['Rows']= $count; + $Response['Message']='No Records Updated'; + } + + + } catch (PDOException $e) { + $Response['Code']='0'; + $Response['Message']= 'Error Updating Database: ' . $e->getMessage(); + + + } + + return json_encode($Response); + } + + + /** + * delete + * + * @param string $table + * @param string $where + * @param integer $limit + * @return integer Affected Rows + */ + public function delete($table, $where, $limit = 1) + { + $sth = $this->prepare("DELETE FROM $table WHERE $where LIMIT $limit"); + return $sth->execute(); + } + + + /** + * Query + * + * @param string $sql + * @return returns array results + */ + public function execQuery($sql, $limit='') { +// echo $sql; +// print_array($limit); + if($limit) { + $LimitStart = $limit['start']; + $LimitEnd = $limit['end']; + } + + $sth = $this->prepare($sql); + if($LimitStart) $sth->bindParam(1, $LimitStart,PDO::PARAM_INT); + if($LimitEnd) $sth->bindParam(2, $LimitEnd,PDO::PARAM_INT); + + $sth->execute(); + return $sth->fetchAll(PDO::FETCH_ASSOC); + + } + + + /** + * Query + * + * @param string $table + * @param array $arrQuery + * @return returns array results + * $ + */ + public function wherein($table, $column, $arrQuery, $CustomSQL='') { + $SQLWhereIn = implode(',', $arrQuery); + if($CustomSQL) { + $CustomSQL = " AND " . $CustomSQL; + } + $sql = "SELECT SQL_CALC_FOUND_ROWS * FROM {$table} WHERE {$column} IN ({$SQLWhereIn}) $CustomSQL"; + // print_array($sql); + $sth = $this->prepare($sql); + $sth->execute(); + + $count = $this->prepare('SELECT FOUND_ROWS() as Rows'); + $count->execute(); + $Counted = $count->fetchAll(PDO::FETCH_ASSOC); + $Counted = $Counted[0]['Rows']; + + $Result['results'] = $sth->fetchAll(PDO::FETCH_ASSOC); + $Result['rowsfound'] = $Counted; + return $Result; + + } + + + + + + /** + * select + * @param string $sql An SQL string + * @param array $array Paramters to bind + * @param constant $fetchMode A PDO Fetch mode + * @return mixed + + public function retrieve($table, $sql, $fetchMode = PDO::FETCH_ASSOC) + { + + $sql = "SELECT * FROM business_profile WHERE 1 LIMIT 0, 100"; + print_array($sql); + $sth = $this->prepare($sql); + $sth->execute(); + + $statement = $this->query('SELECT FOUND_ROWS() AS CountedRows'); + print_array($statement); + $Counted = $Counted[0]['CountedRows']; + + $Result['results'] = $sth->fetchAll(PDO::FETCH_ASSOC); + $Result['rowsfound'] = $Counted; + return $Result; + // return $sth->fetchAll($fetchMode); + + } + */ + + + + + + /** + * select + * @param string $sql An SQL string + * @param array $array Paramters to bind + * @param constant $fetchMode A PDO Fetch mode + * @return mixed + */ + public function retrieve($sql, $array = array(), $pagination = '') + { + + try { + $sth = $this->prepare($sql); + foreach ($array as $key => $value) { + $sth->bindValue("$key", $value); + } + + $sth->execute(); + + $statement = $this->execQuery('SELECT FOUND_ROWS() AS CountedRows'); + $Counted = $statement[0]['CountedRows']; + + $Result['results'] = $sth->fetchAll(PDO::FETCH_ASSOC); + $Result['rowsfound'] = $Counted; + + return $Result; +// return $sth->fetchAll($fetchMode); + + } catch (PDOException $e) { + $Result['Code']='0'; + $Result['Message']= 'Error Select Entry: ' . $e->getMessage(); + + return json_encode($Result); + } + } + + + public function pagination($sql, $page='0', $limit='10') + { + try { + $find = array("select * from", "LIMIT ?,?"); + $NewSQL = str_ireplace($find, '', $sql); + $totalSQL = "SELECT count(*) as Counted FROM {$NewSQL}"; + $statement = $this->execQuery($totalSQL); + $Counted = $statement[0]['Counted']; + + if(!$page) { $page = '0'; } + if(!$limit) { $limit = 10; } + $TotalPages = floor($Counted / $limit); + if($page) { $page_first_result = ($page) * $limit; } else { $page_first_result = '0'; } + + + $sth = $this->prepare($sql); + $sth->execute([$page_first_result, $limit]); + // echo "{$page_first_result}, $limit"; + + + + $Result['results'] = $sth->fetchAll(PDO::FETCH_ASSOC); + $Result['total'] = $Counted; + $Result['limit'] = $limit; + $Result['page'] = $page; + $Result['pages'] = $TotalPages; + + return $Result; + + } catch (PDOException $e) { + $Result['Code']='0'; + $Result['Message']= 'Error Select Entry: ' . $e->getMessage(); + + return json_encode($Result); + } + } + + + +} \ No newline at end of file diff --git a/api/core/ErrorHandler.php b/api/core/ErrorHandler.php new file mode 100644 index 0000000..c155bd4 --- /dev/null +++ b/api/core/ErrorHandler.php @@ -0,0 +1,76 @@ +getMessage(), $exception->getFile(), $exception->getLine()); + self::displayError(E_ERROR, $exception->getMessage(), $exception->getFile(), $exception->getLine(), $exception->getTraceAsString()); + } + + public static function handleShutdown() { + $error = error_get_last(); + if ($error !== null && in_array($error['type'], [E_ERROR, E_CORE_ERROR, E_COMPILE_ERROR, E_PARSE])) { + self::logError($error['type'], $error['message'], $error['file'], $error['line']); + self::displayError($error['type'], $error['message'], $error['file'], $error['line']); + } + } + + private static function logError($errno, $errstr, $errfile, $errline) { + $message = date('[Y-m-d H:i:s]') . " Error: [$errno] $errstr in $errfile on line $errline\n"; + error_log($message, 3, 'app_errors.log'); + } + + private static function displayError($errno, $errstr, $errfile, $errline, $trace = null) { + $errorTypes = [ + E_ERROR => 'Fatal Error', + E_WARNING => 'Warning', + E_PARSE => 'Parse Error', + E_NOTICE => 'Notice', + E_CORE_ERROR => 'Core Error', + E_CORE_WARNING => 'Core Warning', + E_COMPILE_ERROR => 'Compile Error', + E_COMPILE_WARNING => 'Compile Warning', + E_USER_ERROR => 'User Error', + E_USER_WARNING => 'User Warning', + E_USER_NOTICE => 'User Notice', + E_STRICT => 'Strict Standards', + E_RECOVERABLE_ERROR => 'Recoverable Error', + E_DEPRECATED => 'Deprecated', + E_USER_DEPRECATED => 'User Deprecated', + ]; + + $errorType = isset($errorTypes[$errno]) ? $errorTypes[$errno] : 'Unknown Error'; + + if (DEBUG) { + echo "
"; + echo "

$errorType Occurred

"; + echo "

Message: $errstr

"; + echo "

File: $errfile

"; + echo "

Line: $errline

"; + if ($trace) { + echo "

Stack Trace:

"; + echo "
$trace
"; + } + echo "

Request Details:

"; + echo "
";
+            echo "URL: " . $_SERVER['REQUEST_URI'] . "\n";
+            echo "Method: " . $_SERVER['REQUEST_METHOD'] . "\n";
+            echo "Time: " . date('Y-m-d H:i:s') . "\n";
+            echo "IP: " . $_SERVER['REMOTE_ADDR'] . "\n";
+            echo "
"; + echo "
"; + } + } +} diff --git a/api/core/Model.php b/api/core/Model.php new file mode 100644 index 0000000..da3fe2c --- /dev/null +++ b/api/core/Model.php @@ -0,0 +1,14 @@ +db = new Database(DB_TYPE, DB_HOST, DB_NAME, DB_USER, DB_PASS); + $this->db->setAttribute( PDO::ATTR_EMULATE_PREPARES, false ); + + + //$this->remotedb = new Database(DB_TYPE_REMOTE, DB_HOST_REMOTE, DB_NAME_REMOTE, DB_USER_REMOTE, DB_PASS_REMOTE); + //$this->remotedb->setAttribute( PDO::ATTR_EMULATE_PREPARES, false ); + + } +} \ No newline at end of file diff --git a/api/core/Session.php b/api/core/Session.php new file mode 100644 index 0000000..5bd92c8 --- /dev/null +++ b/api/core/Session.php @@ -0,0 +1,65 @@ +path = VIEWS_PATH . '/views/'; + } + + public function render($name, $type='site', $noInclude = true) { + if(empty($type)) { $type = 'site'; } + $name = strtolower($name); + extract((array) $this); + + if ($noInclude == false) { + require $this->path . "" . $name . ".php"; + } else { + include $this->path . "wrapper/{$type}/header.php"; + require $this->path . $name . ".php"; + include $this->path . "wrapper/{$type}/footer.php"; + } + } + + + /** + * generates partial view. Good for straight JSON or XML responses for internal views. + * @param $name + * @return string + */ + function PartialView($name, $param=false) + { + extract((array) $this); + ob_start(); + include ($this->path . "partial/" . $name . ".php"); + $name = ob_get_clean(); + + return ($name); + ob_end_flush(); + } + + function js($data){ + print_array($data); + } + + /* + public function render($name) { + require 'views/' . $name . '.php'; + } + */ + +} \ No newline at end of file diff --git a/api/core/core.md b/api/core/core.md new file mode 100644 index 0000000..988e594 --- /dev/null +++ b/api/core/core.md @@ -0,0 +1,128 @@ +# Core Framework — `core/` + +Framework internals. **Rarely modified.** Contains the request lifecycle engine. + +## Files + +| File | Purpose | +|------|---------| +| `Bootstrap.php` | Bootstraps environment, loads config, starts routing | +| `Controller.php` | Base controller class — all UI controllers extend this (via `AppController`) | +| `View.php` | View renderer — handles `render()`, `PartialView()`, asset injection | +| `AltoRouter.php` | URL router used for complex custom routes | + +--- + +## Request Lifecycle + +``` +Request + → public/index.php + → config.php (loads .env, defines constants) + → core/Bootstrap.php (environment setup, session, autoload) + → AltoRouter / auto-routing + → ControllerClass::method($param) + → $this->view->render(...) + → Response (HTML or JSON) +``` + +Auto-routing maps `/controller/method/param` directly to `ControllerClass::method($param)`. + +--- + +## `Controller` (`core/Controller.php`) + +Base class. Provides: + +- `$this->view` — View instance +- `$this->JavaScript[]` — array of JS paths to inject +- `$this->Styles[]` — array of CSS paths to inject +- `redirect($url)` — HTTP redirect helper + +**Do not modify this file.** App-wide customisation belongs in `app/Controllers/AppController.php`. + +--- + +## `AppController` (`app/Controllers/AppController.php`) + +Sits between `core/Controller` and all UI page controllers. +UI controllers extend `AppController`, not `Controller` directly. + +**Current responsibilities:** +- Injects `$UserProfile` (logged-in user's full profile via `User::profile()`) into every view automatically. + +**`use` statement scoping rule:** +- Namespaces needed on every page → `AppController` +- Namespaces needed by one controller → that controller only +- Namespaces needed by one method → inline as fully-qualified class name + +```php +// app/Controllers/AppController.php +use App\Components\User; + +class AppController extends Controller { + function __construct() { + parent::__construct(); + if (!empty($_SESSION['login']['userid'])) { + $this->view->UserProfile = User::profile($_SESSION['login']['userid']); + } + } +} +``` + +--- + +## `View` (`core/View.php`) + +**Properties:** +- Any property set on `$this->view` becomes a variable in the rendered template. + +**Methods:** +- `render($viewPath, $wrapper = '', $useWelcome = true)` — render a view +- `PartialView($partialName)` — load and return a partial as a string + +**Render signatures used in practice:** +```php +$this->view->render(__CLASS__ . '/' . __FUNCTION__); // default admin wrapper +$this->view->render(__CLASS__ . '/' . __FUNCTION__, 'login'); // login wrapper +$this->view->render('clients/index', true, 'site'); // legacy style +``` + +--- + +## Routing + +### Auto-Routing (default) + +| URL | Maps to | +|-----|---------| +| `/clients` | `Clients::index()` | +| `/clients/show/123` | `Clients::show(123)` | +| `/products/edit/456` | `Products::edit(456)` | + +### AltoRouter (complex routes) + +Define in `public/routes.php` or `api/routes.php`: + +```php +// Named route with typed parameter +$router->map('GET', '/users/[i:id]', 'Users#show', 'user_show'); + +// Multiple parameters +$router->map('GET', '/blog/[i:year]/[i:month]/[*:slug]', 'Blog#show', 'blog_post'); + +// POST +$router->map('POST', '/api/users/create', 'UsersAPI#create', 'api_user_create'); +``` + +**Match type tokens:** +- `[i:id]` — integer +- `[a:action]` — alphanumeric (A-Z, a-z, 0-9, -) +- `[h:key]` — hex +- `[*:trailing]` — catch-all (no slashes) +- `[**:path]` — catch-all including slashes + +```php +// Generate URL from named route +echo $router->generate('user_show', ['id' => 5]); // → /users/5 +``` diff --git a/api/index.php b/api/index.php new file mode 100644 index 0000000..a4bde58 --- /dev/null +++ b/api/index.php @@ -0,0 +1,6 @@ +{let e=t.getAttribute("data-bs-target");if(!e||"#"===e){let i=t.getAttribute("href");if(!i||!i.includes("#")&&!i.startsWith("."))return null;i.includes("#")&&!i.startsWith("#")&&(i=`#${i.split("#")[1]}`),e=i&&"#"!==i?i.trim():null}return e},i=t=>{const i=e(t);return i&&document.querySelector(i)?i:null},n=t=>{const i=e(t);return i?document.querySelector(i):null},s=e=>{e.dispatchEvent(new Event(t))},o=t=>!(!t||"object"!=typeof t)&&(void 0!==t.jquery&&(t=t[0]),void 0!==t.nodeType),r=t=>o(t)?t.jquery?t[0]:t:"string"==typeof t&&t.length>0?document.querySelector(t):null,a=(t,e,i)=>{Object.keys(i).forEach((n=>{const s=i[n],r=e[n],a=r&&o(r)?"element":null==(l=r)?`${l}`:{}.toString.call(l).match(/\s([a-z]+)/i)[1].toLowerCase();var l;if(!new RegExp(s).test(a))throw new TypeError(`${t.toUpperCase()}: Option "${n}" provided type "${a}" but expected type "${s}".`)}))},l=t=>!(!o(t)||0===t.getClientRects().length)&&"visible"===getComputedStyle(t).getPropertyValue("visibility"),c=t=>!t||t.nodeType!==Node.ELEMENT_NODE||!!t.classList.contains("disabled")||(void 0!==t.disabled?t.disabled:t.hasAttribute("disabled")&&"false"!==t.getAttribute("disabled")),h=t=>{if(!document.documentElement.attachShadow)return null;if("function"==typeof t.getRootNode){const e=t.getRootNode();return e instanceof ShadowRoot?e:null}return t instanceof ShadowRoot?t:t.parentNode?h(t.parentNode):null},d=()=>{},u=t=>{t.offsetHeight},f=()=>{const{jQuery:t}=window;return t&&!document.body.hasAttribute("data-bs-no-jquery")?t:null},p=[],m=()=>"rtl"===document.documentElement.dir,g=t=>{var e;e=()=>{const e=f();if(e){const i=t.NAME,n=e.fn[i];e.fn[i]=t.jQueryInterface,e.fn[i].Constructor=t,e.fn[i].noConflict=()=>(e.fn[i]=n,t.jQueryInterface)}},"loading"===document.readyState?(p.length||document.addEventListener("DOMContentLoaded",(()=>{p.forEach((t=>t()))})),p.push(e)):e()},_=t=>{"function"==typeof t&&t()},b=(e,i,n=!0)=>{if(!n)return void _(e);const o=(t=>{if(!t)return 0;let{transitionDuration:e,transitionDelay:i}=window.getComputedStyle(t);const n=Number.parseFloat(e),s=Number.parseFloat(i);return n||s?(e=e.split(",")[0],i=i.split(",")[0],1e3*(Number.parseFloat(e)+Number.parseFloat(i))):0})(i)+5;let r=!1;const a=({target:n})=>{n===i&&(r=!0,i.removeEventListener(t,a),_(e))};i.addEventListener(t,a),setTimeout((()=>{r||s(i)}),o)},v=(t,e,i,n)=>{let s=t.indexOf(e);if(-1===s)return t[!i&&n?t.length-1:0];const o=t.length;return s+=i?1:-1,n&&(s=(s+o)%o),t[Math.max(0,Math.min(s,o-1))]},y=/[^.]*(?=\..*)\.|.*/,w=/\..*/,E=/::\d+$/,A={};let T=1;const O={mouseenter:"mouseover",mouseleave:"mouseout"},C=/^(mouseenter|mouseleave)/i,k=new Set(["click","dblclick","mouseup","mousedown","contextmenu","mousewheel","DOMMouseScroll","mouseover","mouseout","mousemove","selectstart","selectend","keydown","keypress","keyup","orientationchange","touchstart","touchmove","touchend","touchcancel","pointerdown","pointermove","pointerup","pointerleave","pointercancel","gesturestart","gesturechange","gestureend","focus","blur","change","reset","select","submit","focusin","focusout","load","unload","beforeunload","resize","move","DOMContentLoaded","readystatechange","error","abort","scroll"]);function L(t,e){return e&&`${e}::${T++}`||t.uidEvent||T++}function x(t){const e=L(t);return t.uidEvent=e,A[e]=A[e]||{},A[e]}function D(t,e,i=null){const n=Object.keys(t);for(let s=0,o=n.length;sfunction(e){if(!e.relatedTarget||e.relatedTarget!==e.delegateTarget&&!e.delegateTarget.contains(e.relatedTarget))return t.call(this,e)};n?n=t(n):i=t(i)}const[o,r,a]=S(e,i,n),l=x(t),c=l[a]||(l[a]={}),h=D(c,r,o?i:null);if(h)return void(h.oneOff=h.oneOff&&s);const d=L(r,e.replace(y,"")),u=o?function(t,e,i){return function n(s){const o=t.querySelectorAll(e);for(let{target:r}=s;r&&r!==this;r=r.parentNode)for(let a=o.length;a--;)if(o[a]===r)return s.delegateTarget=r,n.oneOff&&j.off(t,s.type,e,i),i.apply(r,[s]);return null}}(t,i,n):function(t,e){return function i(n){return n.delegateTarget=t,i.oneOff&&j.off(t,n.type,e),e.apply(t,[n])}}(t,i);u.delegationSelector=o?i:null,u.originalHandler=r,u.oneOff=s,u.uidEvent=d,c[d]=u,t.addEventListener(a,u,o)}function I(t,e,i,n,s){const o=D(e[i],n,s);o&&(t.removeEventListener(i,o,Boolean(s)),delete e[i][o.uidEvent])}function P(t){return t=t.replace(w,""),O[t]||t}const j={on(t,e,i,n){N(t,e,i,n,!1)},one(t,e,i,n){N(t,e,i,n,!0)},off(t,e,i,n){if("string"!=typeof e||!t)return;const[s,o,r]=S(e,i,n),a=r!==e,l=x(t),c=e.startsWith(".");if(void 0!==o){if(!l||!l[r])return;return void I(t,l,r,o,s?i:null)}c&&Object.keys(l).forEach((i=>{!function(t,e,i,n){const s=e[i]||{};Object.keys(s).forEach((o=>{if(o.includes(n)){const n=s[o];I(t,e,i,n.originalHandler,n.delegationSelector)}}))}(t,l,i,e.slice(1))}));const h=l[r]||{};Object.keys(h).forEach((i=>{const n=i.replace(E,"");if(!a||e.includes(n)){const e=h[i];I(t,l,r,e.originalHandler,e.delegationSelector)}}))},trigger(t,e,i){if("string"!=typeof e||!t)return null;const n=f(),s=P(e),o=e!==s,r=k.has(s);let a,l=!0,c=!0,h=!1,d=null;return o&&n&&(a=n.Event(e,i),n(t).trigger(a),l=!a.isPropagationStopped(),c=!a.isImmediatePropagationStopped(),h=a.isDefaultPrevented()),r?(d=document.createEvent("HTMLEvents"),d.initEvent(s,l,!0)):d=new CustomEvent(e,{bubbles:l,cancelable:!0}),void 0!==i&&Object.keys(i).forEach((t=>{Object.defineProperty(d,t,{get:()=>i[t]})})),h&&d.preventDefault(),c&&t.dispatchEvent(d),d.defaultPrevented&&void 0!==a&&a.preventDefault(),d}},M=new Map,H={set(t,e,i){M.has(t)||M.set(t,new Map);const n=M.get(t);n.has(e)||0===n.size?n.set(e,i):console.error(`Bootstrap doesn't allow more than one instance per element. Bound instance: ${Array.from(n.keys())[0]}.`)},get:(t,e)=>M.has(t)&&M.get(t).get(e)||null,remove(t,e){if(!M.has(t))return;const i=M.get(t);i.delete(e),0===i.size&&M.delete(t)}};class B{constructor(t){(t=r(t))&&(this._element=t,H.set(this._element,this.constructor.DATA_KEY,this))}dispose(){H.remove(this._element,this.constructor.DATA_KEY),j.off(this._element,this.constructor.EVENT_KEY),Object.getOwnPropertyNames(this).forEach((t=>{this[t]=null}))}_queueCallback(t,e,i=!0){b(t,e,i)}static getInstance(t){return H.get(r(t),this.DATA_KEY)}static getOrCreateInstance(t,e={}){return this.getInstance(t)||new this(t,"object"==typeof e?e:null)}static get VERSION(){return"5.1.3"}static get NAME(){throw new Error('You have to implement the static method "NAME", for each component!')}static get DATA_KEY(){return`bs.${this.NAME}`}static get EVENT_KEY(){return`.${this.DATA_KEY}`}}const R=(t,e="hide")=>{const i=`click.dismiss${t.EVENT_KEY}`,s=t.NAME;j.on(document,i,`[data-bs-dismiss="${s}"]`,(function(i){if(["A","AREA"].includes(this.tagName)&&i.preventDefault(),c(this))return;const o=n(this)||this.closest(`.${s}`);t.getOrCreateInstance(o)[e]()}))};class W extends B{static get NAME(){return"alert"}close(){if(j.trigger(this._element,"close.bs.alert").defaultPrevented)return;this._element.classList.remove("show");const t=this._element.classList.contains("fade");this._queueCallback((()=>this._destroyElement()),this._element,t)}_destroyElement(){this._element.remove(),j.trigger(this._element,"closed.bs.alert"),this.dispose()}static jQueryInterface(t){return this.each((function(){const e=W.getOrCreateInstance(this);if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t](this)}}))}}R(W,"close"),g(W);const $='[data-bs-toggle="button"]';class z extends B{static get NAME(){return"button"}toggle(){this._element.setAttribute("aria-pressed",this._element.classList.toggle("active"))}static jQueryInterface(t){return this.each((function(){const e=z.getOrCreateInstance(this);"toggle"===t&&e[t]()}))}}function q(t){return"true"===t||"false"!==t&&(t===Number(t).toString()?Number(t):""===t||"null"===t?null:t)}function F(t){return t.replace(/[A-Z]/g,(t=>`-${t.toLowerCase()}`))}j.on(document,"click.bs.button.data-api",$,(t=>{t.preventDefault();const e=t.target.closest($);z.getOrCreateInstance(e).toggle()})),g(z);const U={setDataAttribute(t,e,i){t.setAttribute(`data-bs-${F(e)}`,i)},removeDataAttribute(t,e){t.removeAttribute(`data-bs-${F(e)}`)},getDataAttributes(t){if(!t)return{};const e={};return Object.keys(t.dataset).filter((t=>t.startsWith("bs"))).forEach((i=>{let n=i.replace(/^bs/,"");n=n.charAt(0).toLowerCase()+n.slice(1,n.length),e[n]=q(t.dataset[i])})),e},getDataAttribute:(t,e)=>q(t.getAttribute(`data-bs-${F(e)}`)),offset(t){const e=t.getBoundingClientRect();return{top:e.top+window.pageYOffset,left:e.left+window.pageXOffset}},position:t=>({top:t.offsetTop,left:t.offsetLeft})},V={find:(t,e=document.documentElement)=>[].concat(...Element.prototype.querySelectorAll.call(e,t)),findOne:(t,e=document.documentElement)=>Element.prototype.querySelector.call(e,t),children:(t,e)=>[].concat(...t.children).filter((t=>t.matches(e))),parents(t,e){const i=[];let n=t.parentNode;for(;n&&n.nodeType===Node.ELEMENT_NODE&&3!==n.nodeType;)n.matches(e)&&i.push(n),n=n.parentNode;return i},prev(t,e){let i=t.previousElementSibling;for(;i;){if(i.matches(e))return[i];i=i.previousElementSibling}return[]},next(t,e){let i=t.nextElementSibling;for(;i;){if(i.matches(e))return[i];i=i.nextElementSibling}return[]},focusableChildren(t){const e=["a","button","input","textarea","select","details","[tabindex]",'[contenteditable="true"]'].map((t=>`${t}:not([tabindex^="-"])`)).join(", ");return this.find(e,t).filter((t=>!c(t)&&l(t)))}},K="carousel",X={interval:5e3,keyboard:!0,slide:!1,pause:"hover",wrap:!0,touch:!0},Y={interval:"(number|boolean)",keyboard:"boolean",slide:"(boolean|string)",pause:"(string|boolean)",wrap:"boolean",touch:"boolean"},Q="next",G="prev",Z="left",J="right",tt={ArrowLeft:J,ArrowRight:Z},et="slid.bs.carousel",it="active",nt=".active.carousel-item";class st extends B{constructor(t,e){super(t),this._items=null,this._interval=null,this._activeElement=null,this._isPaused=!1,this._isSliding=!1,this.touchTimeout=null,this.touchStartX=0,this.touchDeltaX=0,this._config=this._getConfig(e),this._indicatorsElement=V.findOne(".carousel-indicators",this._element),this._touchSupported="ontouchstart"in document.documentElement||navigator.maxTouchPoints>0,this._pointerEvent=Boolean(window.PointerEvent),this._addEventListeners()}static get Default(){return X}static get NAME(){return K}next(){this._slide(Q)}nextWhenVisible(){!document.hidden&&l(this._element)&&this.next()}prev(){this._slide(G)}pause(t){t||(this._isPaused=!0),V.findOne(".carousel-item-next, .carousel-item-prev",this._element)&&(s(this._element),this.cycle(!0)),clearInterval(this._interval),this._interval=null}cycle(t){t||(this._isPaused=!1),this._interval&&(clearInterval(this._interval),this._interval=null),this._config&&this._config.interval&&!this._isPaused&&(this._updateInterval(),this._interval=setInterval((document.visibilityState?this.nextWhenVisible:this.next).bind(this),this._config.interval))}to(t){this._activeElement=V.findOne(nt,this._element);const e=this._getItemIndex(this._activeElement);if(t>this._items.length-1||t<0)return;if(this._isSliding)return void j.one(this._element,et,(()=>this.to(t)));if(e===t)return this.pause(),void this.cycle();const i=t>e?Q:G;this._slide(i,this._items[t])}_getConfig(t){return t={...X,...U.getDataAttributes(this._element),..."object"==typeof t?t:{}},a(K,t,Y),t}_handleSwipe(){const t=Math.abs(this.touchDeltaX);if(t<=40)return;const e=t/this.touchDeltaX;this.touchDeltaX=0,e&&this._slide(e>0?J:Z)}_addEventListeners(){this._config.keyboard&&j.on(this._element,"keydown.bs.carousel",(t=>this._keydown(t))),"hover"===this._config.pause&&(j.on(this._element,"mouseenter.bs.carousel",(t=>this.pause(t))),j.on(this._element,"mouseleave.bs.carousel",(t=>this.cycle(t)))),this._config.touch&&this._touchSupported&&this._addTouchEventListeners()}_addTouchEventListeners(){const t=t=>this._pointerEvent&&("pen"===t.pointerType||"touch"===t.pointerType),e=e=>{t(e)?this.touchStartX=e.clientX:this._pointerEvent||(this.touchStartX=e.touches[0].clientX)},i=t=>{this.touchDeltaX=t.touches&&t.touches.length>1?0:t.touches[0].clientX-this.touchStartX},n=e=>{t(e)&&(this.touchDeltaX=e.clientX-this.touchStartX),this._handleSwipe(),"hover"===this._config.pause&&(this.pause(),this.touchTimeout&&clearTimeout(this.touchTimeout),this.touchTimeout=setTimeout((t=>this.cycle(t)),500+this._config.interval))};V.find(".carousel-item img",this._element).forEach((t=>{j.on(t,"dragstart.bs.carousel",(t=>t.preventDefault()))})),this._pointerEvent?(j.on(this._element,"pointerdown.bs.carousel",(t=>e(t))),j.on(this._element,"pointerup.bs.carousel",(t=>n(t))),this._element.classList.add("pointer-event")):(j.on(this._element,"touchstart.bs.carousel",(t=>e(t))),j.on(this._element,"touchmove.bs.carousel",(t=>i(t))),j.on(this._element,"touchend.bs.carousel",(t=>n(t))))}_keydown(t){if(/input|textarea/i.test(t.target.tagName))return;const e=tt[t.key];e&&(t.preventDefault(),this._slide(e))}_getItemIndex(t){return this._items=t&&t.parentNode?V.find(".carousel-item",t.parentNode):[],this._items.indexOf(t)}_getItemByOrder(t,e){const i=t===Q;return v(this._items,e,i,this._config.wrap)}_triggerSlideEvent(t,e){const i=this._getItemIndex(t),n=this._getItemIndex(V.findOne(nt,this._element));return j.trigger(this._element,"slide.bs.carousel",{relatedTarget:t,direction:e,from:n,to:i})}_setActiveIndicatorElement(t){if(this._indicatorsElement){const e=V.findOne(".active",this._indicatorsElement);e.classList.remove(it),e.removeAttribute("aria-current");const i=V.find("[data-bs-target]",this._indicatorsElement);for(let e=0;e{j.trigger(this._element,et,{relatedTarget:o,direction:d,from:s,to:r})};if(this._element.classList.contains("slide")){o.classList.add(h),u(o),n.classList.add(c),o.classList.add(c);const t=()=>{o.classList.remove(c,h),o.classList.add(it),n.classList.remove(it,h,c),this._isSliding=!1,setTimeout(f,0)};this._queueCallback(t,n,!0)}else n.classList.remove(it),o.classList.add(it),this._isSliding=!1,f();a&&this.cycle()}_directionToOrder(t){return[J,Z].includes(t)?m()?t===Z?G:Q:t===Z?Q:G:t}_orderToDirection(t){return[Q,G].includes(t)?m()?t===G?Z:J:t===G?J:Z:t}static carouselInterface(t,e){const i=st.getOrCreateInstance(t,e);let{_config:n}=i;"object"==typeof e&&(n={...n,...e});const s="string"==typeof e?e:n.slide;if("number"==typeof e)i.to(e);else if("string"==typeof s){if(void 0===i[s])throw new TypeError(`No method named "${s}"`);i[s]()}else n.interval&&n.ride&&(i.pause(),i.cycle())}static jQueryInterface(t){return this.each((function(){st.carouselInterface(this,t)}))}static dataApiClickHandler(t){const e=n(this);if(!e||!e.classList.contains("carousel"))return;const i={...U.getDataAttributes(e),...U.getDataAttributes(this)},s=this.getAttribute("data-bs-slide-to");s&&(i.interval=!1),st.carouselInterface(e,i),s&&st.getInstance(e).to(s),t.preventDefault()}}j.on(document,"click.bs.carousel.data-api","[data-bs-slide], [data-bs-slide-to]",st.dataApiClickHandler),j.on(window,"load.bs.carousel.data-api",(()=>{const t=V.find('[data-bs-ride="carousel"]');for(let e=0,i=t.length;et===this._element));null!==s&&o.length&&(this._selector=s,this._triggerArray.push(e))}this._initializeChildren(),this._config.parent||this._addAriaAndCollapsedClass(this._triggerArray,this._isShown()),this._config.toggle&&this.toggle()}static get Default(){return rt}static get NAME(){return ot}toggle(){this._isShown()?this.hide():this.show()}show(){if(this._isTransitioning||this._isShown())return;let t,e=[];if(this._config.parent){const t=V.find(ut,this._config.parent);e=V.find(".collapse.show, .collapse.collapsing",this._config.parent).filter((e=>!t.includes(e)))}const i=V.findOne(this._selector);if(e.length){const n=e.find((t=>i!==t));if(t=n?pt.getInstance(n):null,t&&t._isTransitioning)return}if(j.trigger(this._element,"show.bs.collapse").defaultPrevented)return;e.forEach((e=>{i!==e&&pt.getOrCreateInstance(e,{toggle:!1}).hide(),t||H.set(e,"bs.collapse",null)}));const n=this._getDimension();this._element.classList.remove(ct),this._element.classList.add(ht),this._element.style[n]=0,this._addAriaAndCollapsedClass(this._triggerArray,!0),this._isTransitioning=!0;const s=`scroll${n[0].toUpperCase()+n.slice(1)}`;this._queueCallback((()=>{this._isTransitioning=!1,this._element.classList.remove(ht),this._element.classList.add(ct,lt),this._element.style[n]="",j.trigger(this._element,"shown.bs.collapse")}),this._element,!0),this._element.style[n]=`${this._element[s]}px`}hide(){if(this._isTransitioning||!this._isShown())return;if(j.trigger(this._element,"hide.bs.collapse").defaultPrevented)return;const t=this._getDimension();this._element.style[t]=`${this._element.getBoundingClientRect()[t]}px`,u(this._element),this._element.classList.add(ht),this._element.classList.remove(ct,lt);const e=this._triggerArray.length;for(let t=0;t{this._isTransitioning=!1,this._element.classList.remove(ht),this._element.classList.add(ct),j.trigger(this._element,"hidden.bs.collapse")}),this._element,!0)}_isShown(t=this._element){return t.classList.contains(lt)}_getConfig(t){return(t={...rt,...U.getDataAttributes(this._element),...t}).toggle=Boolean(t.toggle),t.parent=r(t.parent),a(ot,t,at),t}_getDimension(){return this._element.classList.contains("collapse-horizontal")?"width":"height"}_initializeChildren(){if(!this._config.parent)return;const t=V.find(ut,this._config.parent);V.find(ft,this._config.parent).filter((e=>!t.includes(e))).forEach((t=>{const e=n(t);e&&this._addAriaAndCollapsedClass([t],this._isShown(e))}))}_addAriaAndCollapsedClass(t,e){t.length&&t.forEach((t=>{e?t.classList.remove(dt):t.classList.add(dt),t.setAttribute("aria-expanded",e)}))}static jQueryInterface(t){return this.each((function(){const e={};"string"==typeof t&&/show|hide/.test(t)&&(e.toggle=!1);const i=pt.getOrCreateInstance(this,e);if("string"==typeof t){if(void 0===i[t])throw new TypeError(`No method named "${t}"`);i[t]()}}))}}j.on(document,"click.bs.collapse.data-api",ft,(function(t){("A"===t.target.tagName||t.delegateTarget&&"A"===t.delegateTarget.tagName)&&t.preventDefault();const e=i(this);V.find(e).forEach((t=>{pt.getOrCreateInstance(t,{toggle:!1}).toggle()}))})),g(pt);var mt="top",gt="bottom",_t="right",bt="left",vt="auto",yt=[mt,gt,_t,bt],wt="start",Et="end",At="clippingParents",Tt="viewport",Ot="popper",Ct="reference",kt=yt.reduce((function(t,e){return t.concat([e+"-"+wt,e+"-"+Et])}),[]),Lt=[].concat(yt,[vt]).reduce((function(t,e){return t.concat([e,e+"-"+wt,e+"-"+Et])}),[]),xt="beforeRead",Dt="read",St="afterRead",Nt="beforeMain",It="main",Pt="afterMain",jt="beforeWrite",Mt="write",Ht="afterWrite",Bt=[xt,Dt,St,Nt,It,Pt,jt,Mt,Ht];function Rt(t){return t?(t.nodeName||"").toLowerCase():null}function Wt(t){if(null==t)return window;if("[object Window]"!==t.toString()){var e=t.ownerDocument;return e&&e.defaultView||window}return t}function $t(t){return t instanceof Wt(t).Element||t instanceof Element}function zt(t){return t instanceof Wt(t).HTMLElement||t instanceof HTMLElement}function qt(t){return"undefined"!=typeof ShadowRoot&&(t instanceof Wt(t).ShadowRoot||t instanceof ShadowRoot)}const Ft={name:"applyStyles",enabled:!0,phase:"write",fn:function(t){var e=t.state;Object.keys(e.elements).forEach((function(t){var i=e.styles[t]||{},n=e.attributes[t]||{},s=e.elements[t];zt(s)&&Rt(s)&&(Object.assign(s.style,i),Object.keys(n).forEach((function(t){var e=n[t];!1===e?s.removeAttribute(t):s.setAttribute(t,!0===e?"":e)})))}))},effect:function(t){var e=t.state,i={popper:{position:e.options.strategy,left:"0",top:"0",margin:"0"},arrow:{position:"absolute"},reference:{}};return Object.assign(e.elements.popper.style,i.popper),e.styles=i,e.elements.arrow&&Object.assign(e.elements.arrow.style,i.arrow),function(){Object.keys(e.elements).forEach((function(t){var n=e.elements[t],s=e.attributes[t]||{},o=Object.keys(e.styles.hasOwnProperty(t)?e.styles[t]:i[t]).reduce((function(t,e){return t[e]="",t}),{});zt(n)&&Rt(n)&&(Object.assign(n.style,o),Object.keys(s).forEach((function(t){n.removeAttribute(t)})))}))}},requires:["computeStyles"]};function Ut(t){return t.split("-")[0]}function Vt(t,e){var i=t.getBoundingClientRect();return{width:i.width/1,height:i.height/1,top:i.top/1,right:i.right/1,bottom:i.bottom/1,left:i.left/1,x:i.left/1,y:i.top/1}}function Kt(t){var e=Vt(t),i=t.offsetWidth,n=t.offsetHeight;return Math.abs(e.width-i)<=1&&(i=e.width),Math.abs(e.height-n)<=1&&(n=e.height),{x:t.offsetLeft,y:t.offsetTop,width:i,height:n}}function Xt(t,e){var i=e.getRootNode&&e.getRootNode();if(t.contains(e))return!0;if(i&&qt(i)){var n=e;do{if(n&&t.isSameNode(n))return!0;n=n.parentNode||n.host}while(n)}return!1}function Yt(t){return Wt(t).getComputedStyle(t)}function Qt(t){return["table","td","th"].indexOf(Rt(t))>=0}function Gt(t){return(($t(t)?t.ownerDocument:t.document)||window.document).documentElement}function Zt(t){return"html"===Rt(t)?t:t.assignedSlot||t.parentNode||(qt(t)?t.host:null)||Gt(t)}function Jt(t){return zt(t)&&"fixed"!==Yt(t).position?t.offsetParent:null}function te(t){for(var e=Wt(t),i=Jt(t);i&&Qt(i)&&"static"===Yt(i).position;)i=Jt(i);return i&&("html"===Rt(i)||"body"===Rt(i)&&"static"===Yt(i).position)?e:i||function(t){var e=-1!==navigator.userAgent.toLowerCase().indexOf("firefox");if(-1!==navigator.userAgent.indexOf("Trident")&&zt(t)&&"fixed"===Yt(t).position)return null;for(var i=Zt(t);zt(i)&&["html","body"].indexOf(Rt(i))<0;){var n=Yt(i);if("none"!==n.transform||"none"!==n.perspective||"paint"===n.contain||-1!==["transform","perspective"].indexOf(n.willChange)||e&&"filter"===n.willChange||e&&n.filter&&"none"!==n.filter)return i;i=i.parentNode}return null}(t)||e}function ee(t){return["top","bottom"].indexOf(t)>=0?"x":"y"}var ie=Math.max,ne=Math.min,se=Math.round;function oe(t,e,i){return ie(t,ne(e,i))}function re(t){return Object.assign({},{top:0,right:0,bottom:0,left:0},t)}function ae(t,e){return e.reduce((function(e,i){return e[i]=t,e}),{})}const le={name:"arrow",enabled:!0,phase:"main",fn:function(t){var e,i=t.state,n=t.name,s=t.options,o=i.elements.arrow,r=i.modifiersData.popperOffsets,a=Ut(i.placement),l=ee(a),c=[bt,_t].indexOf(a)>=0?"height":"width";if(o&&r){var h=function(t,e){return re("number"!=typeof(t="function"==typeof t?t(Object.assign({},e.rects,{placement:e.placement})):t)?t:ae(t,yt))}(s.padding,i),d=Kt(o),u="y"===l?mt:bt,f="y"===l?gt:_t,p=i.rects.reference[c]+i.rects.reference[l]-r[l]-i.rects.popper[c],m=r[l]-i.rects.reference[l],g=te(o),_=g?"y"===l?g.clientHeight||0:g.clientWidth||0:0,b=p/2-m/2,v=h[u],y=_-d[c]-h[f],w=_/2-d[c]/2+b,E=oe(v,w,y),A=l;i.modifiersData[n]=((e={})[A]=E,e.centerOffset=E-w,e)}},effect:function(t){var e=t.state,i=t.options.element,n=void 0===i?"[data-popper-arrow]":i;null!=n&&("string"!=typeof n||(n=e.elements.popper.querySelector(n)))&&Xt(e.elements.popper,n)&&(e.elements.arrow=n)},requires:["popperOffsets"],requiresIfExists:["preventOverflow"]};function ce(t){return t.split("-")[1]}var he={top:"auto",right:"auto",bottom:"auto",left:"auto"};function de(t){var e,i=t.popper,n=t.popperRect,s=t.placement,o=t.variation,r=t.offsets,a=t.position,l=t.gpuAcceleration,c=t.adaptive,h=t.roundOffsets,d=!0===h?function(t){var e=t.x,i=t.y,n=window.devicePixelRatio||1;return{x:se(se(e*n)/n)||0,y:se(se(i*n)/n)||0}}(r):"function"==typeof h?h(r):r,u=d.x,f=void 0===u?0:u,p=d.y,m=void 0===p?0:p,g=r.hasOwnProperty("x"),_=r.hasOwnProperty("y"),b=bt,v=mt,y=window;if(c){var w=te(i),E="clientHeight",A="clientWidth";w===Wt(i)&&"static"!==Yt(w=Gt(i)).position&&"absolute"===a&&(E="scrollHeight",A="scrollWidth"),w=w,s!==mt&&(s!==bt&&s!==_t||o!==Et)||(v=gt,m-=w[E]-n.height,m*=l?1:-1),s!==bt&&(s!==mt&&s!==gt||o!==Et)||(b=_t,f-=w[A]-n.width,f*=l?1:-1)}var T,O=Object.assign({position:a},c&&he);return l?Object.assign({},O,((T={})[v]=_?"0":"",T[b]=g?"0":"",T.transform=(y.devicePixelRatio||1)<=1?"translate("+f+"px, "+m+"px)":"translate3d("+f+"px, "+m+"px, 0)",T)):Object.assign({},O,((e={})[v]=_?m+"px":"",e[b]=g?f+"px":"",e.transform="",e))}const ue={name:"computeStyles",enabled:!0,phase:"beforeWrite",fn:function(t){var e=t.state,i=t.options,n=i.gpuAcceleration,s=void 0===n||n,o=i.adaptive,r=void 0===o||o,a=i.roundOffsets,l=void 0===a||a,c={placement:Ut(e.placement),variation:ce(e.placement),popper:e.elements.popper,popperRect:e.rects.popper,gpuAcceleration:s};null!=e.modifiersData.popperOffsets&&(e.styles.popper=Object.assign({},e.styles.popper,de(Object.assign({},c,{offsets:e.modifiersData.popperOffsets,position:e.options.strategy,adaptive:r,roundOffsets:l})))),null!=e.modifiersData.arrow&&(e.styles.arrow=Object.assign({},e.styles.arrow,de(Object.assign({},c,{offsets:e.modifiersData.arrow,position:"absolute",adaptive:!1,roundOffsets:l})))),e.attributes.popper=Object.assign({},e.attributes.popper,{"data-popper-placement":e.placement})},data:{}};var fe={passive:!0};const pe={name:"eventListeners",enabled:!0,phase:"write",fn:function(){},effect:function(t){var e=t.state,i=t.instance,n=t.options,s=n.scroll,o=void 0===s||s,r=n.resize,a=void 0===r||r,l=Wt(e.elements.popper),c=[].concat(e.scrollParents.reference,e.scrollParents.popper);return o&&c.forEach((function(t){t.addEventListener("scroll",i.update,fe)})),a&&l.addEventListener("resize",i.update,fe),function(){o&&c.forEach((function(t){t.removeEventListener("scroll",i.update,fe)})),a&&l.removeEventListener("resize",i.update,fe)}},data:{}};var me={left:"right",right:"left",bottom:"top",top:"bottom"};function ge(t){return t.replace(/left|right|bottom|top/g,(function(t){return me[t]}))}var _e={start:"end",end:"start"};function be(t){return t.replace(/start|end/g,(function(t){return _e[t]}))}function ve(t){var e=Wt(t);return{scrollLeft:e.pageXOffset,scrollTop:e.pageYOffset}}function ye(t){return Vt(Gt(t)).left+ve(t).scrollLeft}function we(t){var e=Yt(t),i=e.overflow,n=e.overflowX,s=e.overflowY;return/auto|scroll|overlay|hidden/.test(i+s+n)}function Ee(t){return["html","body","#document"].indexOf(Rt(t))>=0?t.ownerDocument.body:zt(t)&&we(t)?t:Ee(Zt(t))}function Ae(t,e){var i;void 0===e&&(e=[]);var n=Ee(t),s=n===(null==(i=t.ownerDocument)?void 0:i.body),o=Wt(n),r=s?[o].concat(o.visualViewport||[],we(n)?n:[]):n,a=e.concat(r);return s?a:a.concat(Ae(Zt(r)))}function Te(t){return Object.assign({},t,{left:t.x,top:t.y,right:t.x+t.width,bottom:t.y+t.height})}function Oe(t,e){return e===Tt?Te(function(t){var e=Wt(t),i=Gt(t),n=e.visualViewport,s=i.clientWidth,o=i.clientHeight,r=0,a=0;return n&&(s=n.width,o=n.height,/^((?!chrome|android).)*safari/i.test(navigator.userAgent)||(r=n.offsetLeft,a=n.offsetTop)),{width:s,height:o,x:r+ye(t),y:a}}(t)):zt(e)?function(t){var e=Vt(t);return e.top=e.top+t.clientTop,e.left=e.left+t.clientLeft,e.bottom=e.top+t.clientHeight,e.right=e.left+t.clientWidth,e.width=t.clientWidth,e.height=t.clientHeight,e.x=e.left,e.y=e.top,e}(e):Te(function(t){var e,i=Gt(t),n=ve(t),s=null==(e=t.ownerDocument)?void 0:e.body,o=ie(i.scrollWidth,i.clientWidth,s?s.scrollWidth:0,s?s.clientWidth:0),r=ie(i.scrollHeight,i.clientHeight,s?s.scrollHeight:0,s?s.clientHeight:0),a=-n.scrollLeft+ye(t),l=-n.scrollTop;return"rtl"===Yt(s||i).direction&&(a+=ie(i.clientWidth,s?s.clientWidth:0)-o),{width:o,height:r,x:a,y:l}}(Gt(t)))}function Ce(t){var e,i=t.reference,n=t.element,s=t.placement,o=s?Ut(s):null,r=s?ce(s):null,a=i.x+i.width/2-n.width/2,l=i.y+i.height/2-n.height/2;switch(o){case mt:e={x:a,y:i.y-n.height};break;case gt:e={x:a,y:i.y+i.height};break;case _t:e={x:i.x+i.width,y:l};break;case bt:e={x:i.x-n.width,y:l};break;default:e={x:i.x,y:i.y}}var c=o?ee(o):null;if(null!=c){var h="y"===c?"height":"width";switch(r){case wt:e[c]=e[c]-(i[h]/2-n[h]/2);break;case Et:e[c]=e[c]+(i[h]/2-n[h]/2)}}return e}function ke(t,e){void 0===e&&(e={});var i=e,n=i.placement,s=void 0===n?t.placement:n,o=i.boundary,r=void 0===o?At:o,a=i.rootBoundary,l=void 0===a?Tt:a,c=i.elementContext,h=void 0===c?Ot:c,d=i.altBoundary,u=void 0!==d&&d,f=i.padding,p=void 0===f?0:f,m=re("number"!=typeof p?p:ae(p,yt)),g=h===Ot?Ct:Ot,_=t.rects.popper,b=t.elements[u?g:h],v=function(t,e,i){var n="clippingParents"===e?function(t){var e=Ae(Zt(t)),i=["absolute","fixed"].indexOf(Yt(t).position)>=0&&zt(t)?te(t):t;return $t(i)?e.filter((function(t){return $t(t)&&Xt(t,i)&&"body"!==Rt(t)})):[]}(t):[].concat(e),s=[].concat(n,[i]),o=s[0],r=s.reduce((function(e,i){var n=Oe(t,i);return e.top=ie(n.top,e.top),e.right=ne(n.right,e.right),e.bottom=ne(n.bottom,e.bottom),e.left=ie(n.left,e.left),e}),Oe(t,o));return r.width=r.right-r.left,r.height=r.bottom-r.top,r.x=r.left,r.y=r.top,r}($t(b)?b:b.contextElement||Gt(t.elements.popper),r,l),y=Vt(t.elements.reference),w=Ce({reference:y,element:_,strategy:"absolute",placement:s}),E=Te(Object.assign({},_,w)),A=h===Ot?E:y,T={top:v.top-A.top+m.top,bottom:A.bottom-v.bottom+m.bottom,left:v.left-A.left+m.left,right:A.right-v.right+m.right},O=t.modifiersData.offset;if(h===Ot&&O){var C=O[s];Object.keys(T).forEach((function(t){var e=[_t,gt].indexOf(t)>=0?1:-1,i=[mt,gt].indexOf(t)>=0?"y":"x";T[t]+=C[i]*e}))}return T}function Le(t,e){void 0===e&&(e={});var i=e,n=i.placement,s=i.boundary,o=i.rootBoundary,r=i.padding,a=i.flipVariations,l=i.allowedAutoPlacements,c=void 0===l?Lt:l,h=ce(n),d=h?a?kt:kt.filter((function(t){return ce(t)===h})):yt,u=d.filter((function(t){return c.indexOf(t)>=0}));0===u.length&&(u=d);var f=u.reduce((function(e,i){return e[i]=ke(t,{placement:i,boundary:s,rootBoundary:o,padding:r})[Ut(i)],e}),{});return Object.keys(f).sort((function(t,e){return f[t]-f[e]}))}const xe={name:"flip",enabled:!0,phase:"main",fn:function(t){var e=t.state,i=t.options,n=t.name;if(!e.modifiersData[n]._skip){for(var s=i.mainAxis,o=void 0===s||s,r=i.altAxis,a=void 0===r||r,l=i.fallbackPlacements,c=i.padding,h=i.boundary,d=i.rootBoundary,u=i.altBoundary,f=i.flipVariations,p=void 0===f||f,m=i.allowedAutoPlacements,g=e.options.placement,_=Ut(g),b=l||(_!==g&&p?function(t){if(Ut(t)===vt)return[];var e=ge(t);return[be(t),e,be(e)]}(g):[ge(g)]),v=[g].concat(b).reduce((function(t,i){return t.concat(Ut(i)===vt?Le(e,{placement:i,boundary:h,rootBoundary:d,padding:c,flipVariations:p,allowedAutoPlacements:m}):i)}),[]),y=e.rects.reference,w=e.rects.popper,E=new Map,A=!0,T=v[0],O=0;O=0,D=x?"width":"height",S=ke(e,{placement:C,boundary:h,rootBoundary:d,altBoundary:u,padding:c}),N=x?L?_t:bt:L?gt:mt;y[D]>w[D]&&(N=ge(N));var I=ge(N),P=[];if(o&&P.push(S[k]<=0),a&&P.push(S[N]<=0,S[I]<=0),P.every((function(t){return t}))){T=C,A=!1;break}E.set(C,P)}if(A)for(var j=function(t){var e=v.find((function(e){var i=E.get(e);if(i)return i.slice(0,t).every((function(t){return t}))}));if(e)return T=e,"break"},M=p?3:1;M>0&&"break"!==j(M);M--);e.placement!==T&&(e.modifiersData[n]._skip=!0,e.placement=T,e.reset=!0)}},requiresIfExists:["offset"],data:{_skip:!1}};function De(t,e,i){return void 0===i&&(i={x:0,y:0}),{top:t.top-e.height-i.y,right:t.right-e.width+i.x,bottom:t.bottom-e.height+i.y,left:t.left-e.width-i.x}}function Se(t){return[mt,_t,gt,bt].some((function(e){return t[e]>=0}))}const Ne={name:"hide",enabled:!0,phase:"main",requiresIfExists:["preventOverflow"],fn:function(t){var e=t.state,i=t.name,n=e.rects.reference,s=e.rects.popper,o=e.modifiersData.preventOverflow,r=ke(e,{elementContext:"reference"}),a=ke(e,{altBoundary:!0}),l=De(r,n),c=De(a,s,o),h=Se(l),d=Se(c);e.modifiersData[i]={referenceClippingOffsets:l,popperEscapeOffsets:c,isReferenceHidden:h,hasPopperEscaped:d},e.attributes.popper=Object.assign({},e.attributes.popper,{"data-popper-reference-hidden":h,"data-popper-escaped":d})}},Ie={name:"offset",enabled:!0,phase:"main",requires:["popperOffsets"],fn:function(t){var e=t.state,i=t.options,n=t.name,s=i.offset,o=void 0===s?[0,0]:s,r=Lt.reduce((function(t,i){return t[i]=function(t,e,i){var n=Ut(t),s=[bt,mt].indexOf(n)>=0?-1:1,o="function"==typeof i?i(Object.assign({},e,{placement:t})):i,r=o[0],a=o[1];return r=r||0,a=(a||0)*s,[bt,_t].indexOf(n)>=0?{x:a,y:r}:{x:r,y:a}}(i,e.rects,o),t}),{}),a=r[e.placement],l=a.x,c=a.y;null!=e.modifiersData.popperOffsets&&(e.modifiersData.popperOffsets.x+=l,e.modifiersData.popperOffsets.y+=c),e.modifiersData[n]=r}},Pe={name:"popperOffsets",enabled:!0,phase:"read",fn:function(t){var e=t.state,i=t.name;e.modifiersData[i]=Ce({reference:e.rects.reference,element:e.rects.popper,strategy:"absolute",placement:e.placement})},data:{}},je={name:"preventOverflow",enabled:!0,phase:"main",fn:function(t){var e=t.state,i=t.options,n=t.name,s=i.mainAxis,o=void 0===s||s,r=i.altAxis,a=void 0!==r&&r,l=i.boundary,c=i.rootBoundary,h=i.altBoundary,d=i.padding,u=i.tether,f=void 0===u||u,p=i.tetherOffset,m=void 0===p?0:p,g=ke(e,{boundary:l,rootBoundary:c,padding:d,altBoundary:h}),_=Ut(e.placement),b=ce(e.placement),v=!b,y=ee(_),w="x"===y?"y":"x",E=e.modifiersData.popperOffsets,A=e.rects.reference,T=e.rects.popper,O="function"==typeof m?m(Object.assign({},e.rects,{placement:e.placement})):m,C={x:0,y:0};if(E){if(o||a){var k="y"===y?mt:bt,L="y"===y?gt:_t,x="y"===y?"height":"width",D=E[y],S=E[y]+g[k],N=E[y]-g[L],I=f?-T[x]/2:0,P=b===wt?A[x]:T[x],j=b===wt?-T[x]:-A[x],M=e.elements.arrow,H=f&&M?Kt(M):{width:0,height:0},B=e.modifiersData["arrow#persistent"]?e.modifiersData["arrow#persistent"].padding:{top:0,right:0,bottom:0,left:0},R=B[k],W=B[L],$=oe(0,A[x],H[x]),z=v?A[x]/2-I-$-R-O:P-$-R-O,q=v?-A[x]/2+I+$+W+O:j+$+W+O,F=e.elements.arrow&&te(e.elements.arrow),U=F?"y"===y?F.clientTop||0:F.clientLeft||0:0,V=e.modifiersData.offset?e.modifiersData.offset[e.placement][y]:0,K=E[y]+z-V-U,X=E[y]+q-V;if(o){var Y=oe(f?ne(S,K):S,D,f?ie(N,X):N);E[y]=Y,C[y]=Y-D}if(a){var Q="x"===y?mt:bt,G="x"===y?gt:_t,Z=E[w],J=Z+g[Q],tt=Z-g[G],et=oe(f?ne(J,K):J,Z,f?ie(tt,X):tt);E[w]=et,C[w]=et-Z}}e.modifiersData[n]=C}},requiresIfExists:["offset"]};function Me(t,e,i){void 0===i&&(i=!1);var n=zt(e);zt(e)&&function(t){var e=t.getBoundingClientRect();e.width,t.offsetWidth,e.height,t.offsetHeight}(e);var s,o,r=Gt(e),a=Vt(t),l={scrollLeft:0,scrollTop:0},c={x:0,y:0};return(n||!n&&!i)&&(("body"!==Rt(e)||we(r))&&(l=(s=e)!==Wt(s)&&zt(s)?{scrollLeft:(o=s).scrollLeft,scrollTop:o.scrollTop}:ve(s)),zt(e)?((c=Vt(e)).x+=e.clientLeft,c.y+=e.clientTop):r&&(c.x=ye(r))),{x:a.left+l.scrollLeft-c.x,y:a.top+l.scrollTop-c.y,width:a.width,height:a.height}}function He(t){var e=new Map,i=new Set,n=[];function s(t){i.add(t.name),[].concat(t.requires||[],t.requiresIfExists||[]).forEach((function(t){if(!i.has(t)){var n=e.get(t);n&&s(n)}})),n.push(t)}return t.forEach((function(t){e.set(t.name,t)})),t.forEach((function(t){i.has(t.name)||s(t)})),n}var Be={placement:"bottom",modifiers:[],strategy:"absolute"};function Re(){for(var t=arguments.length,e=new Array(t),i=0;ij.on(t,"mouseover",d))),this._element.focus(),this._element.setAttribute("aria-expanded",!0),this._menu.classList.add(Je),this._element.classList.add(Je),j.trigger(this._element,"shown.bs.dropdown",t)}hide(){if(c(this._element)||!this._isShown(this._menu))return;const t={relatedTarget:this._element};this._completeHide(t)}dispose(){this._popper&&this._popper.destroy(),super.dispose()}update(){this._inNavbar=this._detectNavbar(),this._popper&&this._popper.update()}_completeHide(t){j.trigger(this._element,"hide.bs.dropdown",t).defaultPrevented||("ontouchstart"in document.documentElement&&[].concat(...document.body.children).forEach((t=>j.off(t,"mouseover",d))),this._popper&&this._popper.destroy(),this._menu.classList.remove(Je),this._element.classList.remove(Je),this._element.setAttribute("aria-expanded","false"),U.removeDataAttribute(this._menu,"popper"),j.trigger(this._element,"hidden.bs.dropdown",t))}_getConfig(t){if(t={...this.constructor.Default,...U.getDataAttributes(this._element),...t},a(Ue,t,this.constructor.DefaultType),"object"==typeof t.reference&&!o(t.reference)&&"function"!=typeof t.reference.getBoundingClientRect)throw new TypeError(`${Ue.toUpperCase()}: Option "reference" provided type "object" without a required "getBoundingClientRect" method.`);return t}_createPopper(t){if(void 0===Fe)throw new TypeError("Bootstrap's dropdowns require Popper (https://popper.js.org)");let e=this._element;"parent"===this._config.reference?e=t:o(this._config.reference)?e=r(this._config.reference):"object"==typeof this._config.reference&&(e=this._config.reference);const i=this._getPopperConfig(),n=i.modifiers.find((t=>"applyStyles"===t.name&&!1===t.enabled));this._popper=qe(e,this._menu,i),n&&U.setDataAttribute(this._menu,"popper","static")}_isShown(t=this._element){return t.classList.contains(Je)}_getMenuElement(){return V.next(this._element,ei)[0]}_getPlacement(){const t=this._element.parentNode;if(t.classList.contains("dropend"))return ri;if(t.classList.contains("dropstart"))return ai;const e="end"===getComputedStyle(this._menu).getPropertyValue("--bs-position").trim();return t.classList.contains("dropup")?e?ni:ii:e?oi:si}_detectNavbar(){return null!==this._element.closest(".navbar")}_getOffset(){const{offset:t}=this._config;return"string"==typeof t?t.split(",").map((t=>Number.parseInt(t,10))):"function"==typeof t?e=>t(e,this._element):t}_getPopperConfig(){const t={placement:this._getPlacement(),modifiers:[{name:"preventOverflow",options:{boundary:this._config.boundary}},{name:"offset",options:{offset:this._getOffset()}}]};return"static"===this._config.display&&(t.modifiers=[{name:"applyStyles",enabled:!1}]),{...t,..."function"==typeof this._config.popperConfig?this._config.popperConfig(t):this._config.popperConfig}}_selectMenuItem({key:t,target:e}){const i=V.find(".dropdown-menu .dropdown-item:not(.disabled):not(:disabled)",this._menu).filter(l);i.length&&v(i,e,t===Ye,!i.includes(e)).focus()}static jQueryInterface(t){return this.each((function(){const e=hi.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t]()}}))}static clearMenus(t){if(t&&(2===t.button||"keyup"===t.type&&"Tab"!==t.key))return;const e=V.find(ti);for(let i=0,n=e.length;ie+t)),this._setElementAttributes(di,"paddingRight",(e=>e+t)),this._setElementAttributes(ui,"marginRight",(e=>e-t))}_disableOverFlow(){this._saveInitialAttribute(this._element,"overflow"),this._element.style.overflow="hidden"}_setElementAttributes(t,e,i){const n=this.getWidth();this._applyManipulationCallback(t,(t=>{if(t!==this._element&&window.innerWidth>t.clientWidth+n)return;this._saveInitialAttribute(t,e);const s=window.getComputedStyle(t)[e];t.style[e]=`${i(Number.parseFloat(s))}px`}))}reset(){this._resetElementAttributes(this._element,"overflow"),this._resetElementAttributes(this._element,"paddingRight"),this._resetElementAttributes(di,"paddingRight"),this._resetElementAttributes(ui,"marginRight")}_saveInitialAttribute(t,e){const i=t.style[e];i&&U.setDataAttribute(t,e,i)}_resetElementAttributes(t,e){this._applyManipulationCallback(t,(t=>{const i=U.getDataAttribute(t,e);void 0===i?t.style.removeProperty(e):(U.removeDataAttribute(t,e),t.style[e]=i)}))}_applyManipulationCallback(t,e){o(t)?e(t):V.find(t,this._element).forEach(e)}isOverflowing(){return this.getWidth()>0}}const pi={className:"modal-backdrop",isVisible:!0,isAnimated:!1,rootElement:"body",clickCallback:null},mi={className:"string",isVisible:"boolean",isAnimated:"boolean",rootElement:"(element|string)",clickCallback:"(function|null)"},gi="show",_i="mousedown.bs.backdrop";class bi{constructor(t){this._config=this._getConfig(t),this._isAppended=!1,this._element=null}show(t){this._config.isVisible?(this._append(),this._config.isAnimated&&u(this._getElement()),this._getElement().classList.add(gi),this._emulateAnimation((()=>{_(t)}))):_(t)}hide(t){this._config.isVisible?(this._getElement().classList.remove(gi),this._emulateAnimation((()=>{this.dispose(),_(t)}))):_(t)}_getElement(){if(!this._element){const t=document.createElement("div");t.className=this._config.className,this._config.isAnimated&&t.classList.add("fade"),this._element=t}return this._element}_getConfig(t){return(t={...pi,..."object"==typeof t?t:{}}).rootElement=r(t.rootElement),a("backdrop",t,mi),t}_append(){this._isAppended||(this._config.rootElement.append(this._getElement()),j.on(this._getElement(),_i,(()=>{_(this._config.clickCallback)})),this._isAppended=!0)}dispose(){this._isAppended&&(j.off(this._element,_i),this._element.remove(),this._isAppended=!1)}_emulateAnimation(t){b(t,this._getElement(),this._config.isAnimated)}}const vi={trapElement:null,autofocus:!0},yi={trapElement:"element",autofocus:"boolean"},wi=".bs.focustrap",Ei="backward";class Ai{constructor(t){this._config=this._getConfig(t),this._isActive=!1,this._lastTabNavDirection=null}activate(){const{trapElement:t,autofocus:e}=this._config;this._isActive||(e&&t.focus(),j.off(document,wi),j.on(document,"focusin.bs.focustrap",(t=>this._handleFocusin(t))),j.on(document,"keydown.tab.bs.focustrap",(t=>this._handleKeydown(t))),this._isActive=!0)}deactivate(){this._isActive&&(this._isActive=!1,j.off(document,wi))}_handleFocusin(t){const{target:e}=t,{trapElement:i}=this._config;if(e===document||e===i||i.contains(e))return;const n=V.focusableChildren(i);0===n.length?i.focus():this._lastTabNavDirection===Ei?n[n.length-1].focus():n[0].focus()}_handleKeydown(t){"Tab"===t.key&&(this._lastTabNavDirection=t.shiftKey?Ei:"forward")}_getConfig(t){return t={...vi,..."object"==typeof t?t:{}},a("focustrap",t,yi),t}}const Ti="modal",Oi="Escape",Ci={backdrop:!0,keyboard:!0,focus:!0},ki={backdrop:"(boolean|string)",keyboard:"boolean",focus:"boolean"},Li="hidden.bs.modal",xi="show.bs.modal",Di="resize.bs.modal",Si="click.dismiss.bs.modal",Ni="keydown.dismiss.bs.modal",Ii="mousedown.dismiss.bs.modal",Pi="modal-open",ji="show",Mi="modal-static";class Hi extends B{constructor(t,e){super(t),this._config=this._getConfig(e),this._dialog=V.findOne(".modal-dialog",this._element),this._backdrop=this._initializeBackDrop(),this._focustrap=this._initializeFocusTrap(),this._isShown=!1,this._ignoreBackdropClick=!1,this._isTransitioning=!1,this._scrollBar=new fi}static get Default(){return Ci}static get NAME(){return Ti}toggle(t){return this._isShown?this.hide():this.show(t)}show(t){this._isShown||this._isTransitioning||j.trigger(this._element,xi,{relatedTarget:t}).defaultPrevented||(this._isShown=!0,this._isAnimated()&&(this._isTransitioning=!0),this._scrollBar.hide(),document.body.classList.add(Pi),this._adjustDialog(),this._setEscapeEvent(),this._setResizeEvent(),j.on(this._dialog,Ii,(()=>{j.one(this._element,"mouseup.dismiss.bs.modal",(t=>{t.target===this._element&&(this._ignoreBackdropClick=!0)}))})),this._showBackdrop((()=>this._showElement(t))))}hide(){if(!this._isShown||this._isTransitioning)return;if(j.trigger(this._element,"hide.bs.modal").defaultPrevented)return;this._isShown=!1;const t=this._isAnimated();t&&(this._isTransitioning=!0),this._setEscapeEvent(),this._setResizeEvent(),this._focustrap.deactivate(),this._element.classList.remove(ji),j.off(this._element,Si),j.off(this._dialog,Ii),this._queueCallback((()=>this._hideModal()),this._element,t)}dispose(){[window,this._dialog].forEach((t=>j.off(t,".bs.modal"))),this._backdrop.dispose(),this._focustrap.deactivate(),super.dispose()}handleUpdate(){this._adjustDialog()}_initializeBackDrop(){return new bi({isVisible:Boolean(this._config.backdrop),isAnimated:this._isAnimated()})}_initializeFocusTrap(){return new Ai({trapElement:this._element})}_getConfig(t){return t={...Ci,...U.getDataAttributes(this._element),..."object"==typeof t?t:{}},a(Ti,t,ki),t}_showElement(t){const e=this._isAnimated(),i=V.findOne(".modal-body",this._dialog);this._element.parentNode&&this._element.parentNode.nodeType===Node.ELEMENT_NODE||document.body.append(this._element),this._element.style.display="block",this._element.removeAttribute("aria-hidden"),this._element.setAttribute("aria-modal",!0),this._element.setAttribute("role","dialog"),this._element.scrollTop=0,i&&(i.scrollTop=0),e&&u(this._element),this._element.classList.add(ji),this._queueCallback((()=>{this._config.focus&&this._focustrap.activate(),this._isTransitioning=!1,j.trigger(this._element,"shown.bs.modal",{relatedTarget:t})}),this._dialog,e)}_setEscapeEvent(){this._isShown?j.on(this._element,Ni,(t=>{this._config.keyboard&&t.key===Oi?(t.preventDefault(),this.hide()):this._config.keyboard||t.key!==Oi||this._triggerBackdropTransition()})):j.off(this._element,Ni)}_setResizeEvent(){this._isShown?j.on(window,Di,(()=>this._adjustDialog())):j.off(window,Di)}_hideModal(){this._element.style.display="none",this._element.setAttribute("aria-hidden",!0),this._element.removeAttribute("aria-modal"),this._element.removeAttribute("role"),this._isTransitioning=!1,this._backdrop.hide((()=>{document.body.classList.remove(Pi),this._resetAdjustments(),this._scrollBar.reset(),j.trigger(this._element,Li)}))}_showBackdrop(t){j.on(this._element,Si,(t=>{this._ignoreBackdropClick?this._ignoreBackdropClick=!1:t.target===t.currentTarget&&(!0===this._config.backdrop?this.hide():"static"===this._config.backdrop&&this._triggerBackdropTransition())})),this._backdrop.show(t)}_isAnimated(){return this._element.classList.contains("fade")}_triggerBackdropTransition(){if(j.trigger(this._element,"hidePrevented.bs.modal").defaultPrevented)return;const{classList:t,scrollHeight:e,style:i}=this._element,n=e>document.documentElement.clientHeight;!n&&"hidden"===i.overflowY||t.contains(Mi)||(n||(i.overflowY="hidden"),t.add(Mi),this._queueCallback((()=>{t.remove(Mi),n||this._queueCallback((()=>{i.overflowY=""}),this._dialog)}),this._dialog),this._element.focus())}_adjustDialog(){const t=this._element.scrollHeight>document.documentElement.clientHeight,e=this._scrollBar.getWidth(),i=e>0;(!i&&t&&!m()||i&&!t&&m())&&(this._element.style.paddingLeft=`${e}px`),(i&&!t&&!m()||!i&&t&&m())&&(this._element.style.paddingRight=`${e}px`)}_resetAdjustments(){this._element.style.paddingLeft="",this._element.style.paddingRight=""}static jQueryInterface(t,e){return this.each((function(){const i=Hi.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===i[t])throw new TypeError(`No method named "${t}"`);i[t](e)}}))}}j.on(document,"click.bs.modal.data-api",'[data-bs-toggle="modal"]',(function(t){const e=n(this);["A","AREA"].includes(this.tagName)&&t.preventDefault(),j.one(e,xi,(t=>{t.defaultPrevented||j.one(e,Li,(()=>{l(this)&&this.focus()}))}));const i=V.findOne(".modal.show");i&&Hi.getInstance(i).hide(),Hi.getOrCreateInstance(e).toggle(this)})),R(Hi),g(Hi);const Bi="offcanvas",Ri={backdrop:!0,keyboard:!0,scroll:!1},Wi={backdrop:"boolean",keyboard:"boolean",scroll:"boolean"},$i="show",zi=".offcanvas.show",qi="hidden.bs.offcanvas";class Fi extends B{constructor(t,e){super(t),this._config=this._getConfig(e),this._isShown=!1,this._backdrop=this._initializeBackDrop(),this._focustrap=this._initializeFocusTrap(),this._addEventListeners()}static get NAME(){return Bi}static get Default(){return Ri}toggle(t){return this._isShown?this.hide():this.show(t)}show(t){this._isShown||j.trigger(this._element,"show.bs.offcanvas",{relatedTarget:t}).defaultPrevented||(this._isShown=!0,this._element.style.visibility="visible",this._backdrop.show(),this._config.scroll||(new fi).hide(),this._element.removeAttribute("aria-hidden"),this._element.setAttribute("aria-modal",!0),this._element.setAttribute("role","dialog"),this._element.classList.add($i),this._queueCallback((()=>{this._config.scroll||this._focustrap.activate(),j.trigger(this._element,"shown.bs.offcanvas",{relatedTarget:t})}),this._element,!0))}hide(){this._isShown&&(j.trigger(this._element,"hide.bs.offcanvas").defaultPrevented||(this._focustrap.deactivate(),this._element.blur(),this._isShown=!1,this._element.classList.remove($i),this._backdrop.hide(),this._queueCallback((()=>{this._element.setAttribute("aria-hidden",!0),this._element.removeAttribute("aria-modal"),this._element.removeAttribute("role"),this._element.style.visibility="hidden",this._config.scroll||(new fi).reset(),j.trigger(this._element,qi)}),this._element,!0)))}dispose(){this._backdrop.dispose(),this._focustrap.deactivate(),super.dispose()}_getConfig(t){return t={...Ri,...U.getDataAttributes(this._element),..."object"==typeof t?t:{}},a(Bi,t,Wi),t}_initializeBackDrop(){return new bi({className:"offcanvas-backdrop",isVisible:this._config.backdrop,isAnimated:!0,rootElement:this._element.parentNode,clickCallback:()=>this.hide()})}_initializeFocusTrap(){return new Ai({trapElement:this._element})}_addEventListeners(){j.on(this._element,"keydown.dismiss.bs.offcanvas",(t=>{this._config.keyboard&&"Escape"===t.key&&this.hide()}))}static jQueryInterface(t){return this.each((function(){const e=Fi.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t](this)}}))}}j.on(document,"click.bs.offcanvas.data-api",'[data-bs-toggle="offcanvas"]',(function(t){const e=n(this);if(["A","AREA"].includes(this.tagName)&&t.preventDefault(),c(this))return;j.one(e,qi,(()=>{l(this)&&this.focus()}));const i=V.findOne(zi);i&&i!==e&&Fi.getInstance(i).hide(),Fi.getOrCreateInstance(e).toggle(this)})),j.on(window,"load.bs.offcanvas.data-api",(()=>V.find(zi).forEach((t=>Fi.getOrCreateInstance(t).show())))),R(Fi),g(Fi);const Ui=new Set(["background","cite","href","itemtype","longdesc","poster","src","xlink:href"]),Vi=/^(?:(?:https?|mailto|ftp|tel|file|sms):|[^#&/:?]*(?:[#/?]|$))/i,Ki=/^data:(?:image\/(?:bmp|gif|jpeg|jpg|png|tiff|webp)|video\/(?:mpeg|mp4|ogg|webm)|audio\/(?:mp3|oga|ogg|opus));base64,[\d+/a-z]+=*$/i,Xi=(t,e)=>{const i=t.nodeName.toLowerCase();if(e.includes(i))return!Ui.has(i)||Boolean(Vi.test(t.nodeValue)||Ki.test(t.nodeValue));const n=e.filter((t=>t instanceof RegExp));for(let t=0,e=n.length;t{Xi(t,r)||i.removeAttribute(t.nodeName)}))}return n.body.innerHTML}const Qi="tooltip",Gi=new Set(["sanitize","allowList","sanitizeFn"]),Zi={animation:"boolean",template:"string",title:"(string|element|function)",trigger:"string",delay:"(number|object)",html:"boolean",selector:"(string|boolean)",placement:"(string|function)",offset:"(array|string|function)",container:"(string|element|boolean)",fallbackPlacements:"array",boundary:"(string|element)",customClass:"(string|function)",sanitize:"boolean",sanitizeFn:"(null|function)",allowList:"object",popperConfig:"(null|object|function)"},Ji={AUTO:"auto",TOP:"top",RIGHT:m()?"left":"right",BOTTOM:"bottom",LEFT:m()?"right":"left"},tn={animation:!0,template:'',trigger:"hover focus",title:"",delay:0,html:!1,selector:!1,placement:"top",offset:[0,0],container:!1,fallbackPlacements:["top","right","bottom","left"],boundary:"clippingParents",customClass:"",sanitize:!0,sanitizeFn:null,allowList:{"*":["class","dir","id","lang","role",/^aria-[\w-]*$/i],a:["target","href","title","rel"],area:[],b:[],br:[],col:[],code:[],div:[],em:[],hr:[],h1:[],h2:[],h3:[],h4:[],h5:[],h6:[],i:[],img:["src","srcset","alt","title","width","height"],li:[],ol:[],p:[],pre:[],s:[],small:[],span:[],sub:[],sup:[],strong:[],u:[],ul:[]},popperConfig:null},en={HIDE:"hide.bs.tooltip",HIDDEN:"hidden.bs.tooltip",SHOW:"show.bs.tooltip",SHOWN:"shown.bs.tooltip",INSERTED:"inserted.bs.tooltip",CLICK:"click.bs.tooltip",FOCUSIN:"focusin.bs.tooltip",FOCUSOUT:"focusout.bs.tooltip",MOUSEENTER:"mouseenter.bs.tooltip",MOUSELEAVE:"mouseleave.bs.tooltip"},nn="fade",sn="show",on="show",rn="out",an=".tooltip-inner",ln=".modal",cn="hide.bs.modal",hn="hover",dn="focus";class un extends B{constructor(t,e){if(void 0===Fe)throw new TypeError("Bootstrap's tooltips require Popper (https://popper.js.org)");super(t),this._isEnabled=!0,this._timeout=0,this._hoverState="",this._activeTrigger={},this._popper=null,this._config=this._getConfig(e),this.tip=null,this._setListeners()}static get Default(){return tn}static get NAME(){return Qi}static get Event(){return en}static get DefaultType(){return Zi}enable(){this._isEnabled=!0}disable(){this._isEnabled=!1}toggleEnabled(){this._isEnabled=!this._isEnabled}toggle(t){if(this._isEnabled)if(t){const e=this._initializeOnDelegatedTarget(t);e._activeTrigger.click=!e._activeTrigger.click,e._isWithActiveTrigger()?e._enter(null,e):e._leave(null,e)}else{if(this.getTipElement().classList.contains(sn))return void this._leave(null,this);this._enter(null,this)}}dispose(){clearTimeout(this._timeout),j.off(this._element.closest(ln),cn,this._hideModalHandler),this.tip&&this.tip.remove(),this._disposePopper(),super.dispose()}show(){if("none"===this._element.style.display)throw new Error("Please use show on visible elements");if(!this.isWithContent()||!this._isEnabled)return;const t=j.trigger(this._element,this.constructor.Event.SHOW),e=h(this._element),i=null===e?this._element.ownerDocument.documentElement.contains(this._element):e.contains(this._element);if(t.defaultPrevented||!i)return;"tooltip"===this.constructor.NAME&&this.tip&&this.getTitle()!==this.tip.querySelector(an).innerHTML&&(this._disposePopper(),this.tip.remove(),this.tip=null);const n=this.getTipElement(),s=(t=>{do{t+=Math.floor(1e6*Math.random())}while(document.getElementById(t));return t})(this.constructor.NAME);n.setAttribute("id",s),this._element.setAttribute("aria-describedby",s),this._config.animation&&n.classList.add(nn);const o="function"==typeof this._config.placement?this._config.placement.call(this,n,this._element):this._config.placement,r=this._getAttachment(o);this._addAttachmentClass(r);const{container:a}=this._config;H.set(n,this.constructor.DATA_KEY,this),this._element.ownerDocument.documentElement.contains(this.tip)||(a.append(n),j.trigger(this._element,this.constructor.Event.INSERTED)),this._popper?this._popper.update():this._popper=qe(this._element,n,this._getPopperConfig(r)),n.classList.add(sn);const l=this._resolvePossibleFunction(this._config.customClass);l&&n.classList.add(...l.split(" ")),"ontouchstart"in document.documentElement&&[].concat(...document.body.children).forEach((t=>{j.on(t,"mouseover",d)}));const c=this.tip.classList.contains(nn);this._queueCallback((()=>{const t=this._hoverState;this._hoverState=null,j.trigger(this._element,this.constructor.Event.SHOWN),t===rn&&this._leave(null,this)}),this.tip,c)}hide(){if(!this._popper)return;const t=this.getTipElement();if(j.trigger(this._element,this.constructor.Event.HIDE).defaultPrevented)return;t.classList.remove(sn),"ontouchstart"in document.documentElement&&[].concat(...document.body.children).forEach((t=>j.off(t,"mouseover",d))),this._activeTrigger.click=!1,this._activeTrigger.focus=!1,this._activeTrigger.hover=!1;const e=this.tip.classList.contains(nn);this._queueCallback((()=>{this._isWithActiveTrigger()||(this._hoverState!==on&&t.remove(),this._cleanTipClass(),this._element.removeAttribute("aria-describedby"),j.trigger(this._element,this.constructor.Event.HIDDEN),this._disposePopper())}),this.tip,e),this._hoverState=""}update(){null!==this._popper&&this._popper.update()}isWithContent(){return Boolean(this.getTitle())}getTipElement(){if(this.tip)return this.tip;const t=document.createElement("div");t.innerHTML=this._config.template;const e=t.children[0];return this.setContent(e),e.classList.remove(nn,sn),this.tip=e,this.tip}setContent(t){this._sanitizeAndSetContent(t,this.getTitle(),an)}_sanitizeAndSetContent(t,e,i){const n=V.findOne(i,t);e||!n?this.setElementContent(n,e):n.remove()}setElementContent(t,e){if(null!==t)return o(e)?(e=r(e),void(this._config.html?e.parentNode!==t&&(t.innerHTML="",t.append(e)):t.textContent=e.textContent)):void(this._config.html?(this._config.sanitize&&(e=Yi(e,this._config.allowList,this._config.sanitizeFn)),t.innerHTML=e):t.textContent=e)}getTitle(){const t=this._element.getAttribute("data-bs-original-title")||this._config.title;return this._resolvePossibleFunction(t)}updateAttachment(t){return"right"===t?"end":"left"===t?"start":t}_initializeOnDelegatedTarget(t,e){return e||this.constructor.getOrCreateInstance(t.delegateTarget,this._getDelegateConfig())}_getOffset(){const{offset:t}=this._config;return"string"==typeof t?t.split(",").map((t=>Number.parseInt(t,10))):"function"==typeof t?e=>t(e,this._element):t}_resolvePossibleFunction(t){return"function"==typeof t?t.call(this._element):t}_getPopperConfig(t){const e={placement:t,modifiers:[{name:"flip",options:{fallbackPlacements:this._config.fallbackPlacements}},{name:"offset",options:{offset:this._getOffset()}},{name:"preventOverflow",options:{boundary:this._config.boundary}},{name:"arrow",options:{element:`.${this.constructor.NAME}-arrow`}},{name:"onChange",enabled:!0,phase:"afterWrite",fn:t=>this._handlePopperPlacementChange(t)}],onFirstUpdate:t=>{t.options.placement!==t.placement&&this._handlePopperPlacementChange(t)}};return{...e,..."function"==typeof this._config.popperConfig?this._config.popperConfig(e):this._config.popperConfig}}_addAttachmentClass(t){this.getTipElement().classList.add(`${this._getBasicClassPrefix()}-${this.updateAttachment(t)}`)}_getAttachment(t){return Ji[t.toUpperCase()]}_setListeners(){this._config.trigger.split(" ").forEach((t=>{if("click"===t)j.on(this._element,this.constructor.Event.CLICK,this._config.selector,(t=>this.toggle(t)));else if("manual"!==t){const e=t===hn?this.constructor.Event.MOUSEENTER:this.constructor.Event.FOCUSIN,i=t===hn?this.constructor.Event.MOUSELEAVE:this.constructor.Event.FOCUSOUT;j.on(this._element,e,this._config.selector,(t=>this._enter(t))),j.on(this._element,i,this._config.selector,(t=>this._leave(t)))}})),this._hideModalHandler=()=>{this._element&&this.hide()},j.on(this._element.closest(ln),cn,this._hideModalHandler),this._config.selector?this._config={...this._config,trigger:"manual",selector:""}:this._fixTitle()}_fixTitle(){const t=this._element.getAttribute("title"),e=typeof this._element.getAttribute("data-bs-original-title");(t||"string"!==e)&&(this._element.setAttribute("data-bs-original-title",t||""),!t||this._element.getAttribute("aria-label")||this._element.textContent||this._element.setAttribute("aria-label",t),this._element.setAttribute("title",""))}_enter(t,e){e=this._initializeOnDelegatedTarget(t,e),t&&(e._activeTrigger["focusin"===t.type?dn:hn]=!0),e.getTipElement().classList.contains(sn)||e._hoverState===on?e._hoverState=on:(clearTimeout(e._timeout),e._hoverState=on,e._config.delay&&e._config.delay.show?e._timeout=setTimeout((()=>{e._hoverState===on&&e.show()}),e._config.delay.show):e.show())}_leave(t,e){e=this._initializeOnDelegatedTarget(t,e),t&&(e._activeTrigger["focusout"===t.type?dn:hn]=e._element.contains(t.relatedTarget)),e._isWithActiveTrigger()||(clearTimeout(e._timeout),e._hoverState=rn,e._config.delay&&e._config.delay.hide?e._timeout=setTimeout((()=>{e._hoverState===rn&&e.hide()}),e._config.delay.hide):e.hide())}_isWithActiveTrigger(){for(const t in this._activeTrigger)if(this._activeTrigger[t])return!0;return!1}_getConfig(t){const e=U.getDataAttributes(this._element);return Object.keys(e).forEach((t=>{Gi.has(t)&&delete e[t]})),(t={...this.constructor.Default,...e,..."object"==typeof t&&t?t:{}}).container=!1===t.container?document.body:r(t.container),"number"==typeof t.delay&&(t.delay={show:t.delay,hide:t.delay}),"number"==typeof t.title&&(t.title=t.title.toString()),"number"==typeof t.content&&(t.content=t.content.toString()),a(Qi,t,this.constructor.DefaultType),t.sanitize&&(t.template=Yi(t.template,t.allowList,t.sanitizeFn)),t}_getDelegateConfig(){const t={};for(const e in this._config)this.constructor.Default[e]!==this._config[e]&&(t[e]=this._config[e]);return t}_cleanTipClass(){const t=this.getTipElement(),e=new RegExp(`(^|\\s)${this._getBasicClassPrefix()}\\S+`,"g"),i=t.getAttribute("class").match(e);null!==i&&i.length>0&&i.map((t=>t.trim())).forEach((e=>t.classList.remove(e)))}_getBasicClassPrefix(){return"bs-tooltip"}_handlePopperPlacementChange(t){const{state:e}=t;e&&(this.tip=e.elements.popper,this._cleanTipClass(),this._addAttachmentClass(this._getAttachment(e.placement)))}_disposePopper(){this._popper&&(this._popper.destroy(),this._popper=null)}static jQueryInterface(t){return this.each((function(){const e=un.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t]()}}))}}g(un);const fn={...un.Default,placement:"right",offset:[0,8],trigger:"click",content:"",template:''},pn={...un.DefaultType,content:"(string|element|function)"},mn={HIDE:"hide.bs.popover",HIDDEN:"hidden.bs.popover",SHOW:"show.bs.popover",SHOWN:"shown.bs.popover",INSERTED:"inserted.bs.popover",CLICK:"click.bs.popover",FOCUSIN:"focusin.bs.popover",FOCUSOUT:"focusout.bs.popover",MOUSEENTER:"mouseenter.bs.popover",MOUSELEAVE:"mouseleave.bs.popover"};class gn extends un{static get Default(){return fn}static get NAME(){return"popover"}static get Event(){return mn}static get DefaultType(){return pn}isWithContent(){return this.getTitle()||this._getContent()}setContent(t){this._sanitizeAndSetContent(t,this.getTitle(),".popover-header"),this._sanitizeAndSetContent(t,this._getContent(),".popover-body")}_getContent(){return this._resolvePossibleFunction(this._config.content)}_getBasicClassPrefix(){return"bs-popover"}static jQueryInterface(t){return this.each((function(){const e=gn.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t]()}}))}}g(gn);const _n="scrollspy",bn={offset:10,method:"auto",target:""},vn={offset:"number",method:"string",target:"(string|element)"},yn="active",wn=".nav-link, .list-group-item, .dropdown-item",En="position";class An extends B{constructor(t,e){super(t),this._scrollElement="BODY"===this._element.tagName?window:this._element,this._config=this._getConfig(e),this._offsets=[],this._targets=[],this._activeTarget=null,this._scrollHeight=0,j.on(this._scrollElement,"scroll.bs.scrollspy",(()=>this._process())),this.refresh(),this._process()}static get Default(){return bn}static get NAME(){return _n}refresh(){const t=this._scrollElement===this._scrollElement.window?"offset":En,e="auto"===this._config.method?t:this._config.method,n=e===En?this._getScrollTop():0;this._offsets=[],this._targets=[],this._scrollHeight=this._getScrollHeight(),V.find(wn,this._config.target).map((t=>{const s=i(t),o=s?V.findOne(s):null;if(o){const t=o.getBoundingClientRect();if(t.width||t.height)return[U[e](o).top+n,s]}return null})).filter((t=>t)).sort(((t,e)=>t[0]-e[0])).forEach((t=>{this._offsets.push(t[0]),this._targets.push(t[1])}))}dispose(){j.off(this._scrollElement,".bs.scrollspy"),super.dispose()}_getConfig(t){return(t={...bn,...U.getDataAttributes(this._element),..."object"==typeof t&&t?t:{}}).target=r(t.target)||document.documentElement,a(_n,t,vn),t}_getScrollTop(){return this._scrollElement===window?this._scrollElement.pageYOffset:this._scrollElement.scrollTop}_getScrollHeight(){return this._scrollElement.scrollHeight||Math.max(document.body.scrollHeight,document.documentElement.scrollHeight)}_getOffsetHeight(){return this._scrollElement===window?window.innerHeight:this._scrollElement.getBoundingClientRect().height}_process(){const t=this._getScrollTop()+this._config.offset,e=this._getScrollHeight(),i=this._config.offset+e-this._getOffsetHeight();if(this._scrollHeight!==e&&this.refresh(),t>=i){const t=this._targets[this._targets.length-1];this._activeTarget!==t&&this._activate(t)}else{if(this._activeTarget&&t0)return this._activeTarget=null,void this._clear();for(let e=this._offsets.length;e--;)this._activeTarget!==this._targets[e]&&t>=this._offsets[e]&&(void 0===this._offsets[e+1]||t`${e}[data-bs-target="${t}"],${e}[href="${t}"]`)),i=V.findOne(e.join(","),this._config.target);i.classList.add(yn),i.classList.contains("dropdown-item")?V.findOne(".dropdown-toggle",i.closest(".dropdown")).classList.add(yn):V.parents(i,".nav, .list-group").forEach((t=>{V.prev(t,".nav-link, .list-group-item").forEach((t=>t.classList.add(yn))),V.prev(t,".nav-item").forEach((t=>{V.children(t,".nav-link").forEach((t=>t.classList.add(yn)))}))})),j.trigger(this._scrollElement,"activate.bs.scrollspy",{relatedTarget:t})}_clear(){V.find(wn,this._config.target).filter((t=>t.classList.contains(yn))).forEach((t=>t.classList.remove(yn)))}static jQueryInterface(t){return this.each((function(){const e=An.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t]()}}))}}j.on(window,"load.bs.scrollspy.data-api",(()=>{V.find('[data-bs-spy="scroll"]').forEach((t=>new An(t)))})),g(An);const Tn="active",On="fade",Cn="show",kn=".active",Ln=":scope > li > .active";class xn extends B{static get NAME(){return"tab"}show(){if(this._element.parentNode&&this._element.parentNode.nodeType===Node.ELEMENT_NODE&&this._element.classList.contains(Tn))return;let t;const e=n(this._element),i=this._element.closest(".nav, .list-group");if(i){const e="UL"===i.nodeName||"OL"===i.nodeName?Ln:kn;t=V.find(e,i),t=t[t.length-1]}const s=t?j.trigger(t,"hide.bs.tab",{relatedTarget:this._element}):null;if(j.trigger(this._element,"show.bs.tab",{relatedTarget:t}).defaultPrevented||null!==s&&s.defaultPrevented)return;this._activate(this._element,i);const o=()=>{j.trigger(t,"hidden.bs.tab",{relatedTarget:this._element}),j.trigger(this._element,"shown.bs.tab",{relatedTarget:t})};e?this._activate(e,e.parentNode,o):o()}_activate(t,e,i){const n=(!e||"UL"!==e.nodeName&&"OL"!==e.nodeName?V.children(e,kn):V.find(Ln,e))[0],s=i&&n&&n.classList.contains(On),o=()=>this._transitionComplete(t,n,i);n&&s?(n.classList.remove(Cn),this._queueCallback(o,t,!0)):o()}_transitionComplete(t,e,i){if(e){e.classList.remove(Tn);const t=V.findOne(":scope > .dropdown-menu .active",e.parentNode);t&&t.classList.remove(Tn),"tab"===e.getAttribute("role")&&e.setAttribute("aria-selected",!1)}t.classList.add(Tn),"tab"===t.getAttribute("role")&&t.setAttribute("aria-selected",!0),u(t),t.classList.contains(On)&&t.classList.add(Cn);let n=t.parentNode;if(n&&"LI"===n.nodeName&&(n=n.parentNode),n&&n.classList.contains("dropdown-menu")){const e=t.closest(".dropdown");e&&V.find(".dropdown-toggle",e).forEach((t=>t.classList.add(Tn))),t.setAttribute("aria-expanded",!0)}i&&i()}static jQueryInterface(t){return this.each((function(){const e=xn.getOrCreateInstance(this);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t]()}}))}}j.on(document,"click.bs.tab.data-api",'[data-bs-toggle="tab"], [data-bs-toggle="pill"], [data-bs-toggle="list"]',(function(t){["A","AREA"].includes(this.tagName)&&t.preventDefault(),c(this)||xn.getOrCreateInstance(this).show()})),g(xn);const Dn="toast",Sn="hide",Nn="show",In="showing",Pn={animation:"boolean",autohide:"boolean",delay:"number"},jn={animation:!0,autohide:!0,delay:5e3};class Mn extends B{constructor(t,e){super(t),this._config=this._getConfig(e),this._timeout=null,this._hasMouseInteraction=!1,this._hasKeyboardInteraction=!1,this._setListeners()}static get DefaultType(){return Pn}static get Default(){return jn}static get NAME(){return Dn}show(){j.trigger(this._element,"show.bs.toast").defaultPrevented||(this._clearTimeout(),this._config.animation&&this._element.classList.add("fade"),this._element.classList.remove(Sn),u(this._element),this._element.classList.add(Nn),this._element.classList.add(In),this._queueCallback((()=>{this._element.classList.remove(In),j.trigger(this._element,"shown.bs.toast"),this._maybeScheduleHide()}),this._element,this._config.animation))}hide(){this._element.classList.contains(Nn)&&(j.trigger(this._element,"hide.bs.toast").defaultPrevented||(this._element.classList.add(In),this._queueCallback((()=>{this._element.classList.add(Sn),this._element.classList.remove(In),this._element.classList.remove(Nn),j.trigger(this._element,"hidden.bs.toast")}),this._element,this._config.animation)))}dispose(){this._clearTimeout(),this._element.classList.contains(Nn)&&this._element.classList.remove(Nn),super.dispose()}_getConfig(t){return t={...jn,...U.getDataAttributes(this._element),..."object"==typeof t&&t?t:{}},a(Dn,t,this.constructor.DefaultType),t}_maybeScheduleHide(){this._config.autohide&&(this._hasMouseInteraction||this._hasKeyboardInteraction||(this._timeout=setTimeout((()=>{this.hide()}),this._config.delay)))}_onInteraction(t,e){switch(t.type){case"mouseover":case"mouseout":this._hasMouseInteraction=e;break;case"focusin":case"focusout":this._hasKeyboardInteraction=e}if(e)return void this._clearTimeout();const i=t.relatedTarget;this._element===i||this._element.contains(i)||this._maybeScheduleHide()}_setListeners(){j.on(this._element,"mouseover.bs.toast",(t=>this._onInteraction(t,!0))),j.on(this._element,"mouseout.bs.toast",(t=>this._onInteraction(t,!1))),j.on(this._element,"focusin.bs.toast",(t=>this._onInteraction(t,!0))),j.on(this._element,"focusout.bs.toast",(t=>this._onInteraction(t,!1)))}_clearTimeout(){clearTimeout(this._timeout),this._timeout=null}static jQueryInterface(t){return this.each((function(){const e=Mn.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t](this)}}))}}return R(Mn),g(Mn),{Alert:W,Button:z,Carousel:st,Collapse:pt,Dropdown:hi,Modal:Hi,Offcanvas:Fi,Popover:gn,ScrollSpy:An,Tab:xn,Toast:Mn,Tooltip:un}})); +//# sourceMappingURL=bootstrap.bundle.min.js.map \ No newline at end of file diff --git a/api/install/assets/bootstrap.css b/api/install/assets/bootstrap.css new file mode 100644 index 0000000..04405c7 --- /dev/null +++ b/api/install/assets/bootstrap.css @@ -0,0 +1,10775 @@ +@charset "UTF-8"; +/* +Template Name: SeedProject Starter Framework +Author: Carlos Arias +Version: 1.0.0 +Website: https://www.carlosarias.com/ +Contact: hi@carlosarias.com +File: Custom Bootstrap Css File +*/ + +/*! + * Bootstrap v5.1.3 (https://getbootstrap.com/) + * Copyright 2011-2021 The Bootstrap Authors + * Copyright 2011-2021 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + */ + +:root { + --bs-blue: #2e86de; + --bs-indigo: #564ab1; + --bs-purple: #564ab1; + --bs-pink: #e83e8c; + --bs-red: #f34e4e; + --bs-orange: #f1734f; + --bs-yellow: #f7cc53; + --bs-green: #51d28c; + --bs-teal: #050505; + --bs-cyan: #5fd0f3; + --bs-white: #fff; + --bs-gray: #74788d; + --bs-gray-dark: #343a40; + --bs-gray-100: #f8f9fa; + --bs-gray-200: #f5f6f8; + --bs-gray-300: #eff0f2; + --bs-gray-400: #e2e5e8; + --bs-gray-500: #adb5bd; + --bs-gray-600: #74788d; + --bs-gray-700: #495057; + --bs-gray-800: #343a40; + --bs-gray-900: #212529; + --bs-primary: #2e86de; + --bs-secondary: #74788d; + --bs-success: #51d28c; + --bs-info: #5fd0f3; + --bs-warning: #f7cc53; + --bs-danger: #f34e4e; + --bs-pink: #e83e8c; + --bs-light: #f5f6f8; + --bs-dark: #343a40; + --bs-purple: #564ab1; + --bs-primary-rgb: 3, 142, 220; + --bs-secondary-rgb: 116, 120, 141; + --bs-success-rgb: 81, 210, 140; + --bs-info-rgb: 95, 208, 243; + --bs-warning-rgb: 247, 204, 83; + --bs-danger-rgb: 243, 78, 78; + --bs-pink-rgb: 232, 62, 140; + --bs-light-rgb: 245, 246, 248; + --bs-dark-rgb: 52, 58, 64; + --bs-purple-rgb: 86, 74, 177; + --bs-white-rgb: 255, 255, 255; + --bs-black-rgb: 0, 0, 0; + --bs-body-color-rgb: 73, 80, 87; + --bs-body-bg-rgb: 247, 248, 250; + --bs-font-sans-serif: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", "Liberation Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; + --bs-font-monospace: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; + --bs-gradient: linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0)); + --bs-body-font-family: system-ui, -apple-system, Segoe UI, Roboto, Helvetica Neue, Arial, Noto Sans, Liberation Sans, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol, Noto Color Emoji; + --bs-body-font-size: 0.9rem; + --bs-body-font-weight: 400; + --bs-body-line-height: 1.5; + --bs-body-color: #495057; + --bs-body-bg: #f7f8fa; } + +*, +*::before, +*::after { + -webkit-box-sizing: border-box; + box-sizing: border-box; } + +@media (prefers-reduced-motion: no-preference) { + :root { + scroll-behavior: smooth; } } + +body { + margin: 0; + font-family: var(--bs-body-font-family); + font-size: var(--bs-body-font-size); + font-weight: var(--bs-body-font-weight); + line-height: var(--bs-body-line-height); + color: var(--bs-body-color); + text-align: var(--bs-body-text-align); + background-color: var(--bs-body-bg); + -webkit-text-size-adjust: 100%; + -webkit-tap-highlight-color: rgba(0, 0, 0, 0); } + +hr { + margin: 1rem 0; + color: #adb5bd; + background-color: currentColor; + border: 0; + opacity: 0.25; } + +hr:not([size]) { + height: 1px; } + +h1, .h1, h2, .h2, h3, .h3, h4, .h4, h5, .h5, h6, .h6 { + margin-top: 0; + margin-bottom: 0.5rem; + font-weight: 500; + line-height: 1.2; } + +h1, .h1 { + font-size: calc(1.35rem + 1.2vw); } + @media (min-width: 1200px) { + h1, .h1 { + font-size: 2.25rem; } } + +h2, .h2 { + font-size: calc(1.305rem + 0.66vw); } + @media (min-width: 1200px) { + h2, .h2 { + font-size: 1.8rem; } } + +h3, .h3 { + font-size: calc(1.2825rem + 0.39vw); } + @media (min-width: 1200px) { + h3, .h3 { + font-size: 1.575rem; } } + +h4, .h4 { + font-size: calc(1.26rem + 0.12vw); } + @media (min-width: 1200px) { + h4, .h4 { + font-size: 1.35rem; } } + +h5, .h5 { + font-size: 1.125rem; } + +h6, .h6 { + font-size: 0.9rem; } + +p { + margin-top: 0; + margin-bottom: 1rem; } + +abbr[title], +abbr[data-bs-original-title] { + -webkit-text-decoration: underline dotted; + text-decoration: underline dotted; + cursor: help; + -webkit-text-decoration-skip-ink: none; + text-decoration-skip-ink: none; } + +address { + margin-bottom: 1rem; + font-style: normal; + line-height: inherit; } + +ol, +ul { + padding-left: 2rem; } + +ol, +ul, +dl { + margin-top: 0; + margin-bottom: 1rem; } + +ol ol, +ul ul, +ol ul, +ul ol { + margin-bottom: 0; } + +dt { + font-weight: 700; } + +dd { + margin-bottom: .5rem; + margin-left: 0; } + +blockquote { + margin: 0 0 1rem; } + +b, +strong { + font-weight: bolder; } + +small, .small { + font-size: 87.5%; } + +mark, .mark { + padding: 0.2em; + background-color: #fcf8e3; } + +sub, +sup { + position: relative; + font-size: 0.75em; + line-height: 0; + vertical-align: baseline; } + +sub { + bottom: -.25em; } + +sup { + top: -.5em; } + +a { + color: #2e86de; + text-decoration: none; } + a:hover { + color: #025d91; } + +a:not([href]):not([class]), a:not([href]):not([class]):hover { + color: inherit; + text-decoration: none; } + +pre, +code, +kbd, +samp { + font-family: var(--bs-font-monospace); + font-size: 1em; + direction: ltr /* rtl:ignore */; + unicode-bidi: bidi-override; } + +pre { + display: block; + margin-top: 0; + margin-bottom: 1rem; + overflow: auto; + font-size: 87.5%; } + pre code { + font-size: inherit; + color: inherit; + word-break: normal; } + +code { + font-size: 87.5%; + color: #f34e4e; + word-wrap: break-word; } + a > code { + color: inherit; } + +kbd { + padding: 0.2rem 0.4rem; + font-size: 87.5%; + color: #fff; + background-color: #212529; + border-radius: 0.2rem; } + kbd kbd { + padding: 0; + font-size: 1em; + font-weight: 700; } + +figure { + margin: 0 0 1rem; } + +img, +svg { + vertical-align: middle; } + +table { + caption-side: bottom; + border-collapse: collapse; } + +caption { + padding-top: 0.75rem; + padding-bottom: 0.75rem; + color: #74788d; + text-align: left; } + +th { + font-weight: 500; + text-align: inherit; + text-align: -webkit-match-parent; } + +thead, +tbody, +tfoot, +tr, +td, +th { + border-color: inherit; + border-style: solid; + border-width: 0; } + +label { + display: inline-block; } + +button { + border-radius: 0; } + +button:focus:not(:focus-visible) { + outline: 0; } + +input, +button, +select, +optgroup, +textarea { + margin: 0; + font-family: inherit; + font-size: inherit; + line-height: inherit; } + +button, +select { + text-transform: none; } + +[role="button"] { + cursor: pointer; } + +select { + word-wrap: normal; } + select:disabled { + opacity: 1; } + +[list]::-webkit-calendar-picker-indicator { + display: none; } + +button, +[type="button"], +[type="reset"], +[type="submit"] { + -webkit-appearance: button; } + button:not(:disabled), + [type="button"]:not(:disabled), + [type="reset"]:not(:disabled), + [type="submit"]:not(:disabled) { + cursor: pointer; } + +::-moz-focus-inner { + padding: 0; + border-style: none; } + +textarea { + resize: vertical; } + +fieldset { + min-width: 0; + padding: 0; + margin: 0; + border: 0; } + +legend { + float: left; + width: 100%; + padding: 0; + margin-bottom: 0.5rem; + font-size: calc(1.275rem + 0.3vw); + line-height: inherit; } + @media (min-width: 1200px) { + legend { + font-size: 1.5rem; } } + legend + * { + clear: left; } + +::-webkit-datetime-edit-fields-wrapper, +::-webkit-datetime-edit-text, +::-webkit-datetime-edit-minute, +::-webkit-datetime-edit-hour-field, +::-webkit-datetime-edit-day-field, +::-webkit-datetime-edit-month-field, +::-webkit-datetime-edit-year-field { + padding: 0; } + +::-webkit-inner-spin-button { + height: auto; } + +[type="search"] { + outline-offset: -2px; + -webkit-appearance: textfield; } + +/* rtl:raw: +[type="tel"], +[type="url"], +[type="email"], +[type="number"] { + direction: ltr; +} +*/ +::-webkit-search-decoration { + -webkit-appearance: none; } + +::-webkit-color-swatch-wrapper { + padding: 0; } + +::file-selector-button { + font: inherit; } + +::-webkit-file-upload-button { + font: inherit; + -webkit-appearance: button; } + +output { + display: inline-block; } + +iframe { + border: 0; } + +summary { + display: list-item; + cursor: pointer; } + +progress { + vertical-align: baseline; } + +[hidden] { + display: none !important; } + +.lead { + font-size: 1.125rem; + font-weight: 300; } + +.display-1 { + font-size: calc(1.725rem + 5.7vw); + font-weight: 300; + line-height: 1.2; } + @media (min-width: 1200px) { + .display-1 { + font-size: 6rem; } } + +.display-2 { + font-size: calc(1.675rem + 5.1vw); + font-weight: 300; + line-height: 1.2; } + @media (min-width: 1200px) { + .display-2 { + font-size: 5.5rem; } } + +.display-3 { + font-size: calc(1.575rem + 3.9vw); + font-weight: 300; + line-height: 1.2; } + @media (min-width: 1200px) { + .display-3 { + font-size: 4.5rem; } } + +.display-4 { + font-size: calc(1.475rem + 2.7vw); + font-weight: 300; + line-height: 1.2; } + @media (min-width: 1200px) { + .display-4 { + font-size: 3.5rem; } } + +.display-5 { + font-size: calc(1.425rem + 2.1vw); + font-weight: 300; + line-height: 1.2; } + @media (min-width: 1200px) { + .display-5 { + font-size: 3rem; } } + +.display-6 { + font-size: calc(1.375rem + 1.5vw); + font-weight: 300; + line-height: 1.2; } + @media (min-width: 1200px) { + .display-6 { + font-size: 2.5rem; } } + +.list-unstyled { + padding-left: 0; + list-style: none; } + +.list-inline { + padding-left: 0; + list-style: none; } + +.list-inline-item { + display: inline-block; } + .list-inline-item:not(:last-child) { + margin-right: 0.5rem; } + +.initialism { + font-size: 87.5%; + text-transform: uppercase; } + +.blockquote { + margin-bottom: 1rem; + font-size: 1.125rem; } + .blockquote > :last-child { + margin-bottom: 0; } + +.blockquote-footer { + margin-top: -1rem; + margin-bottom: 1rem; + font-size: 87.5%; + color: #74788d; } + .blockquote-footer::before { + content: "\2014\00A0"; } + +.img-fluid { + max-width: 100%; + height: auto; } + +.img-thumbnail { + padding: 0.25rem; + background-color: #f7f8fa; + border: 1px solid #eff0f2; + border-radius: 0.25rem; + max-width: 100%; + height: auto; } + +.figure { + display: inline-block; } + +.figure-img { + margin-bottom: 0.5rem; + line-height: 1; } + +.figure-caption { + font-size: 87.5%; + color: #74788d; } + +.container, +.container-fluid, +.container-sm, +.container-md, +.container-lg, +.container-xl, +.container-xxl { + width: 100%; + padding-right: var(--bs-gutter-x, 10px); + padding-left: var(--bs-gutter-x, 10px); + margin-right: auto; + margin-left: auto; } + +@media (min-width: 576px) { + .container, .container-sm { + max-width: 540px; } } + +@media (min-width: 768px) { + .container, .container-sm, .container-md { + max-width: 720px; } } + +@media (min-width: 992px) { + .container, .container-sm, .container-md, .container-lg { + max-width: 960px; } } + +@media (min-width: 1200px) { + .container, .container-sm, .container-md, .container-lg, .container-xl { + max-width: 1140px; } } + +@media (min-width: 1400px) { + .container, .container-sm, .container-md, .container-lg, .container-xl, .container-xxl { + max-width: 1320px; } } + +.row { + --bs-gutter-x: 20px; + --bs-gutter-y: 0; + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -ms-flex-wrap: wrap; + flex-wrap: wrap; + margin-top: calc(-1 * var(--bs-gutter-y)); + margin-right: calc(-.5 * var(--bs-gutter-x)); + margin-left: calc(-.5 * var(--bs-gutter-x)); } + .row > * { + -ms-flex-negative: 0; + flex-shrink: 0; + width: 100%; + max-width: 100%; + padding-right: calc(var(--bs-gutter-x) * .5); + padding-left: calc(var(--bs-gutter-x) * .5); + margin-top: var(--bs-gutter-y); } + +.col { + -webkit-box-flex: 1; + -ms-flex: 1 0 0%; + flex: 1 0 0%; } + +.row-cols-auto > * { + -webkit-box-flex: 0; + -ms-flex: 0 0 auto; + flex: 0 0 auto; + width: auto; } + +.row-cols-1 > * { + -webkit-box-flex: 0; + -ms-flex: 0 0 auto; + flex: 0 0 auto; + width: 100%; } + +.row-cols-2 > * { + -webkit-box-flex: 0; + -ms-flex: 0 0 auto; + flex: 0 0 auto; + width: 50%; } + +.row-cols-3 > * { + -webkit-box-flex: 0; + -ms-flex: 0 0 auto; + flex: 0 0 auto; + width: 33.33333%; } + +.row-cols-4 > * { + -webkit-box-flex: 0; + -ms-flex: 0 0 auto; + flex: 0 0 auto; + width: 25%; } + +.row-cols-5 > * { + -webkit-box-flex: 0; + -ms-flex: 0 0 auto; + flex: 0 0 auto; + width: 20%; } + +.row-cols-6 > * { + -webkit-box-flex: 0; + -ms-flex: 0 0 auto; + flex: 0 0 auto; + width: 16.66667%; } + +.col-auto { + -webkit-box-flex: 0; + -ms-flex: 0 0 auto; + flex: 0 0 auto; + width: auto; } + +.col-1 { + -webkit-box-flex: 0; + -ms-flex: 0 0 auto; + flex: 0 0 auto; + width: 8.33333%; } + +.col-2 { + -webkit-box-flex: 0; + -ms-flex: 0 0 auto; + flex: 0 0 auto; + width: 16.66667%; } + +.col-3 { + -webkit-box-flex: 0; + -ms-flex: 0 0 auto; + flex: 0 0 auto; + width: 25%; } + +.col-4 { + -webkit-box-flex: 0; + -ms-flex: 0 0 auto; + flex: 0 0 auto; + width: 33.33333%; } + +.col-5 { + -webkit-box-flex: 0; + -ms-flex: 0 0 auto; + flex: 0 0 auto; + width: 41.66667%; } + +.col-6 { + -webkit-box-flex: 0; + -ms-flex: 0 0 auto; + flex: 0 0 auto; + width: 50%; } + +.col-7 { + -webkit-box-flex: 0; + -ms-flex: 0 0 auto; + flex: 0 0 auto; + width: 58.33333%; } + +.col-8 { + -webkit-box-flex: 0; + -ms-flex: 0 0 auto; + flex: 0 0 auto; + width: 66.66667%; } + +.col-9 { + -webkit-box-flex: 0; + -ms-flex: 0 0 auto; + flex: 0 0 auto; + width: 75%; } + +.col-10 { + -webkit-box-flex: 0; + -ms-flex: 0 0 auto; + flex: 0 0 auto; + width: 83.33333%; } + +.col-11 { + -webkit-box-flex: 0; + -ms-flex: 0 0 auto; + flex: 0 0 auto; + width: 91.66667%; } + +.col-12 { + -webkit-box-flex: 0; + -ms-flex: 0 0 auto; + flex: 0 0 auto; + width: 100%; } + +.offset-1 { + margin-left: 8.33333%; } + +.offset-2 { + margin-left: 16.66667%; } + +.offset-3 { + margin-left: 25%; } + +.offset-4 { + margin-left: 33.33333%; } + +.offset-5 { + margin-left: 41.66667%; } + +.offset-6 { + margin-left: 50%; } + +.offset-7 { + margin-left: 58.33333%; } + +.offset-8 { + margin-left: 66.66667%; } + +.offset-9 { + margin-left: 75%; } + +.offset-10 { + margin-left: 83.33333%; } + +.offset-11 { + margin-left: 91.66667%; } + +.g-0, +.gx-0 { + --bs-gutter-x: 0; } + +.g-0, +.gy-0 { + --bs-gutter-y: 0; } + +.g-1, +.gx-1 { + --bs-gutter-x: 0.25rem; } + +.g-1, +.gy-1 { + --bs-gutter-y: 0.25rem; } + +.g-2, +.gx-2 { + --bs-gutter-x: 0.5rem; } + +.g-2, +.gy-2 { + --bs-gutter-y: 0.5rem; } + +.g-3, +.gx-3 { + --bs-gutter-x: 1rem; } + +.g-3, +.gy-3 { + --bs-gutter-y: 1rem; } + +.g-4, +.gx-4 { + --bs-gutter-x: 1.5rem; } + +.g-4, +.gy-4 { + --bs-gutter-y: 1.5rem; } + +.g-5, +.gx-5 { + --bs-gutter-x: 3rem; } + +.g-5, +.gy-5 { + --bs-gutter-y: 3rem; } + +@media (min-width: 576px) { + .col-sm { + -webkit-box-flex: 1; + -ms-flex: 1 0 0%; + flex: 1 0 0%; } + .row-cols-sm-auto > * { + -webkit-box-flex: 0; + -ms-flex: 0 0 auto; + flex: 0 0 auto; + width: auto; } + .row-cols-sm-1 > * { + -webkit-box-flex: 0; + -ms-flex: 0 0 auto; + flex: 0 0 auto; + width: 100%; } + .row-cols-sm-2 > * { + -webkit-box-flex: 0; + -ms-flex: 0 0 auto; + flex: 0 0 auto; + width: 50%; } + .row-cols-sm-3 > * { + -webkit-box-flex: 0; + -ms-flex: 0 0 auto; + flex: 0 0 auto; + width: 33.33333%; } + .row-cols-sm-4 > * { + -webkit-box-flex: 0; + -ms-flex: 0 0 auto; + flex: 0 0 auto; + width: 25%; } + .row-cols-sm-5 > * { + -webkit-box-flex: 0; + -ms-flex: 0 0 auto; + flex: 0 0 auto; + width: 20%; } + .row-cols-sm-6 > * { + -webkit-box-flex: 0; + -ms-flex: 0 0 auto; + flex: 0 0 auto; + width: 16.66667%; } + .col-sm-auto { + -webkit-box-flex: 0; + -ms-flex: 0 0 auto; + flex: 0 0 auto; + width: auto; } + .col-sm-1 { + -webkit-box-flex: 0; + -ms-flex: 0 0 auto; + flex: 0 0 auto; + width: 8.33333%; } + .col-sm-2 { + -webkit-box-flex: 0; + -ms-flex: 0 0 auto; + flex: 0 0 auto; + width: 16.66667%; } + .col-sm-3 { + -webkit-box-flex: 0; + -ms-flex: 0 0 auto; + flex: 0 0 auto; + width: 25%; } + .col-sm-4 { + -webkit-box-flex: 0; + -ms-flex: 0 0 auto; + flex: 0 0 auto; + width: 33.33333%; } + .col-sm-5 { + -webkit-box-flex: 0; + -ms-flex: 0 0 auto; + flex: 0 0 auto; + width: 41.66667%; } + .col-sm-6 { + -webkit-box-flex: 0; + -ms-flex: 0 0 auto; + flex: 0 0 auto; + width: 50%; } + .col-sm-7 { + -webkit-box-flex: 0; + -ms-flex: 0 0 auto; + flex: 0 0 auto; + width: 58.33333%; } + .col-sm-8 { + -webkit-box-flex: 0; + -ms-flex: 0 0 auto; + flex: 0 0 auto; + width: 66.66667%; } + .col-sm-9 { + -webkit-box-flex: 0; + -ms-flex: 0 0 auto; + flex: 0 0 auto; + width: 75%; } + .col-sm-10 { + -webkit-box-flex: 0; + -ms-flex: 0 0 auto; + flex: 0 0 auto; + width: 83.33333%; } + .col-sm-11 { + -webkit-box-flex: 0; + -ms-flex: 0 0 auto; + flex: 0 0 auto; + width: 91.66667%; } + .col-sm-12 { + -webkit-box-flex: 0; + -ms-flex: 0 0 auto; + flex: 0 0 auto; + width: 100%; } + .offset-sm-0 { + margin-left: 0; } + .offset-sm-1 { + margin-left: 8.33333%; } + .offset-sm-2 { + margin-left: 16.66667%; } + .offset-sm-3 { + margin-left: 25%; } + .offset-sm-4 { + margin-left: 33.33333%; } + .offset-sm-5 { + margin-left: 41.66667%; } + .offset-sm-6 { + margin-left: 50%; } + .offset-sm-7 { + margin-left: 58.33333%; } + .offset-sm-8 { + margin-left: 66.66667%; } + .offset-sm-9 { + margin-left: 75%; } + .offset-sm-10 { + margin-left: 83.33333%; } + .offset-sm-11 { + margin-left: 91.66667%; } + .g-sm-0, + .gx-sm-0 { + --bs-gutter-x: 0; } + .g-sm-0, + .gy-sm-0 { + --bs-gutter-y: 0; } + .g-sm-1, + .gx-sm-1 { + --bs-gutter-x: 0.25rem; } + .g-sm-1, + .gy-sm-1 { + --bs-gutter-y: 0.25rem; } + .g-sm-2, + .gx-sm-2 { + --bs-gutter-x: 0.5rem; } + .g-sm-2, + .gy-sm-2 { + --bs-gutter-y: 0.5rem; } + .g-sm-3, + .gx-sm-3 { + --bs-gutter-x: 1rem; } + .g-sm-3, + .gy-sm-3 { + --bs-gutter-y: 1rem; } + .g-sm-4, + .gx-sm-4 { + --bs-gutter-x: 1.5rem; } + .g-sm-4, + .gy-sm-4 { + --bs-gutter-y: 1.5rem; } + .g-sm-5, + .gx-sm-5 { + --bs-gutter-x: 3rem; } + .g-sm-5, + .gy-sm-5 { + --bs-gutter-y: 3rem; } } + +@media (min-width: 768px) { + .col-md { + -webkit-box-flex: 1; + -ms-flex: 1 0 0%; + flex: 1 0 0%; } + .row-cols-md-auto > * { + -webkit-box-flex: 0; + -ms-flex: 0 0 auto; + flex: 0 0 auto; + width: auto; } + .row-cols-md-1 > * { + -webkit-box-flex: 0; + -ms-flex: 0 0 auto; + flex: 0 0 auto; + width: 100%; } + .row-cols-md-2 > * { + -webkit-box-flex: 0; + -ms-flex: 0 0 auto; + flex: 0 0 auto; + width: 50%; } + .row-cols-md-3 > * { + -webkit-box-flex: 0; + -ms-flex: 0 0 auto; + flex: 0 0 auto; + width: 33.33333%; } + .row-cols-md-4 > * { + -webkit-box-flex: 0; + -ms-flex: 0 0 auto; + flex: 0 0 auto; + width: 25%; } + .row-cols-md-5 > * { + -webkit-box-flex: 0; + -ms-flex: 0 0 auto; + flex: 0 0 auto; + width: 20%; } + .row-cols-md-6 > * { + -webkit-box-flex: 0; + -ms-flex: 0 0 auto; + flex: 0 0 auto; + width: 16.66667%; } + .col-md-auto { + -webkit-box-flex: 0; + -ms-flex: 0 0 auto; + flex: 0 0 auto; + width: auto; } + .col-md-1 { + -webkit-box-flex: 0; + -ms-flex: 0 0 auto; + flex: 0 0 auto; + width: 8.33333%; } + .col-md-2 { + -webkit-box-flex: 0; + -ms-flex: 0 0 auto; + flex: 0 0 auto; + width: 16.66667%; } + .col-md-3 { + -webkit-box-flex: 0; + -ms-flex: 0 0 auto; + flex: 0 0 auto; + width: 25%; } + .col-md-4 { + -webkit-box-flex: 0; + -ms-flex: 0 0 auto; + flex: 0 0 auto; + width: 33.33333%; } + .col-md-5 { + -webkit-box-flex: 0; + -ms-flex: 0 0 auto; + flex: 0 0 auto; + width: 41.66667%; } + .col-md-6 { + -webkit-box-flex: 0; + -ms-flex: 0 0 auto; + flex: 0 0 auto; + width: 50%; } + .col-md-7 { + -webkit-box-flex: 0; + -ms-flex: 0 0 auto; + flex: 0 0 auto; + width: 58.33333%; } + .col-md-8 { + -webkit-box-flex: 0; + -ms-flex: 0 0 auto; + flex: 0 0 auto; + width: 66.66667%; } + .col-md-9 { + -webkit-box-flex: 0; + -ms-flex: 0 0 auto; + flex: 0 0 auto; + width: 75%; } + .col-md-10 { + -webkit-box-flex: 0; + -ms-flex: 0 0 auto; + flex: 0 0 auto; + width: 83.33333%; } + .col-md-11 { + -webkit-box-flex: 0; + -ms-flex: 0 0 auto; + flex: 0 0 auto; + width: 91.66667%; } + .col-md-12 { + -webkit-box-flex: 0; + -ms-flex: 0 0 auto; + flex: 0 0 auto; + width: 100%; } + .offset-md-0 { + margin-left: 0; } + .offset-md-1 { + margin-left: 8.33333%; } + .offset-md-2 { + margin-left: 16.66667%; } + .offset-md-3 { + margin-left: 25%; } + .offset-md-4 { + margin-left: 33.33333%; } + .offset-md-5 { + margin-left: 41.66667%; } + .offset-md-6 { + margin-left: 50%; } + .offset-md-7 { + margin-left: 58.33333%; } + .offset-md-8 { + margin-left: 66.66667%; } + .offset-md-9 { + margin-left: 75%; } + .offset-md-10 { + margin-left: 83.33333%; } + .offset-md-11 { + margin-left: 91.66667%; } + .g-md-0, + .gx-md-0 { + --bs-gutter-x: 0; } + .g-md-0, + .gy-md-0 { + --bs-gutter-y: 0; } + .g-md-1, + .gx-md-1 { + --bs-gutter-x: 0.25rem; } + .g-md-1, + .gy-md-1 { + --bs-gutter-y: 0.25rem; } + .g-md-2, + .gx-md-2 { + --bs-gutter-x: 0.5rem; } + .g-md-2, + .gy-md-2 { + --bs-gutter-y: 0.5rem; } + .g-md-3, + .gx-md-3 { + --bs-gutter-x: 1rem; } + .g-md-3, + .gy-md-3 { + --bs-gutter-y: 1rem; } + .g-md-4, + .gx-md-4 { + --bs-gutter-x: 1.5rem; } + .g-md-4, + .gy-md-4 { + --bs-gutter-y: 1.5rem; } + .g-md-5, + .gx-md-5 { + --bs-gutter-x: 3rem; } + .g-md-5, + .gy-md-5 { + --bs-gutter-y: 3rem; } } + +@media (min-width: 992px) { + .col-lg { + -webkit-box-flex: 1; + -ms-flex: 1 0 0%; + flex: 1 0 0%; } + .row-cols-lg-auto > * { + -webkit-box-flex: 0; + -ms-flex: 0 0 auto; + flex: 0 0 auto; + width: auto; } + .row-cols-lg-1 > * { + -webkit-box-flex: 0; + -ms-flex: 0 0 auto; + flex: 0 0 auto; + width: 100%; } + .row-cols-lg-2 > * { + -webkit-box-flex: 0; + -ms-flex: 0 0 auto; + flex: 0 0 auto; + width: 50%; } + .row-cols-lg-3 > * { + -webkit-box-flex: 0; + -ms-flex: 0 0 auto; + flex: 0 0 auto; + width: 33.33333%; } + .row-cols-lg-4 > * { + -webkit-box-flex: 0; + -ms-flex: 0 0 auto; + flex: 0 0 auto; + width: 25%; } + .row-cols-lg-5 > * { + -webkit-box-flex: 0; + -ms-flex: 0 0 auto; + flex: 0 0 auto; + width: 20%; } + .row-cols-lg-6 > * { + -webkit-box-flex: 0; + -ms-flex: 0 0 auto; + flex: 0 0 auto; + width: 16.66667%; } + .col-lg-auto { + -webkit-box-flex: 0; + -ms-flex: 0 0 auto; + flex: 0 0 auto; + width: auto; } + .col-lg-1 { + -webkit-box-flex: 0; + -ms-flex: 0 0 auto; + flex: 0 0 auto; + width: 8.33333%; } + .col-lg-2 { + -webkit-box-flex: 0; + -ms-flex: 0 0 auto; + flex: 0 0 auto; + width: 16.66667%; } + .col-lg-3 { + -webkit-box-flex: 0; + -ms-flex: 0 0 auto; + flex: 0 0 auto; + width: 25%; } + .col-lg-4 { + -webkit-box-flex: 0; + -ms-flex: 0 0 auto; + flex: 0 0 auto; + width: 33.33333%; } + .col-lg-5 { + -webkit-box-flex: 0; + -ms-flex: 0 0 auto; + flex: 0 0 auto; + width: 41.66667%; } + .col-lg-6 { + -webkit-box-flex: 0; + -ms-flex: 0 0 auto; + flex: 0 0 auto; + width: 50%; } + .col-lg-7 { + -webkit-box-flex: 0; + -ms-flex: 0 0 auto; + flex: 0 0 auto; + width: 58.33333%; } + .col-lg-8 { + -webkit-box-flex: 0; + -ms-flex: 0 0 auto; + flex: 0 0 auto; + width: 66.66667%; } + .col-lg-9 { + -webkit-box-flex: 0; + -ms-flex: 0 0 auto; + flex: 0 0 auto; + width: 75%; } + .col-lg-10 { + -webkit-box-flex: 0; + -ms-flex: 0 0 auto; + flex: 0 0 auto; + width: 83.33333%; } + .col-lg-11 { + -webkit-box-flex: 0; + -ms-flex: 0 0 auto; + flex: 0 0 auto; + width: 91.66667%; } + .col-lg-12 { + -webkit-box-flex: 0; + -ms-flex: 0 0 auto; + flex: 0 0 auto; + width: 100%; } + .offset-lg-0 { + margin-left: 0; } + .offset-lg-1 { + margin-left: 8.33333%; } + .offset-lg-2 { + margin-left: 16.66667%; } + .offset-lg-3 { + margin-left: 25%; } + .offset-lg-4 { + margin-left: 33.33333%; } + .offset-lg-5 { + margin-left: 41.66667%; } + .offset-lg-6 { + margin-left: 50%; } + .offset-lg-7 { + margin-left: 58.33333%; } + .offset-lg-8 { + margin-left: 66.66667%; } + .offset-lg-9 { + margin-left: 75%; } + .offset-lg-10 { + margin-left: 83.33333%; } + .offset-lg-11 { + margin-left: 91.66667%; } + .g-lg-0, + .gx-lg-0 { + --bs-gutter-x: 0; } + .g-lg-0, + .gy-lg-0 { + --bs-gutter-y: 0; } + .g-lg-1, + .gx-lg-1 { + --bs-gutter-x: 0.25rem; } + .g-lg-1, + .gy-lg-1 { + --bs-gutter-y: 0.25rem; } + .g-lg-2, + .gx-lg-2 { + --bs-gutter-x: 0.5rem; } + .g-lg-2, + .gy-lg-2 { + --bs-gutter-y: 0.5rem; } + .g-lg-3, + .gx-lg-3 { + --bs-gutter-x: 1rem; } + .g-lg-3, + .gy-lg-3 { + --bs-gutter-y: 1rem; } + .g-lg-4, + .gx-lg-4 { + --bs-gutter-x: 1.5rem; } + .g-lg-4, + .gy-lg-4 { + --bs-gutter-y: 1.5rem; } + .g-lg-5, + .gx-lg-5 { + --bs-gutter-x: 3rem; } + .g-lg-5, + .gy-lg-5 { + --bs-gutter-y: 3rem; } } + +@media (min-width: 1200px) { + .col-xl { + -webkit-box-flex: 1; + -ms-flex: 1 0 0%; + flex: 1 0 0%; } + .row-cols-xl-auto > * { + -webkit-box-flex: 0; + -ms-flex: 0 0 auto; + flex: 0 0 auto; + width: auto; } + .row-cols-xl-1 > * { + -webkit-box-flex: 0; + -ms-flex: 0 0 auto; + flex: 0 0 auto; + width: 100%; } + .row-cols-xl-2 > * { + -webkit-box-flex: 0; + -ms-flex: 0 0 auto; + flex: 0 0 auto; + width: 50%; } + .row-cols-xl-3 > * { + -webkit-box-flex: 0; + -ms-flex: 0 0 auto; + flex: 0 0 auto; + width: 33.33333%; } + .row-cols-xl-4 > * { + -webkit-box-flex: 0; + -ms-flex: 0 0 auto; + flex: 0 0 auto; + width: 25%; } + .row-cols-xl-5 > * { + -webkit-box-flex: 0; + -ms-flex: 0 0 auto; + flex: 0 0 auto; + width: 20%; } + .row-cols-xl-6 > * { + -webkit-box-flex: 0; + -ms-flex: 0 0 auto; + flex: 0 0 auto; + width: 16.66667%; } + .col-xl-auto { + -webkit-box-flex: 0; + -ms-flex: 0 0 auto; + flex: 0 0 auto; + width: auto; } + .col-xl-1 { + -webkit-box-flex: 0; + -ms-flex: 0 0 auto; + flex: 0 0 auto; + width: 8.33333%; } + .col-xl-2 { + -webkit-box-flex: 0; + -ms-flex: 0 0 auto; + flex: 0 0 auto; + width: 16.66667%; } + .col-xl-3 { + -webkit-box-flex: 0; + -ms-flex: 0 0 auto; + flex: 0 0 auto; + width: 25%; } + .col-xl-4 { + -webkit-box-flex: 0; + -ms-flex: 0 0 auto; + flex: 0 0 auto; + width: 33.33333%; } + .col-xl-5 { + -webkit-box-flex: 0; + -ms-flex: 0 0 auto; + flex: 0 0 auto; + width: 41.66667%; } + .col-xl-6 { + -webkit-box-flex: 0; + -ms-flex: 0 0 auto; + flex: 0 0 auto; + width: 50%; } + .col-xl-7 { + -webkit-box-flex: 0; + -ms-flex: 0 0 auto; + flex: 0 0 auto; + width: 58.33333%; } + .col-xl-8 { + -webkit-box-flex: 0; + -ms-flex: 0 0 auto; + flex: 0 0 auto; + width: 66.66667%; } + .col-xl-9 { + -webkit-box-flex: 0; + -ms-flex: 0 0 auto; + flex: 0 0 auto; + width: 75%; } + .col-xl-10 { + -webkit-box-flex: 0; + -ms-flex: 0 0 auto; + flex: 0 0 auto; + width: 83.33333%; } + .col-xl-11 { + -webkit-box-flex: 0; + -ms-flex: 0 0 auto; + flex: 0 0 auto; + width: 91.66667%; } + .col-xl-12 { + -webkit-box-flex: 0; + -ms-flex: 0 0 auto; + flex: 0 0 auto; + width: 100%; } + .offset-xl-0 { + margin-left: 0; } + .offset-xl-1 { + margin-left: 8.33333%; } + .offset-xl-2 { + margin-left: 16.66667%; } + .offset-xl-3 { + margin-left: 25%; } + .offset-xl-4 { + margin-left: 33.33333%; } + .offset-xl-5 { + margin-left: 41.66667%; } + .offset-xl-6 { + margin-left: 50%; } + .offset-xl-7 { + margin-left: 58.33333%; } + .offset-xl-8 { + margin-left: 66.66667%; } + .offset-xl-9 { + margin-left: 75%; } + .offset-xl-10 { + margin-left: 83.33333%; } + .offset-xl-11 { + margin-left: 91.66667%; } + .g-xl-0, + .gx-xl-0 { + --bs-gutter-x: 0; } + .g-xl-0, + .gy-xl-0 { + --bs-gutter-y: 0; } + .g-xl-1, + .gx-xl-1 { + --bs-gutter-x: 0.25rem; } + .g-xl-1, + .gy-xl-1 { + --bs-gutter-y: 0.25rem; } + .g-xl-2, + .gx-xl-2 { + --bs-gutter-x: 0.5rem; } + .g-xl-2, + .gy-xl-2 { + --bs-gutter-y: 0.5rem; } + .g-xl-3, + .gx-xl-3 { + --bs-gutter-x: 1rem; } + .g-xl-3, + .gy-xl-3 { + --bs-gutter-y: 1rem; } + .g-xl-4, + .gx-xl-4 { + --bs-gutter-x: 1.5rem; } + .g-xl-4, + .gy-xl-4 { + --bs-gutter-y: 1.5rem; } + .g-xl-5, + .gx-xl-5 { + --bs-gutter-x: 3rem; } + .g-xl-5, + .gy-xl-5 { + --bs-gutter-y: 3rem; } } + +@media (min-width: 1400px) { + .col-xxl { + -webkit-box-flex: 1; + -ms-flex: 1 0 0%; + flex: 1 0 0%; } + .row-cols-xxl-auto > * { + -webkit-box-flex: 0; + -ms-flex: 0 0 auto; + flex: 0 0 auto; + width: auto; } + .row-cols-xxl-1 > * { + -webkit-box-flex: 0; + -ms-flex: 0 0 auto; + flex: 0 0 auto; + width: 100%; } + .row-cols-xxl-2 > * { + -webkit-box-flex: 0; + -ms-flex: 0 0 auto; + flex: 0 0 auto; + width: 50%; } + .row-cols-xxl-3 > * { + -webkit-box-flex: 0; + -ms-flex: 0 0 auto; + flex: 0 0 auto; + width: 33.33333%; } + .row-cols-xxl-4 > * { + -webkit-box-flex: 0; + -ms-flex: 0 0 auto; + flex: 0 0 auto; + width: 25%; } + .row-cols-xxl-5 > * { + -webkit-box-flex: 0; + -ms-flex: 0 0 auto; + flex: 0 0 auto; + width: 20%; } + .row-cols-xxl-6 > * { + -webkit-box-flex: 0; + -ms-flex: 0 0 auto; + flex: 0 0 auto; + width: 16.66667%; } + .col-xxl-auto { + -webkit-box-flex: 0; + -ms-flex: 0 0 auto; + flex: 0 0 auto; + width: auto; } + .col-xxl-1 { + -webkit-box-flex: 0; + -ms-flex: 0 0 auto; + flex: 0 0 auto; + width: 8.33333%; } + .col-xxl-2 { + -webkit-box-flex: 0; + -ms-flex: 0 0 auto; + flex: 0 0 auto; + width: 16.66667%; } + .col-xxl-3 { + -webkit-box-flex: 0; + -ms-flex: 0 0 auto; + flex: 0 0 auto; + width: 25%; } + .col-xxl-4 { + -webkit-box-flex: 0; + -ms-flex: 0 0 auto; + flex: 0 0 auto; + width: 33.33333%; } + .col-xxl-5 { + -webkit-box-flex: 0; + -ms-flex: 0 0 auto; + flex: 0 0 auto; + width: 41.66667%; } + .col-xxl-6 { + -webkit-box-flex: 0; + -ms-flex: 0 0 auto; + flex: 0 0 auto; + width: 50%; } + .col-xxl-7 { + -webkit-box-flex: 0; + -ms-flex: 0 0 auto; + flex: 0 0 auto; + width: 58.33333%; } + .col-xxl-8 { + -webkit-box-flex: 0; + -ms-flex: 0 0 auto; + flex: 0 0 auto; + width: 66.66667%; } + .col-xxl-9 { + -webkit-box-flex: 0; + -ms-flex: 0 0 auto; + flex: 0 0 auto; + width: 75%; } + .col-xxl-10 { + -webkit-box-flex: 0; + -ms-flex: 0 0 auto; + flex: 0 0 auto; + width: 83.33333%; } + .col-xxl-11 { + -webkit-box-flex: 0; + -ms-flex: 0 0 auto; + flex: 0 0 auto; + width: 91.66667%; } + .col-xxl-12 { + -webkit-box-flex: 0; + -ms-flex: 0 0 auto; + flex: 0 0 auto; + width: 100%; } + .offset-xxl-0 { + margin-left: 0; } + .offset-xxl-1 { + margin-left: 8.33333%; } + .offset-xxl-2 { + margin-left: 16.66667%; } + .offset-xxl-3 { + margin-left: 25%; } + .offset-xxl-4 { + margin-left: 33.33333%; } + .offset-xxl-5 { + margin-left: 41.66667%; } + .offset-xxl-6 { + margin-left: 50%; } + .offset-xxl-7 { + margin-left: 58.33333%; } + .offset-xxl-8 { + margin-left: 66.66667%; } + .offset-xxl-9 { + margin-left: 75%; } + .offset-xxl-10 { + margin-left: 83.33333%; } + .offset-xxl-11 { + margin-left: 91.66667%; } + .g-xxl-0, + .gx-xxl-0 { + --bs-gutter-x: 0; } + .g-xxl-0, + .gy-xxl-0 { + --bs-gutter-y: 0; } + .g-xxl-1, + .gx-xxl-1 { + --bs-gutter-x: 0.25rem; } + .g-xxl-1, + .gy-xxl-1 { + --bs-gutter-y: 0.25rem; } + .g-xxl-2, + .gx-xxl-2 { + --bs-gutter-x: 0.5rem; } + .g-xxl-2, + .gy-xxl-2 { + --bs-gutter-y: 0.5rem; } + .g-xxl-3, + .gx-xxl-3 { + --bs-gutter-x: 1rem; } + .g-xxl-3, + .gy-xxl-3 { + --bs-gutter-y: 1rem; } + .g-xxl-4, + .gx-xxl-4 { + --bs-gutter-x: 1.5rem; } + .g-xxl-4, + .gy-xxl-4 { + --bs-gutter-y: 1.5rem; } + .g-xxl-5, + .gx-xxl-5 { + --bs-gutter-x: 3rem; } + .g-xxl-5, + .gy-xxl-5 { + --bs-gutter-y: 3rem; } } + +.table { + --bs-table-bg: transparent; + --bs-table-accent-bg: transparent; + --bs-table-striped-color: #495057; + --bs-table-striped-bg: #f8f9fa; + --bs-table-active-color: #495057; + --bs-table-active-bg: rgba(0, 0, 0, 0.1); + --bs-table-hover-color: #495057; + --bs-table-hover-bg: #f8f9fa; + width: 100%; + margin-bottom: 1rem; + color: #495057; + vertical-align: top; + border-color: #eff0f2; } + .table > :not(caption) > * > * { + padding: 0.75rem 0.75rem; + background-color: var(--bs-table-bg); + border-bottom-width: 1px; + -webkit-box-shadow: inset 0 0 0 9999px var(--bs-table-accent-bg); + box-shadow: inset 0 0 0 9999px var(--bs-table-accent-bg); } + .table > tbody { + vertical-align: inherit; } + .table > thead { + vertical-align: bottom; } + .table > :not(:first-child) { + border-top: 2px solid #eff0f2; } + +.caption-top { + caption-side: top; } + +.table-sm > :not(caption) > * > * { + padding: 0.25rem 0.25rem; } + +.table-bordered > :not(caption) > * { + border-width: 1px 0; } + .table-bordered > :not(caption) > * > * { + border-width: 0 1px; } + +.table-borderless > :not(caption) > * > * { + border-bottom-width: 0; } + +.table-borderless > :not(:first-child) { + border-top-width: 0; } + +.table-striped > tbody > tr:nth-of-type(odd) > * { + --bs-table-accent-bg: var(--bs-table-striped-bg); + color: var(--bs-table-striped-color); } + +.table-active { + --bs-table-accent-bg: var(--bs-table-active-bg); + color: var(--bs-table-active-color); } + +.table-hover > tbody > tr:hover > * { + --bs-table-accent-bg: var(--bs-table-hover-bg); + color: var(--bs-table-hover-color); } + +.table-primary { + --bs-table-bg: #cde8f8; + --bs-table-striped-bg: #c3dcec; + --bs-table-striped-color: #000; + --bs-table-active-bg: #b9d1df; + --bs-table-active-color: #000; + --bs-table-hover-bg: #bed7e5; + --bs-table-hover-color: #000; + color: #000; + border-color: #b9d1df; } + +.table-secondary { + --bs-table-bg: #e3e4e8; + --bs-table-striped-bg: #d8d9dc; + --bs-table-striped-color: #000; + --bs-table-active-bg: #cccdd1; + --bs-table-active-color: #000; + --bs-table-hover-bg: #d2d3d7; + --bs-table-hover-color: #000; + color: #000; + border-color: #cccdd1; } + +.table-success { + --bs-table-bg: #dcf6e8; + --bs-table-striped-bg: #d1eadc; + --bs-table-striped-color: #000; + --bs-table-active-bg: #c6ddd1; + --bs-table-active-color: #000; + --bs-table-hover-bg: #cce4d7; + --bs-table-hover-color: #000; + color: #000; + border-color: #c6ddd1; } + +.table-info { + --bs-table-bg: #dff6fd; + --bs-table-striped-bg: #d4eaf0; + --bs-table-striped-color: #000; + --bs-table-active-bg: #c9dde4; + --bs-table-active-color: #000; + --bs-table-hover-bg: #cee4ea; + --bs-table-hover-color: #000; + color: #000; + border-color: #c9dde4; } + +.table-warning { + --bs-table-bg: #fdf5dd; + --bs-table-striped-bg: #f0e9d2; + --bs-table-striped-color: #000; + --bs-table-active-bg: #e4ddc7; + --bs-table-active-color: #000; + --bs-table-hover-bg: #eae3cc; + --bs-table-hover-color: #000; + color: #000; + border-color: #e4ddc7; } + +.table-danger { + --bs-table-bg: #fddcdc; + --bs-table-striped-bg: #f0d1d1; + --bs-table-striped-color: #000; + --bs-table-active-bg: #e4c6c6; + --bs-table-active-color: #000; + --bs-table-hover-bg: #eacccc; + --bs-table-hover-color: #000; + color: #000; + border-color: #e4c6c6; } + +.table-light { + --bs-table-bg: #f5f6f8; + --bs-table-striped-bg: #e9eaec; + --bs-table-striped-color: #000; + --bs-table-active-bg: #dddddf; + --bs-table-active-color: #000; + --bs-table-hover-bg: #e3e4e5; + --bs-table-hover-color: #000; + color: #000; + border-color: #dddddf; } + +.table-dark { + --bs-table-bg: #343a40; + --bs-table-striped-bg: #3e444a; + --bs-table-striped-color: #fff; + --bs-table-active-bg: #484e53; + --bs-table-active-color: #fff; + --bs-table-hover-bg: #43494e; + --bs-table-hover-color: #fff; + color: #fff; + border-color: #484e53; } + +.table-responsive { + overflow-x: auto; + -webkit-overflow-scrolling: touch; } + +@media (max-width: 575.98px) { + .table-responsive-sm { + overflow-x: auto; + -webkit-overflow-scrolling: touch; } } + +@media (max-width: 767.98px) { + .table-responsive-md { + overflow-x: auto; + -webkit-overflow-scrolling: touch; } } + +@media (max-width: 991.98px) { + .table-responsive-lg { + overflow-x: auto; + -webkit-overflow-scrolling: touch; } } + +@media (max-width: 1199.98px) { + .table-responsive-xl { + overflow-x: auto; + -webkit-overflow-scrolling: touch; } } + +@media (max-width: 1399.98px) { + .table-responsive-xxl { + overflow-x: auto; + -webkit-overflow-scrolling: touch; } } + +.form-label { + margin-bottom: 0.5rem; } + +.col-form-label { + padding-top: calc(0.47rem + 1px); + padding-bottom: calc(0.47rem + 1px); + margin-bottom: 0; + font-size: inherit; + line-height: 1.5; } + +.col-form-label-lg { + padding-top: calc(0.5rem + 1px); + padding-bottom: calc(0.5rem + 1px); + font-size: 1.125rem; } + +.col-form-label-sm { + padding-top: calc(0.25rem + 1px); + padding-bottom: calc(0.25rem + 1px); + font-size: 0.7875rem; } + +.form-text { + margin-top: 0.25rem; + font-size: 87.5%; + color: #74788d; } + +.form-control { + display: block; + width: 100%; + padding: 0.47rem 0.75rem; + font-size: 0.9rem; + font-weight: 400; + line-height: 1.5; + color: #495057; + background-color: #fff; + background-clip: padding-box; + border: 1px solid #e2e5e8; + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + border-radius: 0.25rem; + -webkit-transition: border-color 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out; + transition: border-color 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out; + transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; + transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out; } + @media (prefers-reduced-motion: reduce) { + .form-control { + -webkit-transition: none; + transition: none; } } + .form-control[type="file"] { + overflow: hidden; } + .form-control[type="file"]:not(:disabled):not([readonly]) { + cursor: pointer; } + .form-control:focus { + color: #495057; + background-color: #fff; + border-color: #cbced1; + outline: 0; + -webkit-box-shadow: none; + box-shadow: none; } + .form-control::-webkit-date-and-time-value { + height: 1.5em; } + .form-control::-webkit-input-placeholder { + color: #74788d; + opacity: 1; } + .form-control::-moz-placeholder { + color: #74788d; + opacity: 1; } + .form-control:-ms-input-placeholder { + color: #74788d; + opacity: 1; } + .form-control::-ms-input-placeholder { + color: #74788d; + opacity: 1; } + .form-control::placeholder { + color: #74788d; + opacity: 1; } + .form-control:disabled, .form-control[readonly] { + background-color: #f5f6f8; + opacity: 1; } + .form-control::file-selector-button { + padding: 0.47rem 0.75rem; + margin: -0.47rem -0.75rem; + -webkit-margin-end: 0.75rem; + margin-inline-end: 0.75rem; + color: #495057; + background-color: #f5f6f8; + pointer-events: none; + border-color: inherit; + border-style: solid; + border-width: 0; + border-inline-end-width: 1px; + border-radius: 0; + -webkit-transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out; + transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out; + transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; + transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out; } + @media (prefers-reduced-motion: reduce) { + .form-control::file-selector-button { + -webkit-transition: none; + transition: none; } } + .form-control:hover:not(:disabled):not([readonly])::file-selector-button { + background-color: #e9eaec; } + .form-control::-webkit-file-upload-button { + padding: 0.47rem 0.75rem; + margin: -0.47rem -0.75rem; + -webkit-margin-end: 0.75rem; + margin-inline-end: 0.75rem; + color: #495057; + background-color: #f5f6f8; + pointer-events: none; + border-color: inherit; + border-style: solid; + border-width: 0; + border-inline-end-width: 1px; + border-radius: 0; + -webkit-transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out; + transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out; + transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; + transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out; } + @media (prefers-reduced-motion: reduce) { + .form-control::-webkit-file-upload-button { + -webkit-transition: none; + transition: none; } } + .form-control:hover:not(:disabled):not([readonly])::-webkit-file-upload-button { + background-color: #e9eaec; } + +.form-control-plaintext { + display: block; + width: 100%; + padding: 0.47rem 0; + margin-bottom: 0; + line-height: 1.5; + color: #495057; + background-color: transparent; + border: solid transparent; + border-width: 1px 0; } + .form-control-plaintext.form-control-sm, .form-control-plaintext.form-control-lg { + padding-right: 0; + padding-left: 0; } + +.form-control-sm { + min-height: calc(1.5em + 0.5rem + 2px); + padding: 0.25rem 0.5rem; + font-size: 0.7875rem; + border-radius: 0.2rem; } + .form-control-sm::file-selector-button { + padding: 0.25rem 0.5rem; + margin: -0.25rem -0.5rem; + -webkit-margin-end: 0.5rem; + margin-inline-end: 0.5rem; } + .form-control-sm::-webkit-file-upload-button { + padding: 0.25rem 0.5rem; + margin: -0.25rem -0.5rem; + -webkit-margin-end: 0.5rem; + margin-inline-end: 0.5rem; } + +.form-control-lg { + min-height: calc(1.5em + 1rem + 2px); + padding: 0.5rem 1rem; + font-size: 1.125rem; + border-radius: 0.4rem; } + .form-control-lg::file-selector-button { + padding: 0.5rem 1rem; + margin: -0.5rem -1rem; + -webkit-margin-end: 1rem; + margin-inline-end: 1rem; } + .form-control-lg::-webkit-file-upload-button { + padding: 0.5rem 1rem; + margin: -0.5rem -1rem; + -webkit-margin-end: 1rem; + margin-inline-end: 1rem; } + +textarea.form-control { + min-height: calc(1.5em + 0.94rem + 2px); } + +textarea.form-control-sm { + min-height: calc(1.5em + 0.5rem + 2px); } + +textarea.form-control-lg { + min-height: calc(1.5em + 1rem + 2px); } + +.form-control-color { + width: 3rem; + height: auto; + padding: 0.47rem; } + .form-control-color:not(:disabled):not([readonly]) { + cursor: pointer; } + .form-control-color::-moz-color-swatch { + height: 1.5em; + border-radius: 0.25rem; } + .form-control-color::-webkit-color-swatch { + height: 1.5em; + border-radius: 0.25rem; } + +.form-select { + display: block; + width: 100%; + padding: 0.47rem 1.75rem 0.47rem 0.75rem; + -moz-padding-start: calc(0.75rem - 3px); + font-size: 0.9rem; + font-weight: 400; + line-height: 1.5; + color: #495057; + background-color: #fff; + background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23343a40' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M2 5l6 6 6-6'/%3e%3c/svg%3e"); + background-repeat: no-repeat; + background-position: right 0.75rem center; + background-size: 16px 12px; + border: 1px solid #e2e5e8; + border-radius: 0.25rem; + -webkit-transition: border-color 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out; + transition: border-color 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out; + transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; + transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out; + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; } + @media (prefers-reduced-motion: reduce) { + .form-select { + -webkit-transition: none; + transition: none; } } + .form-select:focus { + border-color: #cbced1; + outline: 0; + -webkit-box-shadow: 0 0 0 0.15rem rgba(3, 142, 220, 0.25); + box-shadow: 0 0 0 0.15rem rgba(3, 142, 220, 0.25); } + .form-select[multiple], .form-select[size]:not([size="1"]) { + padding-right: 0.75rem; + background-image: none; } + .form-select:disabled { + color: #74788d; + background-color: #f5f6f8; } + .form-select:-moz-focusring { + color: transparent; + text-shadow: 0 0 0 #495057; } + +.form-select-sm { + padding-top: 0.25rem; + padding-bottom: 0.25rem; + padding-left: 0.5rem; + font-size: 0.7875rem; + border-radius: 0.2rem; } + +.form-select-lg { + padding-top: 0.5rem; + padding-bottom: 0.5rem; + padding-left: 1rem; + font-size: 1.125rem; + border-radius: 0.3rem; } + +.form-check { + display: block; + min-height: 1.35rem; + padding-left: 1.5em; + margin-bottom: 0rem; } + .form-check .form-check-input { + float: left; + margin-left: -1.5em; } + +.form-check-input { + width: 1em; + height: 1em; + margin-top: 0.25em; + vertical-align: top; + background-color: #fff; + background-repeat: no-repeat; + background-position: center; + background-size: contain; + border: 1px solid #adb5bd; + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + -webkit-print-color-adjust: exact; + color-adjust: exact; + -webkit-transition: background-color 0.15s ease-in-out, background-position 0.15s ease-in-out, border-color 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out; + transition: background-color 0.15s ease-in-out, background-position 0.15s ease-in-out, border-color 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out; + transition: background-color 0.15s ease-in-out, background-position 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; + transition: background-color 0.15s ease-in-out, background-position 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out; } + @media (prefers-reduced-motion: reduce) { + .form-check-input { + -webkit-transition: none; + transition: none; } } + .form-check-input[type="checkbox"] { + border-radius: 0.25em; } + .form-check-input[type="radio"] { + border-radius: 50%; } + .form-check-input:active { + -webkit-filter: brightness(90%); + filter: brightness(90%); } + .form-check-input:focus { + border-color: #cbced1; + outline: 0; + -webkit-box-shadow: none; + box-shadow: none; } + .form-check-input:checked { + background-color: #2e86de; + border-color: #2e86de; } + .form-check-input:checked[type="checkbox"] { + background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='M6 10l3 3l6-6'/%3e%3c/svg%3e"); } + .form-check-input:checked[type="radio"] { + background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='2' fill='%23fff'/%3e%3c/svg%3e"); } + .form-check-input[type="checkbox"]:indeterminate { + background-color: #2e86de; + border-color: #2e86de; + background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='M6 10h8'/%3e%3c/svg%3e"); } + .form-check-input:disabled { + pointer-events: none; + -webkit-filter: none; + filter: none; + opacity: 0.5; } + .form-check-input[disabled] ~ .form-check-label, .form-check-input:disabled ~ .form-check-label { + opacity: 0.5; } + +.form-switch { + padding-left: 2.5em; } + .form-switch .form-check-input { + width: 2em; + margin-left: -2.5em; + background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='rgba%280, 0, 0, 0.25%29'/%3e%3c/svg%3e"); + background-position: left center; + border-radius: 2em; + -webkit-transition: background-position 0.15s ease-in-out; + transition: background-position 0.15s ease-in-out; } + @media (prefers-reduced-motion: reduce) { + .form-switch .form-check-input { + -webkit-transition: none; + transition: none; } } + .form-switch .form-check-input:focus { + background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%23cbced1'/%3e%3c/svg%3e"); } + .form-switch .form-check-input:checked { + background-position: right center; + background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%23fff'/%3e%3c/svg%3e"); } + +.form-check-inline { + display: inline-block; + margin-right: 1rem; } + +.btn-check { + position: absolute; + clip: rect(0, 0, 0, 0); + pointer-events: none; } + .btn-check[disabled] + .btn, .btn-check:disabled + .btn { + pointer-events: none; + -webkit-filter: none; + filter: none; + opacity: 0.65; } + +.form-range { + width: 100%; + height: 1.3rem; + padding: 0; + background-color: transparent; + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; } + .form-range:focus { + outline: 0; } + .form-range:focus::-webkit-slider-thumb { + -webkit-box-shadow: 0 0 0 1px #f7f8fa, none; + box-shadow: 0 0 0 1px #f7f8fa, none; } + .form-range:focus::-moz-range-thumb { + box-shadow: 0 0 0 1px #f7f8fa, none; } + .form-range::-moz-focus-outer { + border: 0; } + .form-range::-webkit-slider-thumb { + width: 1rem; + height: 1rem; + margin-top: -0.25rem; + background-color: #2e86de; + border: 0; + border-radius: 1rem; + -webkit-transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out; + transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out; + transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; + transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out; + -webkit-appearance: none; + appearance: none; } + @media (prefers-reduced-motion: reduce) { + .form-range::-webkit-slider-thumb { + -webkit-transition: none; + transition: none; } } + .form-range::-webkit-slider-thumb:active { + background-color: #b3ddf5; } + .form-range::-webkit-slider-runnable-track { + width: 100%; + height: 0.5rem; + color: transparent; + cursor: pointer; + background-color: #eff0f2; + border-color: transparent; + border-radius: 1rem; } + .form-range::-moz-range-thumb { + width: 1rem; + height: 1rem; + background-color: #2e86de; + border: 0; + border-radius: 1rem; + -moz-transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; + transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; + -moz-appearance: none; + appearance: none; } + @media (prefers-reduced-motion: reduce) { + .form-range::-moz-range-thumb { + -moz-transition: none; + transition: none; } } + .form-range::-moz-range-thumb:active { + background-color: #b3ddf5; } + .form-range::-moz-range-track { + width: 100%; + height: 0.5rem; + color: transparent; + cursor: pointer; + background-color: #eff0f2; + border-color: transparent; + border-radius: 1rem; } + .form-range:disabled { + pointer-events: none; } + .form-range:disabled::-webkit-slider-thumb { + background-color: #adb5bd; } + .form-range:disabled::-moz-range-thumb { + background-color: #adb5bd; } + +.form-floating { + position: relative; } + .form-floating > .form-control, + .form-floating > .form-select { + height: calc(3.5rem + 2px); + line-height: 1.25; } + .form-floating > label { + position: absolute; + top: 0; + left: 0; + height: 100%; + padding: 1rem 0.75rem; + pointer-events: none; + border: 1px solid transparent; + -webkit-transform-origin: 0 0; + transform-origin: 0 0; + -webkit-transition: opacity 0.1s ease-in-out, -webkit-transform 0.1s ease-in-out; + transition: opacity 0.1s ease-in-out, -webkit-transform 0.1s ease-in-out; + transition: opacity 0.1s ease-in-out, transform 0.1s ease-in-out; + transition: opacity 0.1s ease-in-out, transform 0.1s ease-in-out, -webkit-transform 0.1s ease-in-out; } + @media (prefers-reduced-motion: reduce) { + .form-floating > label { + -webkit-transition: none; + transition: none; } } + .form-floating > .form-control { + padding: 1rem 0.75rem; } + .form-floating > .form-control::-webkit-input-placeholder { + color: transparent; } + .form-floating > .form-control::-moz-placeholder { + color: transparent; } + .form-floating > .form-control:-ms-input-placeholder { + color: transparent; } + .form-floating > .form-control::-ms-input-placeholder { + color: transparent; } + .form-floating > .form-control::placeholder { + color: transparent; } + .form-floating > .form-control:not(:-moz-placeholder-shown) { + padding-top: 1.625rem; + padding-bottom: 0.625rem; } + .form-floating > .form-control:not(:-ms-input-placeholder) { + padding-top: 1.625rem; + padding-bottom: 0.625rem; } + .form-floating > .form-control:focus, .form-floating > .form-control:not(:placeholder-shown) { + padding-top: 1.625rem; + padding-bottom: 0.625rem; } + .form-floating > .form-control:-webkit-autofill { + padding-top: 1.625rem; + padding-bottom: 0.625rem; } + .form-floating > .form-select { + padding-top: 1.625rem; + padding-bottom: 0.625rem; } + .form-floating > .form-control:not(:-moz-placeholder-shown) ~ label { + opacity: 0.65; + transform: scale(0.85) translateY(-0.5rem) translateX(0.15rem); } + .form-floating > .form-control:not(:-ms-input-placeholder) ~ label { + opacity: 0.65; + transform: scale(0.85) translateY(-0.5rem) translateX(0.15rem); } + .form-floating > .form-control:focus ~ label, + .form-floating > .form-control:not(:placeholder-shown) ~ label, + .form-floating > .form-select ~ label { + opacity: 0.65; + -webkit-transform: scale(0.85) translateY(-0.5rem) translateX(0.15rem); + transform: scale(0.85) translateY(-0.5rem) translateX(0.15rem); } + .form-floating > .form-control:-webkit-autofill ~ label { + opacity: 0.65; + -webkit-transform: scale(0.85) translateY(-0.5rem) translateX(0.15rem); + transform: scale(0.85) translateY(-0.5rem) translateX(0.15rem); } + +.input-group { + position: relative; + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -ms-flex-wrap: wrap; + flex-wrap: wrap; + -webkit-box-align: stretch; + -ms-flex-align: stretch; + align-items: stretch; + width: 100%; } + .input-group > .form-control, + .input-group > .form-select { + position: relative; + -webkit-box-flex: 1; + -ms-flex: 1 1 auto; + flex: 1 1 auto; + width: 1%; + min-width: 0; } + .input-group > .form-control:focus, + .input-group > .form-select:focus { + z-index: 3; } + .input-group .btn { + position: relative; + z-index: 2; } + .input-group .btn:focus { + z-index: 3; } + +.input-group-text { + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + padding: 0.47rem 0.75rem; + font-size: 0.9rem; + font-weight: 400; + line-height: 1.5; + color: #495057; + text-align: center; + white-space: nowrap; + background-color: #f5f6f8; + border: 1px solid #e2e5e8; + border-radius: 0.25rem; } + +.input-group-lg > .form-control, +.input-group-lg > .form-select, +.input-group-lg > .input-group-text, +.input-group-lg > .btn { + padding: 0.5rem 1rem; + font-size: 1.125rem; + border-radius: 0.4rem; } + +.input-group-sm > .form-control, +.input-group-sm > .form-select, +.input-group-sm > .input-group-text, +.input-group-sm > .btn { + padding: 0.25rem 0.5rem; + font-size: 0.7875rem; + border-radius: 0.2rem; } + +.input-group-lg > .form-select, +.input-group-sm > .form-select { + padding-right: 2.5rem; } + +.input-group:not(.has-validation) > :not(:last-child):not(.dropdown-toggle):not(.dropdown-menu), +.input-group:not(.has-validation) > .dropdown-toggle:nth-last-child(n + 3) { + border-top-right-radius: 0; + border-bottom-right-radius: 0; } + +.input-group.has-validation > :nth-last-child(n + 3):not(.dropdown-toggle):not(.dropdown-menu), +.input-group.has-validation > .dropdown-toggle:nth-last-child(n + 4) { + border-top-right-radius: 0; + border-bottom-right-radius: 0; } + +.input-group > :not(:first-child):not(.dropdown-menu):not(.valid-tooltip):not(.valid-feedback):not(.invalid-tooltip):not(.invalid-feedback) { + margin-left: -1px; + border-top-left-radius: 0; + border-bottom-left-radius: 0; } + +.valid-feedback { + display: none; + width: 100%; + margin-top: 0.25rem; + font-size: 87.5%; + color: #51d28c; } + +.valid-tooltip { + position: absolute; + top: 100%; + z-index: 5; + display: none; + max-width: 100%; + padding: 0.4rem 0.7rem; + margin-top: .1rem; + font-size: 0.7875rem; + color: #fff; + background-color: rgba(81, 210, 140, 0.9); + border-radius: 0.25rem; } + +.was-validated :valid ~ .valid-feedback, +.was-validated :valid ~ .valid-tooltip, +.is-valid ~ .valid-feedback, +.is-valid ~ .valid-tooltip { + display: block; } + +.was-validated .form-control:valid, .form-control.is-valid { + border-color: #51d28c; + padding-right: calc(1.5em + 0.94rem); + background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%2351d28c' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e"); + background-repeat: no-repeat; + background-position: right calc(0.375em + 0.235rem) center; + background-size: calc(0.75em + 0.47rem) calc(0.75em + 0.47rem); } + .was-validated .form-control:valid:focus, .form-control.is-valid:focus { + border-color: #51d28c; + -webkit-box-shadow: 0 0 0 0.15rem rgba(81, 210, 140, 0.25); + box-shadow: 0 0 0 0.15rem rgba(81, 210, 140, 0.25); } + +.was-validated textarea.form-control:valid, textarea.form-control.is-valid { + padding-right: calc(1.5em + 0.94rem); + background-position: top calc(0.375em + 0.235rem) right calc(0.375em + 0.235rem); } + +.was-validated .form-select:valid, .form-select.is-valid { + border-color: #51d28c; } + .was-validated .form-select:valid:not([multiple]):not([size]), .was-validated .form-select:valid:not([multiple])[size="1"], .form-select.is-valid:not([multiple]):not([size]), .form-select.is-valid:not([multiple])[size="1"] { + padding-right: calc(0.75em + 3.205rem); + background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23343a40' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M2 5l6 6 6-6'/%3e%3c/svg%3e"), url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%2351d28c' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e"); + background-position: right 0.75rem center, center right 2.5rem; + background-size: 16px 12px, calc(0.75em + 0.47rem) calc(0.75em + 0.47rem); } + .was-validated .form-select:valid:focus, .form-select.is-valid:focus { + border-color: #51d28c; + -webkit-box-shadow: 0 0 0 0.15rem rgba(81, 210, 140, 0.25); + box-shadow: 0 0 0 0.15rem rgba(81, 210, 140, 0.25); } + +.was-validated .form-check-input:valid, .form-check-input.is-valid { + border-color: #51d28c; } + .was-validated .form-check-input:valid:checked, .form-check-input.is-valid:checked { + background-color: #51d28c; } + .was-validated .form-check-input:valid:focus, .form-check-input.is-valid:focus { + -webkit-box-shadow: 0 0 0 0.15rem rgba(81, 210, 140, 0.25); + box-shadow: 0 0 0 0.15rem rgba(81, 210, 140, 0.25); } + .was-validated .form-check-input:valid ~ .form-check-label, .form-check-input.is-valid ~ .form-check-label { + color: #51d28c; } + +.form-check-inline .form-check-input ~ .valid-feedback { + margin-left: .5em; } + +.was-validated .input-group .form-control:valid, .input-group .form-control.is-valid, .was-validated +.input-group .form-select:valid, +.input-group .form-select.is-valid { + z-index: 1; } + .was-validated .input-group .form-control:valid:focus, .input-group .form-control.is-valid:focus, .was-validated + .input-group .form-select:valid:focus, + .input-group .form-select.is-valid:focus { + z-index: 3; } + +.invalid-feedback { + display: none; + width: 100%; + margin-top: 0.25rem; + font-size: 87.5%; + color: #f34e4e; } + +.invalid-tooltip { + position: absolute; + top: 100%; + z-index: 5; + display: none; + max-width: 100%; + padding: 0.4rem 0.7rem; + margin-top: .1rem; + font-size: 0.7875rem; + color: #fff; + background-color: rgba(243, 78, 78, 0.9); + border-radius: 0.25rem; } + +.was-validated :invalid ~ .invalid-feedback, +.was-validated :invalid ~ .invalid-tooltip, +.is-invalid ~ .invalid-feedback, +.is-invalid ~ .invalid-tooltip { + display: block; } + +.was-validated .form-control:invalid, .form-control.is-invalid { + border-color: #f34e4e; + padding-right: calc(1.5em + 0.94rem); + background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23f34e4e'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23f34e4e' stroke='none'/%3e%3c/svg%3e"); + background-repeat: no-repeat; + background-position: right calc(0.375em + 0.235rem) center; + background-size: calc(0.75em + 0.47rem) calc(0.75em + 0.47rem); } + .was-validated .form-control:invalid:focus, .form-control.is-invalid:focus { + border-color: #f34e4e; + -webkit-box-shadow: 0 0 0 0.15rem rgba(243, 78, 78, 0.25); + box-shadow: 0 0 0 0.15rem rgba(243, 78, 78, 0.25); } + +.was-validated textarea.form-control:invalid, textarea.form-control.is-invalid { + padding-right: calc(1.5em + 0.94rem); + background-position: top calc(0.375em + 0.235rem) right calc(0.375em + 0.235rem); } + +.was-validated .form-select:invalid, .form-select.is-invalid { + border-color: #f34e4e; } + .was-validated .form-select:invalid:not([multiple]):not([size]), .was-validated .form-select:invalid:not([multiple])[size="1"], .form-select.is-invalid:not([multiple]):not([size]), .form-select.is-invalid:not([multiple])[size="1"] { + padding-right: calc(0.75em + 3.205rem); + background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23343a40' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M2 5l6 6 6-6'/%3e%3c/svg%3e"), url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23f34e4e'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23f34e4e' stroke='none'/%3e%3c/svg%3e"); + background-position: right 0.75rem center, center right 2.5rem; + background-size: 16px 12px, calc(0.75em + 0.47rem) calc(0.75em + 0.47rem); } + .was-validated .form-select:invalid:focus, .form-select.is-invalid:focus { + border-color: #f34e4e; + -webkit-box-shadow: 0 0 0 0.15rem rgba(243, 78, 78, 0.25); + box-shadow: 0 0 0 0.15rem rgba(243, 78, 78, 0.25); } + +.was-validated .form-check-input:invalid, .form-check-input.is-invalid { + border-color: #f34e4e; } + .was-validated .form-check-input:invalid:checked, .form-check-input.is-invalid:checked { + background-color: #f34e4e; } + .was-validated .form-check-input:invalid:focus, .form-check-input.is-invalid:focus { + -webkit-box-shadow: 0 0 0 0.15rem rgba(243, 78, 78, 0.25); + box-shadow: 0 0 0 0.15rem rgba(243, 78, 78, 0.25); } + .was-validated .form-check-input:invalid ~ .form-check-label, .form-check-input.is-invalid ~ .form-check-label { + color: #f34e4e; } + +.form-check-inline .form-check-input ~ .invalid-feedback { + margin-left: .5em; } + +.was-validated .input-group .form-control:invalid, .input-group .form-control.is-invalid, .was-validated +.input-group .form-select:invalid, +.input-group .form-select.is-invalid { + z-index: 2; } + .was-validated .input-group .form-control:invalid:focus, .input-group .form-control.is-invalid:focus, .was-validated + .input-group .form-select:invalid:focus, + .input-group .form-select.is-invalid:focus { + z-index: 3; } + +.btn { + display: inline-block; + font-weight: 400; + line-height: 1.5; + color: #495057; + text-align: center; + vertical-align: middle; + cursor: pointer; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + background-color: transparent; + border: 1px solid transparent; + padding: 0.47rem 0.75rem; + font-size: 0.9rem; + border-radius: 0.25rem; + -webkit-transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out; + transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out; + transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; + transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out; } + @media (prefers-reduced-motion: reduce) { + .btn { + -webkit-transition: none; + transition: none; } } + .btn:hover { + color: #495057; } + .btn-check:focus + .btn, .btn:focus { + outline: 0; + -webkit-box-shadow: 0 0 0 0.15rem rgba(3, 142, 220, 0.25); + box-shadow: 0 0 0 0.15rem rgba(3, 142, 220, 0.25); } + .btn:disabled, .btn.disabled, + fieldset:disabled .btn { + pointer-events: none; + opacity: 0.65; } + +.btn-primary { + color: #fff; + background-color: #2e86de; + border-color: #2e86de; } + .btn-primary:hover { + color: #fff; + background-color: #0379bb; + border-color: #0272b0; } + .btn-check:focus + .btn-primary, .btn-primary:focus { + color: #fff; + background-color: #0379bb; + border-color: #0272b0; + -webkit-box-shadow: 0 0 0 0.15rem rgba(41, 159, 225, 0.5); + box-shadow: 0 0 0 0.15rem rgba(41, 159, 225, 0.5); } + .btn-check:checked + .btn-primary, + .btn-check:active + .btn-primary, .btn-primary:active, .btn-primary.active, + .show > .btn-primary.dropdown-toggle { + color: #fff; + background-color: #0272b0; + border-color: #026ba5; } + .btn-check:checked + .btn-primary:focus, + .btn-check:active + .btn-primary:focus, .btn-primary:active:focus, .btn-primary.active:focus, + .show > .btn-primary.dropdown-toggle:focus { + -webkit-box-shadow: 0 0 0 0.15rem rgba(41, 159, 225, 0.5); + box-shadow: 0 0 0 0.15rem rgba(41, 159, 225, 0.5); } + .btn-primary:disabled, .btn-primary.disabled { + color: #fff; + background-color: #2e86de; + border-color: #2e86de; } + +.btn-secondary { + color: #fff; + background-color: #74788d; + border-color: #74788d; } + .btn-secondary:hover { + color: #fff; + background-color: #636678; + border-color: #5d6071; } + .btn-check:focus + .btn-secondary, .btn-secondary:focus { + color: #fff; + background-color: #636678; + border-color: #5d6071; + -webkit-box-shadow: 0 0 0 0.15rem rgba(137, 140, 158, 0.5); + box-shadow: 0 0 0 0.15rem rgba(137, 140, 158, 0.5); } + .btn-check:checked + .btn-secondary, + .btn-check:active + .btn-secondary, .btn-secondary:active, .btn-secondary.active, + .show > .btn-secondary.dropdown-toggle { + color: #fff; + background-color: #5d6071; + border-color: #575a6a; } + .btn-check:checked + .btn-secondary:focus, + .btn-check:active + .btn-secondary:focus, .btn-secondary:active:focus, .btn-secondary.active:focus, + .show > .btn-secondary.dropdown-toggle:focus { + -webkit-box-shadow: 0 0 0 0.15rem rgba(137, 140, 158, 0.5); + box-shadow: 0 0 0 0.15rem rgba(137, 140, 158, 0.5); } + .btn-secondary:disabled, .btn-secondary.disabled { + color: #fff; + background-color: #74788d; + border-color: #74788d; } + +.btn-success { + color: #fff; + background-color: #51d28c; + border-color: #51d28c; } + .btn-success:hover { + color: #fff; + background-color: #45b377; + border-color: #41a870; } + .btn-check:focus + .btn-success, .btn-success:focus { + color: #fff; + background-color: #45b377; + border-color: #41a870; + -webkit-box-shadow: 0 0 0 0.15rem rgba(107, 217, 157, 0.5); + box-shadow: 0 0 0 0.15rem rgba(107, 217, 157, 0.5); } + .btn-check:checked + .btn-success, + .btn-check:active + .btn-success, .btn-success:active, .btn-success.active, + .show > .btn-success.dropdown-toggle { + color: #fff; + background-color: #41a870; + border-color: #3d9e69; } + .btn-check:checked + .btn-success:focus, + .btn-check:active + .btn-success:focus, .btn-success:active:focus, .btn-success.active:focus, + .show > .btn-success.dropdown-toggle:focus { + -webkit-box-shadow: 0 0 0 0.15rem rgba(107, 217, 157, 0.5); + box-shadow: 0 0 0 0.15rem rgba(107, 217, 157, 0.5); } + .btn-success:disabled, .btn-success.disabled { + color: #fff; + background-color: #51d28c; + border-color: #51d28c; } + +.btn-info { + color: #fff; + background-color: #5fd0f3; + border-color: #5fd0f3; } + .btn-info:hover { + color: #fff; + background-color: #51b1cf; + border-color: #4ca6c2; } + .btn-check:focus + .btn-info, .btn-info:focus { + color: #fff; + background-color: #51b1cf; + border-color: #4ca6c2; + -webkit-box-shadow: 0 0 0 0.15rem rgba(119, 215, 245, 0.5); + box-shadow: 0 0 0 0.15rem rgba(119, 215, 245, 0.5); } + .btn-check:checked + .btn-info, + .btn-check:active + .btn-info, .btn-info:active, .btn-info.active, + .show > .btn-info.dropdown-toggle { + color: #fff; + background-color: #4ca6c2; + border-color: #479cb6; } + .btn-check:checked + .btn-info:focus, + .btn-check:active + .btn-info:focus, .btn-info:active:focus, .btn-info.active:focus, + .show > .btn-info.dropdown-toggle:focus { + -webkit-box-shadow: 0 0 0 0.15rem rgba(119, 215, 245, 0.5); + box-shadow: 0 0 0 0.15rem rgba(119, 215, 245, 0.5); } + .btn-info:disabled, .btn-info.disabled { + color: #fff; + background-color: #5fd0f3; + border-color: #5fd0f3; } + +.btn-warning { + color: #000; + background-color: #f7cc53; + border-color: #f7cc53; } + .btn-warning:hover { + color: #000; + background-color: #f8d46d; + border-color: #f8d164; } + .btn-check:focus + .btn-warning, .btn-warning:focus { + color: #000; + background-color: #f8d46d; + border-color: #f8d164; + -webkit-box-shadow: 0 0 0 0.15rem rgba(210, 173, 71, 0.5); + box-shadow: 0 0 0 0.15rem rgba(210, 173, 71, 0.5); } + .btn-check:checked + .btn-warning, + .btn-check:active + .btn-warning, .btn-warning:active, .btn-warning.active, + .show > .btn-warning.dropdown-toggle { + color: #000; + background-color: #f9d675; + border-color: #f8d164; } + .btn-check:checked + .btn-warning:focus, + .btn-check:active + .btn-warning:focus, .btn-warning:active:focus, .btn-warning.active:focus, + .show > .btn-warning.dropdown-toggle:focus { + -webkit-box-shadow: 0 0 0 0.15rem rgba(210, 173, 71, 0.5); + box-shadow: 0 0 0 0.15rem rgba(210, 173, 71, 0.5); } + .btn-warning:disabled, .btn-warning.disabled { + color: #000; + background-color: #f7cc53; + border-color: #f7cc53; } + +.btn-danger { + color: #fff; + background-color: #f34e4e; + border-color: #f34e4e; } + .btn-danger:hover { + color: #fff; + background-color: #cf4242; + border-color: #c23e3e; } + .btn-check:focus + .btn-danger, .btn-danger:focus { + color: #fff; + background-color: #cf4242; + border-color: #c23e3e; + -webkit-box-shadow: 0 0 0 0.15rem rgba(245, 105, 105, 0.5); + box-shadow: 0 0 0 0.15rem rgba(245, 105, 105, 0.5); } + .btn-check:checked + .btn-danger, + .btn-check:active + .btn-danger, .btn-danger:active, .btn-danger.active, + .show > .btn-danger.dropdown-toggle { + color: #fff; + background-color: #c23e3e; + border-color: #b63b3b; } + .btn-check:checked + .btn-danger:focus, + .btn-check:active + .btn-danger:focus, .btn-danger:active:focus, .btn-danger.active:focus, + .show > .btn-danger.dropdown-toggle:focus { + -webkit-box-shadow: 0 0 0 0.15rem rgba(245, 105, 105, 0.5); + box-shadow: 0 0 0 0.15rem rgba(245, 105, 105, 0.5); } + .btn-danger:disabled, .btn-danger.disabled { + color: #fff; + background-color: #f34e4e; + border-color: #f34e4e; } + +.btn-pink { + color: #fff; + background-color: #e83e8c; + border-color: #e83e8c; } + .btn-pink:hover { + color: #fff; + background-color: #c53577; + border-color: #ba3270; } + .btn-check:focus + .btn-pink, .btn-pink:focus { + color: #fff; + background-color: #c53577; + border-color: #ba3270; + -webkit-box-shadow: 0 0 0 0.15rem rgba(235, 91, 157, 0.5); + box-shadow: 0 0 0 0.15rem rgba(235, 91, 157, 0.5); } + .btn-check:checked + .btn-pink, + .btn-check:active + .btn-pink, .btn-pink:active, .btn-pink.active, + .show > .btn-pink.dropdown-toggle { + color: #fff; + background-color: #ba3270; + border-color: #ae2f69; } + .btn-check:checked + .btn-pink:focus, + .btn-check:active + .btn-pink:focus, .btn-pink:active:focus, .btn-pink.active:focus, + .show > .btn-pink.dropdown-toggle:focus { + -webkit-box-shadow: 0 0 0 0.15rem rgba(235, 91, 157, 0.5); + box-shadow: 0 0 0 0.15rem rgba(235, 91, 157, 0.5); } + .btn-pink:disabled, .btn-pink.disabled { + color: #fff; + background-color: #e83e8c; + border-color: #e83e8c; } + +.btn-light { + color: #000; + background-color: #f5f6f8; + border-color: #f5f6f8; } + .btn-light:hover { + color: #000; + background-color: #f7f7f9; + border-color: #f6f7f9; } + .btn-check:focus + .btn-light, .btn-light:focus { + color: #000; + background-color: #f7f7f9; + border-color: #f6f7f9; + -webkit-box-shadow: 0 0 0 0.15rem rgba(208, 209, 211, 0.5); + box-shadow: 0 0 0 0.15rem rgba(208, 209, 211, 0.5); } + .btn-check:checked + .btn-light, + .btn-check:active + .btn-light, .btn-light:active, .btn-light.active, + .show > .btn-light.dropdown-toggle { + color: #000; + background-color: #f7f8f9; + border-color: #f6f7f9; } + .btn-check:checked + .btn-light:focus, + .btn-check:active + .btn-light:focus, .btn-light:active:focus, .btn-light.active:focus, + .show > .btn-light.dropdown-toggle:focus { + -webkit-box-shadow: 0 0 0 0.15rem rgba(208, 209, 211, 0.5); + box-shadow: 0 0 0 0.15rem rgba(208, 209, 211, 0.5); } + .btn-light:disabled, .btn-light.disabled { + color: #000; + background-color: #f5f6f8; + border-color: #f5f6f8; } + +.btn-dark { + color: #fff; + background-color: #343a40; + border-color: #343a40; } + .btn-dark:hover { + color: #fff; + background-color: #2c3136; + border-color: #2a2e33; } + .btn-check:focus + .btn-dark, .btn-dark:focus { + color: #fff; + background-color: #2c3136; + border-color: #2a2e33; + -webkit-box-shadow: 0 0 0 0.15rem rgba(82, 88, 93, 0.5); + box-shadow: 0 0 0 0.15rem rgba(82, 88, 93, 0.5); } + .btn-check:checked + .btn-dark, + .btn-check:active + .btn-dark, .btn-dark:active, .btn-dark.active, + .show > .btn-dark.dropdown-toggle { + color: #fff; + background-color: #2a2e33; + border-color: #272c30; } + .btn-check:checked + .btn-dark:focus, + .btn-check:active + .btn-dark:focus, .btn-dark:active:focus, .btn-dark.active:focus, + .show > .btn-dark.dropdown-toggle:focus { + -webkit-box-shadow: 0 0 0 0.15rem rgba(82, 88, 93, 0.5); + box-shadow: 0 0 0 0.15rem rgba(82, 88, 93, 0.5); } + .btn-dark:disabled, .btn-dark.disabled { + color: #fff; + background-color: #343a40; + border-color: #343a40; } + +.btn-purple { + color: #fff; + background-color: #564ab1; + border-color: #564ab1; } + .btn-purple:hover { + color: #fff; + background-color: #493f96; + border-color: #453b8e; } + .btn-check:focus + .btn-purple, .btn-purple:focus { + color: #fff; + background-color: #493f96; + border-color: #453b8e; + -webkit-box-shadow: 0 0 0 0.15rem rgba(111, 101, 189, 0.5); + box-shadow: 0 0 0 0.15rem rgba(111, 101, 189, 0.5); } + .btn-check:checked + .btn-purple, + .btn-check:active + .btn-purple, .btn-purple:active, .btn-purple.active, + .show > .btn-purple.dropdown-toggle { + color: #fff; + background-color: #453b8e; + border-color: #413885; } + .btn-check:checked + .btn-purple:focus, + .btn-check:active + .btn-purple:focus, .btn-purple:active:focus, .btn-purple.active:focus, + .show > .btn-purple.dropdown-toggle:focus { + -webkit-box-shadow: 0 0 0 0.15rem rgba(111, 101, 189, 0.5); + box-shadow: 0 0 0 0.15rem rgba(111, 101, 189, 0.5); } + .btn-purple:disabled, .btn-purple.disabled { + color: #fff; + background-color: #564ab1; + border-color: #564ab1; } + +.btn-outline-primary { + color: #2e86de; + border-color: #2e86de; } + .btn-outline-primary:hover { + color: #fff; + background-color: #2e86de; + border-color: #2e86de; } + .btn-check:focus + .btn-outline-primary, .btn-outline-primary:focus { + -webkit-box-shadow: 0 0 0 0.15rem rgba(3, 142, 220, 0.5); + box-shadow: 0 0 0 0.15rem rgba(3, 142, 220, 0.5); } + .btn-check:checked + .btn-outline-primary, + .btn-check:active + .btn-outline-primary, .btn-outline-primary:active, .btn-outline-primary.active, .btn-outline-primary.dropdown-toggle.show { + color: #fff; + background-color: #2e86de; + border-color: #2e86de; } + .btn-check:checked + .btn-outline-primary:focus, + .btn-check:active + .btn-outline-primary:focus, .btn-outline-primary:active:focus, .btn-outline-primary.active:focus, .btn-outline-primary.dropdown-toggle.show:focus { + -webkit-box-shadow: 0 0 0 0.15rem rgba(3, 142, 220, 0.5); + box-shadow: 0 0 0 0.15rem rgba(3, 142, 220, 0.5); } + .btn-outline-primary:disabled, .btn-outline-primary.disabled { + color: #2e86de; + background-color: transparent; } + +.btn-outline-secondary { + color: #74788d; + border-color: #74788d; } + .btn-outline-secondary:hover { + color: #fff; + background-color: #74788d; + border-color: #74788d; } + .btn-check:focus + .btn-outline-secondary, .btn-outline-secondary:focus { + -webkit-box-shadow: 0 0 0 0.15rem rgba(116, 120, 141, 0.5); + box-shadow: 0 0 0 0.15rem rgba(116, 120, 141, 0.5); } + .btn-check:checked + .btn-outline-secondary, + .btn-check:active + .btn-outline-secondary, .btn-outline-secondary:active, .btn-outline-secondary.active, .btn-outline-secondary.dropdown-toggle.show { + color: #fff; + background-color: #74788d; + border-color: #74788d; } + .btn-check:checked + .btn-outline-secondary:focus, + .btn-check:active + .btn-outline-secondary:focus, .btn-outline-secondary:active:focus, .btn-outline-secondary.active:focus, .btn-outline-secondary.dropdown-toggle.show:focus { + -webkit-box-shadow: 0 0 0 0.15rem rgba(116, 120, 141, 0.5); + box-shadow: 0 0 0 0.15rem rgba(116, 120, 141, 0.5); } + .btn-outline-secondary:disabled, .btn-outline-secondary.disabled { + color: #74788d; + background-color: transparent; } + +.btn-outline-success { + color: #51d28c; + border-color: #51d28c; } + .btn-outline-success:hover { + color: #fff; + background-color: #51d28c; + border-color: #51d28c; } + .btn-check:focus + .btn-outline-success, .btn-outline-success:focus { + -webkit-box-shadow: 0 0 0 0.15rem rgba(81, 210, 140, 0.5); + box-shadow: 0 0 0 0.15rem rgba(81, 210, 140, 0.5); } + .btn-check:checked + .btn-outline-success, + .btn-check:active + .btn-outline-success, .btn-outline-success:active, .btn-outline-success.active, .btn-outline-success.dropdown-toggle.show { + color: #fff; + background-color: #51d28c; + border-color: #51d28c; } + .btn-check:checked + .btn-outline-success:focus, + .btn-check:active + .btn-outline-success:focus, .btn-outline-success:active:focus, .btn-outline-success.active:focus, .btn-outline-success.dropdown-toggle.show:focus { + -webkit-box-shadow: 0 0 0 0.15rem rgba(81, 210, 140, 0.5); + box-shadow: 0 0 0 0.15rem rgba(81, 210, 140, 0.5); } + .btn-outline-success:disabled, .btn-outline-success.disabled { + color: #51d28c; + background-color: transparent; } + +.btn-outline-info { + color: #5fd0f3; + border-color: #5fd0f3; } + .btn-outline-info:hover { + color: #fff; + background-color: #5fd0f3; + border-color: #5fd0f3; } + .btn-check:focus + .btn-outline-info, .btn-outline-info:focus { + -webkit-box-shadow: 0 0 0 0.15rem rgba(95, 208, 243, 0.5); + box-shadow: 0 0 0 0.15rem rgba(95, 208, 243, 0.5); } + .btn-check:checked + .btn-outline-info, + .btn-check:active + .btn-outline-info, .btn-outline-info:active, .btn-outline-info.active, .btn-outline-info.dropdown-toggle.show { + color: #fff; + background-color: #5fd0f3; + border-color: #5fd0f3; } + .btn-check:checked + .btn-outline-info:focus, + .btn-check:active + .btn-outline-info:focus, .btn-outline-info:active:focus, .btn-outline-info.active:focus, .btn-outline-info.dropdown-toggle.show:focus { + -webkit-box-shadow: 0 0 0 0.15rem rgba(95, 208, 243, 0.5); + box-shadow: 0 0 0 0.15rem rgba(95, 208, 243, 0.5); } + .btn-outline-info:disabled, .btn-outline-info.disabled { + color: #5fd0f3; + background-color: transparent; } + +.btn-outline-warning { + color: #f7cc53; + border-color: #f7cc53; } + .btn-outline-warning:hover { + color: #000; + background-color: #f7cc53; + border-color: #f7cc53; } + .btn-check:focus + .btn-outline-warning, .btn-outline-warning:focus { + -webkit-box-shadow: 0 0 0 0.15rem rgba(247, 204, 83, 0.5); + box-shadow: 0 0 0 0.15rem rgba(247, 204, 83, 0.5); } + .btn-check:checked + .btn-outline-warning, + .btn-check:active + .btn-outline-warning, .btn-outline-warning:active, .btn-outline-warning.active, .btn-outline-warning.dropdown-toggle.show { + color: #000; + background-color: #f7cc53; + border-color: #f7cc53; } + .btn-check:checked + .btn-outline-warning:focus, + .btn-check:active + .btn-outline-warning:focus, .btn-outline-warning:active:focus, .btn-outline-warning.active:focus, .btn-outline-warning.dropdown-toggle.show:focus { + -webkit-box-shadow: 0 0 0 0.15rem rgba(247, 204, 83, 0.5); + box-shadow: 0 0 0 0.15rem rgba(247, 204, 83, 0.5); } + .btn-outline-warning:disabled, .btn-outline-warning.disabled { + color: #f7cc53; + background-color: transparent; } + +.btn-outline-danger { + color: #f34e4e; + border-color: #f34e4e; } + .btn-outline-danger:hover { + color: #fff; + background-color: #f34e4e; + border-color: #f34e4e; } + .btn-check:focus + .btn-outline-danger, .btn-outline-danger:focus { + -webkit-box-shadow: 0 0 0 0.15rem rgba(243, 78, 78, 0.5); + box-shadow: 0 0 0 0.15rem rgba(243, 78, 78, 0.5); } + .btn-check:checked + .btn-outline-danger, + .btn-check:active + .btn-outline-danger, .btn-outline-danger:active, .btn-outline-danger.active, .btn-outline-danger.dropdown-toggle.show { + color: #fff; + background-color: #f34e4e; + border-color: #f34e4e; } + .btn-check:checked + .btn-outline-danger:focus, + .btn-check:active + .btn-outline-danger:focus, .btn-outline-danger:active:focus, .btn-outline-danger.active:focus, .btn-outline-danger.dropdown-toggle.show:focus { + -webkit-box-shadow: 0 0 0 0.15rem rgba(243, 78, 78, 0.5); + box-shadow: 0 0 0 0.15rem rgba(243, 78, 78, 0.5); } + .btn-outline-danger:disabled, .btn-outline-danger.disabled { + color: #f34e4e; + background-color: transparent; } + +.btn-outline-pink { + color: #e83e8c; + border-color: #e83e8c; } + .btn-outline-pink:hover { + color: #fff; + background-color: #e83e8c; + border-color: #e83e8c; } + .btn-check:focus + .btn-outline-pink, .btn-outline-pink:focus { + -webkit-box-shadow: 0 0 0 0.15rem rgba(232, 62, 140, 0.5); + box-shadow: 0 0 0 0.15rem rgba(232, 62, 140, 0.5); } + .btn-check:checked + .btn-outline-pink, + .btn-check:active + .btn-outline-pink, .btn-outline-pink:active, .btn-outline-pink.active, .btn-outline-pink.dropdown-toggle.show { + color: #fff; + background-color: #e83e8c; + border-color: #e83e8c; } + .btn-check:checked + .btn-outline-pink:focus, + .btn-check:active + .btn-outline-pink:focus, .btn-outline-pink:active:focus, .btn-outline-pink.active:focus, .btn-outline-pink.dropdown-toggle.show:focus { + -webkit-box-shadow: 0 0 0 0.15rem rgba(232, 62, 140, 0.5); + box-shadow: 0 0 0 0.15rem rgba(232, 62, 140, 0.5); } + .btn-outline-pink:disabled, .btn-outline-pink.disabled { + color: #e83e8c; + background-color: transparent; } + +.btn-outline-light { + color: #f5f6f8; + border-color: #f5f6f8; } + .btn-outline-light:hover { + color: #000; + background-color: #f5f6f8; + border-color: #f5f6f8; } + .btn-check:focus + .btn-outline-light, .btn-outline-light:focus { + -webkit-box-shadow: 0 0 0 0.15rem rgba(245, 246, 248, 0.5); + box-shadow: 0 0 0 0.15rem rgba(245, 246, 248, 0.5); } + .btn-check:checked + .btn-outline-light, + .btn-check:active + .btn-outline-light, .btn-outline-light:active, .btn-outline-light.active, .btn-outline-light.dropdown-toggle.show { + color: #000; + background-color: #f5f6f8; + border-color: #f5f6f8; } + .btn-check:checked + .btn-outline-light:focus, + .btn-check:active + .btn-outline-light:focus, .btn-outline-light:active:focus, .btn-outline-light.active:focus, .btn-outline-light.dropdown-toggle.show:focus { + -webkit-box-shadow: 0 0 0 0.15rem rgba(245, 246, 248, 0.5); + box-shadow: 0 0 0 0.15rem rgba(245, 246, 248, 0.5); } + .btn-outline-light:disabled, .btn-outline-light.disabled { + color: #f5f6f8; + background-color: transparent; } + +.btn-outline-dark { + color: #343a40; + border-color: #343a40; } + .btn-outline-dark:hover { + color: #fff; + background-color: #343a40; + border-color: #343a40; } + .btn-check:focus + .btn-outline-dark, .btn-outline-dark:focus { + -webkit-box-shadow: 0 0 0 0.15rem rgba(52, 58, 64, 0.5); + box-shadow: 0 0 0 0.15rem rgba(52, 58, 64, 0.5); } + .btn-check:checked + .btn-outline-dark, + .btn-check:active + .btn-outline-dark, .btn-outline-dark:active, .btn-outline-dark.active, .btn-outline-dark.dropdown-toggle.show { + color: #fff; + background-color: #343a40; + border-color: #343a40; } + .btn-check:checked + .btn-outline-dark:focus, + .btn-check:active + .btn-outline-dark:focus, .btn-outline-dark:active:focus, .btn-outline-dark.active:focus, .btn-outline-dark.dropdown-toggle.show:focus { + -webkit-box-shadow: 0 0 0 0.15rem rgba(52, 58, 64, 0.5); + box-shadow: 0 0 0 0.15rem rgba(52, 58, 64, 0.5); } + .btn-outline-dark:disabled, .btn-outline-dark.disabled { + color: #343a40; + background-color: transparent; } + +.btn-outline-purple { + color: #564ab1; + border-color: #564ab1; } + .btn-outline-purple:hover { + color: #fff; + background-color: #564ab1; + border-color: #564ab1; } + .btn-check:focus + .btn-outline-purple, .btn-outline-purple:focus { + -webkit-box-shadow: 0 0 0 0.15rem rgba(86, 74, 177, 0.5); + box-shadow: 0 0 0 0.15rem rgba(86, 74, 177, 0.5); } + .btn-check:checked + .btn-outline-purple, + .btn-check:active + .btn-outline-purple, .btn-outline-purple:active, .btn-outline-purple.active, .btn-outline-purple.dropdown-toggle.show { + color: #fff; + background-color: #564ab1; + border-color: #564ab1; } + .btn-check:checked + .btn-outline-purple:focus, + .btn-check:active + .btn-outline-purple:focus, .btn-outline-purple:active:focus, .btn-outline-purple.active:focus, .btn-outline-purple.dropdown-toggle.show:focus { + -webkit-box-shadow: 0 0 0 0.15rem rgba(86, 74, 177, 0.5); + box-shadow: 0 0 0 0.15rem rgba(86, 74, 177, 0.5); } + .btn-outline-purple:disabled, .btn-outline-purple.disabled { + color: #564ab1; + background-color: transparent; } + +.btn-link { + font-weight: 400; + color: #2e86de; + text-decoration: none; } + .btn-link:hover { + color: #025d91; } + .btn-link:disabled, .btn-link.disabled { + color: #74788d; } + +.btn-lg, .btn-group-lg > .btn { + padding: 0.5rem 1rem; + font-size: 1.125rem; + border-radius: 0.4rem; } + +.btn-sm, .btn-group-sm > .btn { + padding: 0.25rem 0.5rem; + font-size: 0.7875rem; + border-radius: 0.2rem; } + +.fade { + -webkit-transition: opacity 0.15s linear; + transition: opacity 0.15s linear; } + @media (prefers-reduced-motion: reduce) { + .fade { + -webkit-transition: none; + transition: none; } } + .fade:not(.show) { + opacity: 0; } + +.collapse:not(.show) { + display: none; } + +.collapsing { + height: 0; + overflow: hidden; + -webkit-transition: height 0.35s ease; + transition: height 0.35s ease; } + @media (prefers-reduced-motion: reduce) { + .collapsing { + -webkit-transition: none; + transition: none; } } + .collapsing.collapse-horizontal { + width: 0; + height: auto; + -webkit-transition: width 0.35s ease; + transition: width 0.35s ease; } + @media (prefers-reduced-motion: reduce) { + .collapsing.collapse-horizontal { + -webkit-transition: none; + transition: none; } } + +.dropup, +.dropend, +.dropdown, +.dropstart { + position: relative; } + +.dropdown-toggle { + white-space: nowrap; } + +.dropdown-menu { + position: absolute; + z-index: 1000; + display: none; + min-width: 10rem; + padding: 0.5rem 0; + margin: 0; + font-size: 0.9rem; + color: #495057; + text-align: left; + list-style: none; + background-color: #fff; + background-clip: padding-box; + border: 1px solid #e2e5e8; + border-radius: 0.25rem; } + .dropdown-menu[data-bs-popper] { + top: 100%; + left: 0; + margin-top: 0.125rem; } + +.dropdown-menu-start { + --bs-position: start; } + .dropdown-menu-start[data-bs-popper] { + right: auto; + left: 0; } + +.dropdown-menu-end { + --bs-position: end; } + .dropdown-menu-end[data-bs-popper] { + right: 0; + left: auto; } + +@media (min-width: 576px) { + .dropdown-menu-sm-start { + --bs-position: start; } + .dropdown-menu-sm-start[data-bs-popper] { + right: auto; + left: 0; } + .dropdown-menu-sm-end { + --bs-position: end; } + .dropdown-menu-sm-end[data-bs-popper] { + right: 0; + left: auto; } } + +@media (min-width: 768px) { + .dropdown-menu-md-start { + --bs-position: start; } + .dropdown-menu-md-start[data-bs-popper] { + right: auto; + left: 0; } + .dropdown-menu-md-end { + --bs-position: end; } + .dropdown-menu-md-end[data-bs-popper] { + right: 0; + left: auto; } } + +@media (min-width: 992px) { + .dropdown-menu-lg-start { + --bs-position: start; } + .dropdown-menu-lg-start[data-bs-popper] { + right: auto; + left: 0; } + .dropdown-menu-lg-end { + --bs-position: end; } + .dropdown-menu-lg-end[data-bs-popper] { + right: 0; + left: auto; } } + +@media (min-width: 1200px) { + .dropdown-menu-xl-start { + --bs-position: start; } + .dropdown-menu-xl-start[data-bs-popper] { + right: auto; + left: 0; } + .dropdown-menu-xl-end { + --bs-position: end; } + .dropdown-menu-xl-end[data-bs-popper] { + right: 0; + left: auto; } } + +@media (min-width: 1400px) { + .dropdown-menu-xxl-start { + --bs-position: start; } + .dropdown-menu-xxl-start[data-bs-popper] { + right: auto; + left: 0; } + .dropdown-menu-xxl-end { + --bs-position: end; } + .dropdown-menu-xxl-end[data-bs-popper] { + right: 0; + left: auto; } } + +.dropup .dropdown-menu[data-bs-popper] { + top: auto; + bottom: 100%; + margin-top: 0; + margin-bottom: 0.125rem; } + +.dropend .dropdown-menu[data-bs-popper] { + top: 0; + right: auto; + left: 100%; + margin-top: 0; + margin-left: 0.125rem; } + +.dropend .dropdown-toggle::after { + vertical-align: 0; } + +.dropstart .dropdown-menu[data-bs-popper] { + top: 0; + right: 100%; + left: auto; + margin-top: 0; + margin-right: 0.125rem; } + +.dropstart .dropdown-toggle::before { + vertical-align: 0; } + +.dropdown-divider { + height: 0; + margin: 0.5rem 0; + overflow: hidden; + border-top: 1px solid #e2e5e8; } + +.dropdown-item { + display: block; + width: 100%; + padding: 0.35rem 1.5rem; + clear: both; + font-weight: 400; + color: #495057; + text-align: inherit; + white-space: nowrap; + background-color: transparent; + border: 0; } + .dropdown-item:hover, .dropdown-item:focus { + color: #2c3034; + background-color: #f8f9fa; } + .dropdown-item.active, .dropdown-item:active { + color: #2c3034; + text-decoration: none; + background-color: #f8f9fa; } + .dropdown-item.disabled, .dropdown-item:disabled { + color: #74788d; + pointer-events: none; + background-color: transparent; } + +.dropdown-menu.show { + display: block; } + +.dropdown-header { + display: block; + padding: 0.5rem 1.5rem; + margin-bottom: 0; + font-size: 0.7875rem; + color: #74788d; + white-space: nowrap; } + +.dropdown-item-text { + display: block; + padding: 0.35rem 1.5rem; + color: #495057; } + +.dropdown-menu-dark { + color: #eff0f2; + background-color: #495057; + border-color: #e2e5e8; } + .dropdown-menu-dark .dropdown-item { + color: #eff0f2; } + .dropdown-menu-dark .dropdown-item:hover, .dropdown-menu-dark .dropdown-item:focus { + color: #fff; + background-color: #555d65; } + .dropdown-menu-dark .dropdown-item.active, .dropdown-menu-dark .dropdown-item:active { + color: #e2e5e8; + background-color: #555d65; } + .dropdown-menu-dark .dropdown-item.disabled, .dropdown-menu-dark .dropdown-item:disabled { + color: #eff0f2; } + .dropdown-menu-dark .dropdown-divider { + border-color: #e2e5e8; } + .dropdown-menu-dark .dropdown-item-text { + color: #eff0f2; } + .dropdown-menu-dark .dropdown-header { + color: #adb5bd; } + +.btn-group, +.btn-group-vertical { + position: relative; + display: -webkit-inline-box; + display: -ms-inline-flexbox; + display: inline-flex; + vertical-align: middle; } + .btn-group > .btn, + .btn-group-vertical > .btn { + position: relative; + -webkit-box-flex: 1; + -ms-flex: 1 1 auto; + flex: 1 1 auto; } + .btn-group > .btn-check:checked + .btn, + .btn-group > .btn-check:focus + .btn, + .btn-group > .btn:hover, + .btn-group > .btn:focus, + .btn-group > .btn:active, + .btn-group > .btn.active, + .btn-group-vertical > .btn-check:checked + .btn, + .btn-group-vertical > .btn-check:focus + .btn, + .btn-group-vertical > .btn:hover, + .btn-group-vertical > .btn:focus, + .btn-group-vertical > .btn:active, + .btn-group-vertical > .btn.active { + z-index: 1; } + +.btn-toolbar { + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -ms-flex-wrap: wrap; + flex-wrap: wrap; + -webkit-box-pack: start; + -ms-flex-pack: start; + justify-content: flex-start; } + .btn-toolbar .input-group { + width: auto; } + +.btn-group > .btn:not(:first-child), +.btn-group > .btn-group:not(:first-child) { + margin-left: -1px; } + +.btn-group > .btn:not(:last-child):not(.dropdown-toggle), +.btn-group > .btn-group:not(:last-child) > .btn { + border-top-right-radius: 0; + border-bottom-right-radius: 0; } + +.btn-group > .btn:nth-child(n + 3), +.btn-group > :not(.btn-check) + .btn, +.btn-group > .btn-group:not(:first-child) > .btn { + border-top-left-radius: 0; + border-bottom-left-radius: 0; } + +.dropdown-toggle-split { + padding-right: 0.5625rem; + padding-left: 0.5625rem; } + .dropdown-toggle-split::after, + .dropup .dropdown-toggle-split::after, + .dropend .dropdown-toggle-split::after { + margin-left: 0; } + .dropstart .dropdown-toggle-split::before { + margin-right: 0; } + +.btn-sm + .dropdown-toggle-split, .btn-group-sm > .btn + .dropdown-toggle-split { + padding-right: 0.375rem; + padding-left: 0.375rem; } + +.btn-lg + .dropdown-toggle-split, .btn-group-lg > .btn + .dropdown-toggle-split { + padding-right: 0.75rem; + padding-left: 0.75rem; } + +.btn-group-vertical { + -webkit-box-orient: vertical; + -webkit-box-direction: normal; + -ms-flex-direction: column; + flex-direction: column; + -webkit-box-align: start; + -ms-flex-align: start; + align-items: flex-start; + -webkit-box-pack: center; + -ms-flex-pack: center; + justify-content: center; } + .btn-group-vertical > .btn, + .btn-group-vertical > .btn-group { + width: 100%; } + .btn-group-vertical > .btn:not(:first-child), + .btn-group-vertical > .btn-group:not(:first-child) { + margin-top: -1px; } + .btn-group-vertical > .btn:not(:last-child):not(.dropdown-toggle), + .btn-group-vertical > .btn-group:not(:last-child) > .btn { + border-bottom-right-radius: 0; + border-bottom-left-radius: 0; } + .btn-group-vertical > .btn ~ .btn, + .btn-group-vertical > .btn-group:not(:first-child) > .btn { + border-top-left-radius: 0; + border-top-right-radius: 0; } + +.nav { + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -ms-flex-wrap: wrap; + flex-wrap: wrap; + padding-left: 0; + margin-bottom: 0; + list-style: none; } + +.nav-link { + display: block; + padding: 0.5rem 1rem; + color: #2e86de; + -webkit-transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out; + transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out; } + @media (prefers-reduced-motion: reduce) { + .nav-link { + -webkit-transition: none; + transition: none; } } + .nav-link:hover, .nav-link:focus { + color: #025d91; } + .nav-link.disabled { + color: #74788d; + pointer-events: none; + cursor: default; } + +.nav-tabs { + border-bottom: 1px solid #e2e5e8; } + .nav-tabs .nav-link { + margin-bottom: -1px; + background: none; + border: 1px solid transparent; + border-top-left-radius: 0.25rem; + border-top-right-radius: 0.25rem; } + .nav-tabs .nav-link:hover, .nav-tabs .nav-link:focus { + border-color: #f5f6f8 #f5f6f8 #e2e5e8; + isolation: isolate; } + .nav-tabs .nav-link.disabled { + color: #74788d; + background-color: transparent; + border-color: transparent; } + .nav-tabs .nav-link.active, + .nav-tabs .nav-item.show .nav-link { + color: #495057; + background-color: #fff; + border-color: #e2e5e8 #e2e5e8 #fff; } + .nav-tabs .dropdown-menu { + margin-top: -1px; + border-top-left-radius: 0; + border-top-right-radius: 0; } + +.nav-pills .nav-link { + background: none; + border: 0; + border-radius: 0.25rem; } + +.nav-pills .nav-link.active, +.nav-pills .show > .nav-link { + color: #fff; + background-color: #2e86de; } + +.nav-fill > .nav-link, +.nav-fill .nav-item { + -webkit-box-flex: 1; + -ms-flex: 1 1 auto; + flex: 1 1 auto; + text-align: center; } + +.nav-justified > .nav-link, +.nav-justified .nav-item { + -ms-flex-preferred-size: 0; + flex-basis: 0; + -webkit-box-flex: 1; + -ms-flex-positive: 1; + flex-grow: 1; + text-align: center; } + +.nav-fill .nav-item .nav-link, +.nav-justified .nav-item .nav-link { + width: 100%; } + +.tab-content > .tab-pane { + display: none; } + +.tab-content > .active { + display: block; } + +.navbar { + position: relative; + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -ms-flex-wrap: wrap; + flex-wrap: wrap; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: justify; + -ms-flex-pack: justify; + justify-content: space-between; + padding-top: 0.5rem; + padding-bottom: 0.5rem; } + .navbar > .container, + .navbar > .container-fluid, .navbar > .container-sm, .navbar > .container-md, .navbar > .container-lg, .navbar > .container-xl, .navbar > .container-xxl { + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -ms-flex-wrap: inherit; + flex-wrap: inherit; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: justify; + -ms-flex-pack: justify; + justify-content: space-between; } + +.navbar-brand { + padding-top: 0.33125rem; + padding-bottom: 0.33125rem; + margin-right: 1rem; + font-size: 1.125rem; + white-space: nowrap; } + +.navbar-nav { + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-orient: vertical; + -webkit-box-direction: normal; + -ms-flex-direction: column; + flex-direction: column; + padding-left: 0; + margin-bottom: 0; + list-style: none; } + .navbar-nav .nav-link { + padding-right: 0; + padding-left: 0; } + .navbar-nav .dropdown-menu { + position: static; } + +.navbar-text { + padding-top: 0.5rem; + padding-bottom: 0.5rem; } + +.navbar-collapse { + -ms-flex-preferred-size: 100%; + flex-basis: 100%; + -webkit-box-flex: 1; + -ms-flex-positive: 1; + flex-grow: 1; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; } + +.navbar-toggler { + padding: 0.25rem 0.75rem; + font-size: 1.125rem; + line-height: 1; + background-color: transparent; + border: 1px solid transparent; + border-radius: 0.25rem; + -webkit-transition: -webkit-box-shadow 0.15s ease-in-out; + transition: -webkit-box-shadow 0.15s ease-in-out; + transition: box-shadow 0.15s ease-in-out; + transition: box-shadow 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out; } + @media (prefers-reduced-motion: reduce) { + .navbar-toggler { + -webkit-transition: none; + transition: none; } } + .navbar-toggler:hover { + text-decoration: none; } + .navbar-toggler:focus { + text-decoration: none; + outline: 0; + -webkit-box-shadow: 0 0 0 0.15rem; + box-shadow: 0 0 0 0.15rem; } + +.navbar-toggler-icon { + display: inline-block; + width: 1.5em; + height: 1.5em; + vertical-align: middle; + background-repeat: no-repeat; + background-position: center; + background-size: 100%; } + +.navbar-nav-scroll { + max-height: var(--bs-scroll-height, 75vh); + overflow-y: auto; } + +@media (min-width: 576px) { + .navbar-expand-sm { + -ms-flex-wrap: nowrap; + flex-wrap: nowrap; + -webkit-box-pack: start; + -ms-flex-pack: start; + justify-content: flex-start; } + .navbar-expand-sm .navbar-nav { + -webkit-box-orient: horizontal; + -webkit-box-direction: normal; + -ms-flex-direction: row; + flex-direction: row; } + .navbar-expand-sm .navbar-nav .dropdown-menu { + position: absolute; } + .navbar-expand-sm .navbar-nav .nav-link { + padding-right: 0.5rem; + padding-left: 0.5rem; } + .navbar-expand-sm .navbar-nav-scroll { + overflow: visible; } + .navbar-expand-sm .navbar-collapse { + display: -webkit-box !important; + display: -ms-flexbox !important; + display: flex !important; + -ms-flex-preferred-size: auto; + flex-basis: auto; } + .navbar-expand-sm .navbar-toggler { + display: none; } + .navbar-expand-sm .offcanvas-header { + display: none; } + .navbar-expand-sm .offcanvas { + position: inherit; + bottom: 0; + z-index: 1000; + -webkit-box-flex: 1; + -ms-flex-positive: 1; + flex-grow: 1; + visibility: visible !important; + background-color: transparent; + border-right: 0; + border-left: 0; + -webkit-transition: none; + transition: none; + -webkit-transform: none; + transform: none; } + .navbar-expand-sm .offcanvas-top, + .navbar-expand-sm .offcanvas-bottom { + height: auto; + border-top: 0; + border-bottom: 0; } + .navbar-expand-sm .offcanvas-body { + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-flex: 0; + -ms-flex-positive: 0; + flex-grow: 0; + padding: 0; + overflow-y: visible; } } + +@media (min-width: 768px) { + .navbar-expand-md { + -ms-flex-wrap: nowrap; + flex-wrap: nowrap; + -webkit-box-pack: start; + -ms-flex-pack: start; + justify-content: flex-start; } + .navbar-expand-md .navbar-nav { + -webkit-box-orient: horizontal; + -webkit-box-direction: normal; + -ms-flex-direction: row; + flex-direction: row; } + .navbar-expand-md .navbar-nav .dropdown-menu { + position: absolute; } + .navbar-expand-md .navbar-nav .nav-link { + padding-right: 0.5rem; + padding-left: 0.5rem; } + .navbar-expand-md .navbar-nav-scroll { + overflow: visible; } + .navbar-expand-md .navbar-collapse { + display: -webkit-box !important; + display: -ms-flexbox !important; + display: flex !important; + -ms-flex-preferred-size: auto; + flex-basis: auto; } + .navbar-expand-md .navbar-toggler { + display: none; } + .navbar-expand-md .offcanvas-header { + display: none; } + .navbar-expand-md .offcanvas { + position: inherit; + bottom: 0; + z-index: 1000; + -webkit-box-flex: 1; + -ms-flex-positive: 1; + flex-grow: 1; + visibility: visible !important; + background-color: transparent; + border-right: 0; + border-left: 0; + -webkit-transition: none; + transition: none; + -webkit-transform: none; + transform: none; } + .navbar-expand-md .offcanvas-top, + .navbar-expand-md .offcanvas-bottom { + height: auto; + border-top: 0; + border-bottom: 0; } + .navbar-expand-md .offcanvas-body { + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-flex: 0; + -ms-flex-positive: 0; + flex-grow: 0; + padding: 0; + overflow-y: visible; } } + +@media (min-width: 992px) { + .navbar-expand-lg { + -ms-flex-wrap: nowrap; + flex-wrap: nowrap; + -webkit-box-pack: start; + -ms-flex-pack: start; + justify-content: flex-start; } + .navbar-expand-lg .navbar-nav { + -webkit-box-orient: horizontal; + -webkit-box-direction: normal; + -ms-flex-direction: row; + flex-direction: row; } + .navbar-expand-lg .navbar-nav .dropdown-menu { + position: absolute; } + .navbar-expand-lg .navbar-nav .nav-link { + padding-right: 0.5rem; + padding-left: 0.5rem; } + .navbar-expand-lg .navbar-nav-scroll { + overflow: visible; } + .navbar-expand-lg .navbar-collapse { + display: -webkit-box !important; + display: -ms-flexbox !important; + display: flex !important; + -ms-flex-preferred-size: auto; + flex-basis: auto; } + .navbar-expand-lg .navbar-toggler { + display: none; } + .navbar-expand-lg .offcanvas-header { + display: none; } + .navbar-expand-lg .offcanvas { + position: inherit; + bottom: 0; + z-index: 1000; + -webkit-box-flex: 1; + -ms-flex-positive: 1; + flex-grow: 1; + visibility: visible !important; + background-color: transparent; + border-right: 0; + border-left: 0; + -webkit-transition: none; + transition: none; + -webkit-transform: none; + transform: none; } + .navbar-expand-lg .offcanvas-top, + .navbar-expand-lg .offcanvas-bottom { + height: auto; + border-top: 0; + border-bottom: 0; } + .navbar-expand-lg .offcanvas-body { + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-flex: 0; + -ms-flex-positive: 0; + flex-grow: 0; + padding: 0; + overflow-y: visible; } } + +@media (min-width: 1200px) { + .navbar-expand-xl { + -ms-flex-wrap: nowrap; + flex-wrap: nowrap; + -webkit-box-pack: start; + -ms-flex-pack: start; + justify-content: flex-start; } + .navbar-expand-xl .navbar-nav { + -webkit-box-orient: horizontal; + -webkit-box-direction: normal; + -ms-flex-direction: row; + flex-direction: row; } + .navbar-expand-xl .navbar-nav .dropdown-menu { + position: absolute; } + .navbar-expand-xl .navbar-nav .nav-link { + padding-right: 0.5rem; + padding-left: 0.5rem; } + .navbar-expand-xl .navbar-nav-scroll { + overflow: visible; } + .navbar-expand-xl .navbar-collapse { + display: -webkit-box !important; + display: -ms-flexbox !important; + display: flex !important; + -ms-flex-preferred-size: auto; + flex-basis: auto; } + .navbar-expand-xl .navbar-toggler { + display: none; } + .navbar-expand-xl .offcanvas-header { + display: none; } + .navbar-expand-xl .offcanvas { + position: inherit; + bottom: 0; + z-index: 1000; + -webkit-box-flex: 1; + -ms-flex-positive: 1; + flex-grow: 1; + visibility: visible !important; + background-color: transparent; + border-right: 0; + border-left: 0; + -webkit-transition: none; + transition: none; + -webkit-transform: none; + transform: none; } + .navbar-expand-xl .offcanvas-top, + .navbar-expand-xl .offcanvas-bottom { + height: auto; + border-top: 0; + border-bottom: 0; } + .navbar-expand-xl .offcanvas-body { + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-flex: 0; + -ms-flex-positive: 0; + flex-grow: 0; + padding: 0; + overflow-y: visible; } } + +@media (min-width: 1400px) { + .navbar-expand-xxl { + -ms-flex-wrap: nowrap; + flex-wrap: nowrap; + -webkit-box-pack: start; + -ms-flex-pack: start; + justify-content: flex-start; } + .navbar-expand-xxl .navbar-nav { + -webkit-box-orient: horizontal; + -webkit-box-direction: normal; + -ms-flex-direction: row; + flex-direction: row; } + .navbar-expand-xxl .navbar-nav .dropdown-menu { + position: absolute; } + .navbar-expand-xxl .navbar-nav .nav-link { + padding-right: 0.5rem; + padding-left: 0.5rem; } + .navbar-expand-xxl .navbar-nav-scroll { + overflow: visible; } + .navbar-expand-xxl .navbar-collapse { + display: -webkit-box !important; + display: -ms-flexbox !important; + display: flex !important; + -ms-flex-preferred-size: auto; + flex-basis: auto; } + .navbar-expand-xxl .navbar-toggler { + display: none; } + .navbar-expand-xxl .offcanvas-header { + display: none; } + .navbar-expand-xxl .offcanvas { + position: inherit; + bottom: 0; + z-index: 1000; + -webkit-box-flex: 1; + -ms-flex-positive: 1; + flex-grow: 1; + visibility: visible !important; + background-color: transparent; + border-right: 0; + border-left: 0; + -webkit-transition: none; + transition: none; + -webkit-transform: none; + transform: none; } + .navbar-expand-xxl .offcanvas-top, + .navbar-expand-xxl .offcanvas-bottom { + height: auto; + border-top: 0; + border-bottom: 0; } + .navbar-expand-xxl .offcanvas-body { + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-flex: 0; + -ms-flex-positive: 0; + flex-grow: 0; + padding: 0; + overflow-y: visible; } } + +.navbar-expand { + -ms-flex-wrap: nowrap; + flex-wrap: nowrap; + -webkit-box-pack: start; + -ms-flex-pack: start; + justify-content: flex-start; } + .navbar-expand .navbar-nav { + -webkit-box-orient: horizontal; + -webkit-box-direction: normal; + -ms-flex-direction: row; + flex-direction: row; } + .navbar-expand .navbar-nav .dropdown-menu { + position: absolute; } + .navbar-expand .navbar-nav .nav-link { + padding-right: 0.5rem; + padding-left: 0.5rem; } + .navbar-expand .navbar-nav-scroll { + overflow: visible; } + .navbar-expand .navbar-collapse { + display: -webkit-box !important; + display: -ms-flexbox !important; + display: flex !important; + -ms-flex-preferred-size: auto; + flex-basis: auto; } + .navbar-expand .navbar-toggler { + display: none; } + .navbar-expand .offcanvas-header { + display: none; } + .navbar-expand .offcanvas { + position: inherit; + bottom: 0; + z-index: 1000; + -webkit-box-flex: 1; + -ms-flex-positive: 1; + flex-grow: 1; + visibility: visible !important; + background-color: transparent; + border-right: 0; + border-left: 0; + -webkit-transition: none; + transition: none; + -webkit-transform: none; + transform: none; } + .navbar-expand .offcanvas-top, + .navbar-expand .offcanvas-bottom { + height: auto; + border-top: 0; + border-bottom: 0; } + .navbar-expand .offcanvas-body { + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-flex: 0; + -ms-flex-positive: 0; + flex-grow: 0; + padding: 0; + overflow-y: visible; } + +.navbar-light .navbar-brand { + color: rgba(0, 0, 0, 0.9); } + .navbar-light .navbar-brand:hover, .navbar-light .navbar-brand:focus { + color: rgba(0, 0, 0, 0.9); } + +.navbar-light .navbar-nav .nav-link { + color: rgba(0, 0, 0, 0.55); } + .navbar-light .navbar-nav .nav-link:hover, .navbar-light .navbar-nav .nav-link:focus { + color: rgba(0, 0, 0, 0.7); } + .navbar-light .navbar-nav .nav-link.disabled { + color: rgba(0, 0, 0, 0.3); } + +.navbar-light .navbar-nav .show > .nav-link, +.navbar-light .navbar-nav .nav-link.active { + color: rgba(0, 0, 0, 0.9); } + +.navbar-light .navbar-toggler { + color: rgba(0, 0, 0, 0.55); + border-color: rgba(0, 0, 0, 0.1); } + +.navbar-light .navbar-toggler-icon { + background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%280, 0, 0, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e"); } + +.navbar-light .navbar-text { + color: rgba(0, 0, 0, 0.55); } + .navbar-light .navbar-text a, + .navbar-light .navbar-text a:hover, + .navbar-light .navbar-text a:focus { + color: rgba(0, 0, 0, 0.9); } + +.navbar-dark .navbar-brand { + color: #fff; } + .navbar-dark .navbar-brand:hover, .navbar-dark .navbar-brand:focus { + color: #fff; } + +.navbar-dark .navbar-nav .nav-link { + color: rgba(255, 255, 255, 0.55); } + .navbar-dark .navbar-nav .nav-link:hover, .navbar-dark .navbar-nav .nav-link:focus { + color: rgba(255, 255, 255, 0.75); } + .navbar-dark .navbar-nav .nav-link.disabled { + color: rgba(255, 255, 255, 0.25); } + +.navbar-dark .navbar-nav .show > .nav-link, +.navbar-dark .navbar-nav .nav-link.active { + color: #fff; } + +.navbar-dark .navbar-toggler { + color: rgba(255, 255, 255, 0.55); + border-color: rgba(255, 255, 255, 0.1); } + +.navbar-dark .navbar-toggler-icon { + background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e"); } + +.navbar-dark .navbar-text { + color: rgba(255, 255, 255, 0.55); } + .navbar-dark .navbar-text a, + .navbar-dark .navbar-text a:hover, + .navbar-dark .navbar-text a:focus { + color: #fff; } + +.card { + position: relative; + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-orient: vertical; + -webkit-box-direction: normal; + -ms-flex-direction: column; + flex-direction: column; + min-width: 0; + word-wrap: break-word; + background-color: #fff; + background-clip: border-box; + border: 1px solid #eff0f2; + border-radius: 0.25rem; } + .card > hr { + margin-right: 0; + margin-left: 0; } + .card > .list-group { + border-top: inherit; + border-bottom: inherit; } + .card > .list-group:first-child { + border-top-width: 0; + border-top-left-radius: calc(0.25rem - 1px); + border-top-right-radius: calc(0.25rem - 1px); } + .card > .list-group:last-child { + border-bottom-width: 0; + border-bottom-right-radius: calc(0.25rem - 1px); + border-bottom-left-radius: calc(0.25rem - 1px); } + .card > .card-header + .list-group, + .card > .list-group + .card-footer { + border-top: 0; } + +.card-body { + -webkit-box-flex: 1; + -ms-flex: 1 1 auto; + flex: 1 1 auto; + padding: 1.25rem 1.25rem; } + +.card-title { + margin-bottom: 0.5rem; } + +.card-subtitle { + margin-top: -0.25rem; + margin-bottom: 0; } + +.card-text:last-child { + margin-bottom: 0; } + +.card-link + .card-link { + margin-left: 1.25rem; } + +.card-header { + padding: 1rem 1.25rem; + margin-bottom: 0; + background-color: #fff; + border-bottom: 1px solid #eff0f2; } + .card-header:first-child { + border-radius: calc(0.25rem - 1px) calc(0.25rem - 1px) 0 0; } + +.card-footer { + padding: 1rem 1.25rem; + background-color: #fff; + border-top: 1px solid #eff0f2; } + .card-footer:last-child { + border-radius: 0 0 calc(0.25rem - 1px) calc(0.25rem - 1px); } + +.card-header-tabs { + margin-right: -0.625rem; + margin-bottom: -1rem; + margin-left: -0.625rem; + border-bottom: 0; } + +.card-header-pills { + margin-right: -0.625rem; + margin-left: -0.625rem; } + +.card-img-overlay { + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + padding: 1rem; + border-radius: calc(0.25rem - 1px); } + +.card-img, +.card-img-top, +.card-img-bottom { + width: 100%; } + +.card-img, +.card-img-top { + border-top-left-radius: calc(0.25rem - 1px); + border-top-right-radius: calc(0.25rem - 1px); } + +.card-img, +.card-img-bottom { + border-bottom-right-radius: calc(0.25rem - 1px); + border-bottom-left-radius: calc(0.25rem - 1px); } + +.card-group > .card { + margin-bottom: 10px; } + +@media (min-width: 576px) { + .card-group { + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-orient: horizontal; + -webkit-box-direction: normal; + -ms-flex-flow: row wrap; + flex-flow: row wrap; } + .card-group > .card { + -webkit-box-flex: 1; + -ms-flex: 1 0 0%; + flex: 1 0 0%; + margin-bottom: 0; } + .card-group > .card + .card { + margin-left: 0; + border-left: 0; } + .card-group > .card:not(:last-child) { + border-top-right-radius: 0; + border-bottom-right-radius: 0; } + .card-group > .card:not(:last-child) .card-img-top, + .card-group > .card:not(:last-child) .card-header { + border-top-right-radius: 0; } + .card-group > .card:not(:last-child) .card-img-bottom, + .card-group > .card:not(:last-child) .card-footer { + border-bottom-right-radius: 0; } + .card-group > .card:not(:first-child) { + border-top-left-radius: 0; + border-bottom-left-radius: 0; } + .card-group > .card:not(:first-child) .card-img-top, + .card-group > .card:not(:first-child) .card-header { + border-top-left-radius: 0; } + .card-group > .card:not(:first-child) .card-img-bottom, + .card-group > .card:not(:first-child) .card-footer { + border-bottom-left-radius: 0; } } + +.accordion-button { + position: relative; + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + width: 100%; + padding: 1rem 1.25rem; + font-size: 0.9rem; + color: #495057; + text-align: left; + background-color: transparent; + border: 0; + border-radius: 0; + overflow-anchor: none; + -webkit-transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, border-radius 0.15s ease, -webkit-box-shadow 0.15s ease-in-out; + transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, border-radius 0.15s ease, -webkit-box-shadow 0.15s ease-in-out; + transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out, border-radius 0.15s ease; + transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out, border-radius 0.15s ease, -webkit-box-shadow 0.15s ease-in-out; } + @media (prefers-reduced-motion: reduce) { + .accordion-button { + -webkit-transition: none; + transition: none; } } + .accordion-button:not(.collapsed) { + color: #0380c6; + background-color: #e6f4fc; + -webkit-box-shadow: inset 0 -1px 0 #eff0f2; + box-shadow: inset 0 -1px 0 #eff0f2; } + .accordion-button:not(.collapsed)::after { + background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%230380c6'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e"); + -webkit-transform: rotate(180deg); + transform: rotate(180deg); } + .accordion-button::after { + -ms-flex-negative: 0; + flex-shrink: 0; + width: 14px; + height: 14px; + margin-left: auto; + content: ""; + background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23495057'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e"); + background-repeat: no-repeat; + background-size: 14px; + -webkit-transition: -webkit-transform 0.2s ease-in-out; + transition: -webkit-transform 0.2s ease-in-out; + transition: transform 0.2s ease-in-out; + transition: transform 0.2s ease-in-out, -webkit-transform 0.2s ease-in-out; } + @media (prefers-reduced-motion: reduce) { + .accordion-button::after { + -webkit-transition: none; + transition: none; } } + .accordion-button:hover { + z-index: 2; } + .accordion-button:focus { + z-index: 3; + border-color: #cbced1; + outline: 0; + -webkit-box-shadow: none; + box-shadow: none; } + +.accordion-header { + margin-bottom: 0; } + +.accordion-item { + background-color: transparent; + border: 1px solid #eff0f2; } + .accordion-item:first-of-type { + border-top-left-radius: 0.25rem; + border-top-right-radius: 0.25rem; } + .accordion-item:first-of-type .accordion-button { + border-top-left-radius: calc(0.25rem - 1px); + border-top-right-radius: calc(0.25rem - 1px); } + .accordion-item:not(:first-of-type) { + border-top: 0; } + .accordion-item:last-of-type { + border-bottom-right-radius: 0.25rem; + border-bottom-left-radius: 0.25rem; } + .accordion-item:last-of-type .accordion-button.collapsed { + border-bottom-right-radius: calc(0.25rem - 1px); + border-bottom-left-radius: calc(0.25rem - 1px); } + .accordion-item:last-of-type .accordion-collapse { + border-bottom-right-radius: 0.25rem; + border-bottom-left-radius: 0.25rem; } + +.accordion-body { + padding: 1rem 1.25rem; } + +.accordion-flush .accordion-collapse { + border-width: 0; } + +.accordion-flush .accordion-item { + border-right: 0; + border-left: 0; + border-radius: 0; } + .accordion-flush .accordion-item:first-child { + border-top: 0; } + .accordion-flush .accordion-item:last-child { + border-bottom: 0; } + .accordion-flush .accordion-item .accordion-button { + border-radius: 0; } + +.breadcrumb { + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -ms-flex-wrap: wrap; + flex-wrap: wrap; + padding: 0 0; + margin-bottom: 1rem; + list-style: none; } + +.breadcrumb-item + .breadcrumb-item { + padding-left: 0.5rem; } + .breadcrumb-item + .breadcrumb-item::before { + float: left; + padding-right: 0.5rem; + color: #74788d; + content: var(--bs-breadcrumb-divider, "󰅂") /* rtl: var(--bs-breadcrumb-divider, "󰅂") */; } + +.breadcrumb-item.active { + color: #74788d; } + +.pagination { + display: -webkit-box; + display: -ms-flexbox; + display: flex; + padding-left: 0; + list-style: none; } + +.page-link { + position: relative; + display: block; + color: #74788d; + background-color: #fff; + border: 1px solid #e2e5e8; + -webkit-transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out; + transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out; + transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; + transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out; } + @media (prefers-reduced-motion: reduce) { + .page-link { + -webkit-transition: none; + transition: none; } } + .page-link:hover { + z-index: 2; + color: #025d91; + background-color: #f5f6f8; + border-color: #e2e5e8; } + .page-link:focus { + z-index: 3; + color: #025d91; + background-color: #f5f6f8; + outline: 0; + -webkit-box-shadow: none; + box-shadow: none; } + +.page-item:not(:first-child) .page-link { + margin-left: -1px; } + +.page-item.active .page-link { + z-index: 3; + color: #fff; + background-color: #2e86de; + border-color: #2e86de; } + +.page-item.disabled .page-link { + color: #adb5bd; + pointer-events: none; + background-color: #f8f9fa; + border-color: #e2e5e8; } + +.page-link { + padding: 0.375rem 0.75rem; } + +.page-item:first-child .page-link { + border-top-left-radius: 0.25rem; + border-bottom-left-radius: 0.25rem; } + +.page-item:last-child .page-link { + border-top-right-radius: 0.25rem; + border-bottom-right-radius: 0.25rem; } + +.pagination-lg .page-link { + padding: 0.75rem 1.5rem; + font-size: 1.125rem; } + +.pagination-lg .page-item:first-child .page-link { + border-top-left-radius: 0.4rem; + border-bottom-left-radius: 0.4rem; } + +.pagination-lg .page-item:last-child .page-link { + border-top-right-radius: 0.4rem; + border-bottom-right-radius: 0.4rem; } + +.pagination-sm .page-link { + padding: 0.25rem 0.5rem; + font-size: 0.7875rem; } + +.pagination-sm .page-item:first-child .page-link { + border-top-left-radius: 0.2rem; + border-bottom-left-radius: 0.2rem; } + +.pagination-sm .page-item:last-child .page-link { + border-top-right-radius: 0.2rem; + border-bottom-right-radius: 0.2rem; } + +.badge { + display: inline-block; + padding: 0.25em 0.6em; + font-size: 75%; + font-weight: 500; + line-height: 1; + color: #fff; + text-align: center; + white-space: nowrap; + vertical-align: baseline; + border-radius: 0.25rem; } + .badge:empty { + display: none; } + +.btn .badge { + position: relative; + top: -1px; } + +.alert { + position: relative; + padding: 0.75rem 1.25rem; + margin-bottom: 1rem; + border: 1px solid transparent; + border-radius: 0.25rem; } + +.alert-heading { + color: inherit; } + +.alert-link { + font-weight: 700; } + +.alert-dismissible { + padding-right: 3.75rem; } + .alert-dismissible .btn-close { + position: absolute; + top: 0; + right: 0; + z-index: 2; + padding: 0.9375rem 1.25rem; } + +.alert-primary { + color: #025584; + background-color: #cde8f8; + border-color: #b3ddf5; } + .alert-primary .alert-link { + color: #02446a; } + +.alert-secondary { + color: #464855; + background-color: #e3e4e8; + border-color: #d5d7dd; } + .alert-secondary .alert-link { + color: #383a44; } + +.alert-success { + color: #317e54; + background-color: #dcf6e8; + border-color: #cbf2dd; } + .alert-success .alert-link { + color: #276543; } + +.alert-info { + color: #397d92; + background-color: #dff6fd; + border-color: #cff1fb; } + .alert-info .alert-link { + color: #2e6475; } + +.alert-warning { + color: #947a32; + background-color: #fdf5dd; + border-color: #fdf0cb; } + .alert-warning .alert-link { + color: #766228; } + +.alert-danger { + color: #922f2f; + background-color: #fddcdc; + border-color: #fbcaca; } + .alert-danger .alert-link { + color: #752626; } + +.alert-pink { + color: #8b2554; + background-color: #fad8e8; + border-color: #f8c5dd; } + .alert-pink .alert-link { + color: #6f1e43; } + +.alert-light { + color: #939495; + background-color: #fdfdfe; + border-color: #fcfcfd; } + .alert-light .alert-link { + color: #767677; } + +.alert-dark { + color: #1f2326; + background-color: #d6d8d9; + border-color: #c2c4c6; } + .alert-dark .alert-link { + color: #191c1e; } + +.alert-purple { + color: #342c6a; + background-color: #dddbef; + border-color: #ccc9e8; } + .alert-purple .alert-link { + color: #2a2355; } + +@-webkit-keyframes progress-bar-stripes { + 0% { + background-position-x: 0.625rem; } } + +@keyframes progress-bar-stripes { + 0% { + background-position-x: 0.625rem; } } + +.progress { + display: -webkit-box; + display: -ms-flexbox; + display: flex; + height: 0.625rem; + overflow: hidden; + font-size: 0.675rem; + background-color: #f5f6f8; + border-radius: 0.25rem; } + +.progress-bar { + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-orient: vertical; + -webkit-box-direction: normal; + -ms-flex-direction: column; + flex-direction: column; + -webkit-box-pack: center; + -ms-flex-pack: center; + justify-content: center; + overflow: hidden; + color: #fff; + text-align: center; + white-space: nowrap; + background-color: #2e86de; + -webkit-transition: width 0.6s ease; + transition: width 0.6s ease; } + @media (prefers-reduced-motion: reduce) { + .progress-bar { + -webkit-transition: none; + transition: none; } } + +.progress-bar-striped { + background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-size: 0.625rem 0.625rem; } + +.progress-bar-animated { + -webkit-animation: 1s linear infinite progress-bar-stripes; + animation: 1s linear infinite progress-bar-stripes; } + @media (prefers-reduced-motion: reduce) { + .progress-bar-animated { + -webkit-animation: none; + animation: none; } } + +.list-group { + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-orient: vertical; + -webkit-box-direction: normal; + -ms-flex-direction: column; + flex-direction: column; + padding-left: 0; + margin-bottom: 0; + border-radius: 0.25rem; } + +.list-group-numbered { + list-style-type: none; + counter-reset: section; } + .list-group-numbered > li::before { + content: counters(section, ".") ". "; + counter-increment: section; } + +.list-group-item-action { + width: 100%; + color: #495057; + text-align: inherit; } + .list-group-item-action:hover, .list-group-item-action:focus { + z-index: 1; + color: #495057; + text-decoration: none; + background-color: #f8f9fa; } + .list-group-item-action:active { + color: #495057; + background-color: #f5f6f8; } + +.list-group-item { + position: relative; + display: block; + padding: 0.75rem 1.25rem; + color: #212529; + background-color: #fff; + border: 1px solid #eff0f2; } + .list-group-item:first-child { + border-top-left-radius: inherit; + border-top-right-radius: inherit; } + .list-group-item:last-child { + border-bottom-right-radius: inherit; + border-bottom-left-radius: inherit; } + .list-group-item.disabled, .list-group-item:disabled { + color: #74788d; + pointer-events: none; + background-color: #fff; } + .list-group-item.active { + z-index: 2; + color: #fff; + background-color: #2e86de; + border-color: #2e86de; } + .list-group-item + .list-group-item { + border-top-width: 0; } + .list-group-item + .list-group-item.active { + margin-top: -1px; + border-top-width: 1px; } + +.list-group-horizontal { + -webkit-box-orient: horizontal; + -webkit-box-direction: normal; + -ms-flex-direction: row; + flex-direction: row; } + .list-group-horizontal > .list-group-item:first-child { + border-bottom-left-radius: 0.25rem; + border-top-right-radius: 0; } + .list-group-horizontal > .list-group-item:last-child { + border-top-right-radius: 0.25rem; + border-bottom-left-radius: 0; } + .list-group-horizontal > .list-group-item.active { + margin-top: 0; } + .list-group-horizontal > .list-group-item + .list-group-item { + border-top-width: 1px; + border-left-width: 0; } + .list-group-horizontal > .list-group-item + .list-group-item.active { + margin-left: -1px; + border-left-width: 1px; } + +@media (min-width: 576px) { + .list-group-horizontal-sm { + -webkit-box-orient: horizontal; + -webkit-box-direction: normal; + -ms-flex-direction: row; + flex-direction: row; } + .list-group-horizontal-sm > .list-group-item:first-child { + border-bottom-left-radius: 0.25rem; + border-top-right-radius: 0; } + .list-group-horizontal-sm > .list-group-item:last-child { + border-top-right-radius: 0.25rem; + border-bottom-left-radius: 0; } + .list-group-horizontal-sm > .list-group-item.active { + margin-top: 0; } + .list-group-horizontal-sm > .list-group-item + .list-group-item { + border-top-width: 1px; + border-left-width: 0; } + .list-group-horizontal-sm > .list-group-item + .list-group-item.active { + margin-left: -1px; + border-left-width: 1px; } } + +@media (min-width: 768px) { + .list-group-horizontal-md { + -webkit-box-orient: horizontal; + -webkit-box-direction: normal; + -ms-flex-direction: row; + flex-direction: row; } + .list-group-horizontal-md > .list-group-item:first-child { + border-bottom-left-radius: 0.25rem; + border-top-right-radius: 0; } + .list-group-horizontal-md > .list-group-item:last-child { + border-top-right-radius: 0.25rem; + border-bottom-left-radius: 0; } + .list-group-horizontal-md > .list-group-item.active { + margin-top: 0; } + .list-group-horizontal-md > .list-group-item + .list-group-item { + border-top-width: 1px; + border-left-width: 0; } + .list-group-horizontal-md > .list-group-item + .list-group-item.active { + margin-left: -1px; + border-left-width: 1px; } } + +@media (min-width: 992px) { + .list-group-horizontal-lg { + -webkit-box-orient: horizontal; + -webkit-box-direction: normal; + -ms-flex-direction: row; + flex-direction: row; } + .list-group-horizontal-lg > .list-group-item:first-child { + border-bottom-left-radius: 0.25rem; + border-top-right-radius: 0; } + .list-group-horizontal-lg > .list-group-item:last-child { + border-top-right-radius: 0.25rem; + border-bottom-left-radius: 0; } + .list-group-horizontal-lg > .list-group-item.active { + margin-top: 0; } + .list-group-horizontal-lg > .list-group-item + .list-group-item { + border-top-width: 1px; + border-left-width: 0; } + .list-group-horizontal-lg > .list-group-item + .list-group-item.active { + margin-left: -1px; + border-left-width: 1px; } } + +@media (min-width: 1200px) { + .list-group-horizontal-xl { + -webkit-box-orient: horizontal; + -webkit-box-direction: normal; + -ms-flex-direction: row; + flex-direction: row; } + .list-group-horizontal-xl > .list-group-item:first-child { + border-bottom-left-radius: 0.25rem; + border-top-right-radius: 0; } + .list-group-horizontal-xl > .list-group-item:last-child { + border-top-right-radius: 0.25rem; + border-bottom-left-radius: 0; } + .list-group-horizontal-xl > .list-group-item.active { + margin-top: 0; } + .list-group-horizontal-xl > .list-group-item + .list-group-item { + border-top-width: 1px; + border-left-width: 0; } + .list-group-horizontal-xl > .list-group-item + .list-group-item.active { + margin-left: -1px; + border-left-width: 1px; } } + +@media (min-width: 1400px) { + .list-group-horizontal-xxl { + -webkit-box-orient: horizontal; + -webkit-box-direction: normal; + -ms-flex-direction: row; + flex-direction: row; } + .list-group-horizontal-xxl > .list-group-item:first-child { + border-bottom-left-radius: 0.25rem; + border-top-right-radius: 0; } + .list-group-horizontal-xxl > .list-group-item:last-child { + border-top-right-radius: 0.25rem; + border-bottom-left-radius: 0; } + .list-group-horizontal-xxl > .list-group-item.active { + margin-top: 0; } + .list-group-horizontal-xxl > .list-group-item + .list-group-item { + border-top-width: 1px; + border-left-width: 0; } + .list-group-horizontal-xxl > .list-group-item + .list-group-item.active { + margin-left: -1px; + border-left-width: 1px; } } + +.list-group-flush { + border-radius: 0; } + .list-group-flush > .list-group-item { + border-width: 0 0 1px; } + .list-group-flush > .list-group-item:last-child { + border-bottom-width: 0; } + +.list-group-item-primary { + color: #025584; + background-color: #cde8f8; } + .list-group-item-primary.list-group-item-action:hover, .list-group-item-primary.list-group-item-action:focus { + color: #025584; + background-color: #b9d1df; } + .list-group-item-primary.list-group-item-action.active { + color: #fff; + background-color: #025584; + border-color: #025584; } + +.list-group-item-secondary { + color: #464855; + background-color: #e3e4e8; } + .list-group-item-secondary.list-group-item-action:hover, .list-group-item-secondary.list-group-item-action:focus { + color: #464855; + background-color: #cccdd1; } + .list-group-item-secondary.list-group-item-action.active { + color: #fff; + background-color: #464855; + border-color: #464855; } + +.list-group-item-success { + color: #317e54; + background-color: #dcf6e8; } + .list-group-item-success.list-group-item-action:hover, .list-group-item-success.list-group-item-action:focus { + color: #317e54; + background-color: #c6ddd1; } + .list-group-item-success.list-group-item-action.active { + color: #fff; + background-color: #317e54; + border-color: #317e54; } + +.list-group-item-info { + color: #397d92; + background-color: #dff6fd; } + .list-group-item-info.list-group-item-action:hover, .list-group-item-info.list-group-item-action:focus { + color: #397d92; + background-color: #c9dde4; } + .list-group-item-info.list-group-item-action.active { + color: #fff; + background-color: #397d92; + border-color: #397d92; } + +.list-group-item-warning { + color: #947a32; + background-color: #fdf5dd; } + .list-group-item-warning.list-group-item-action:hover, .list-group-item-warning.list-group-item-action:focus { + color: #947a32; + background-color: #e4ddc7; } + .list-group-item-warning.list-group-item-action.active { + color: #fff; + background-color: #947a32; + border-color: #947a32; } + +.list-group-item-danger { + color: #922f2f; + background-color: #fddcdc; } + .list-group-item-danger.list-group-item-action:hover, .list-group-item-danger.list-group-item-action:focus { + color: #922f2f; + background-color: #e4c6c6; } + .list-group-item-danger.list-group-item-action.active { + color: #fff; + background-color: #922f2f; + border-color: #922f2f; } + +.list-group-item-pink { + color: #8b2554; + background-color: #fad8e8; } + .list-group-item-pink.list-group-item-action:hover, .list-group-item-pink.list-group-item-action:focus { + color: #8b2554; + background-color: #e1c2d1; } + .list-group-item-pink.list-group-item-action.active { + color: #fff; + background-color: #8b2554; + border-color: #8b2554; } + +.list-group-item-light { + color: #939495; + background-color: #fdfdfe; } + .list-group-item-light.list-group-item-action:hover, .list-group-item-light.list-group-item-action:focus { + color: #939495; + background-color: #e4e4e5; } + .list-group-item-light.list-group-item-action.active { + color: #fff; + background-color: #939495; + border-color: #939495; } + +.list-group-item-dark { + color: #1f2326; + background-color: #d6d8d9; } + .list-group-item-dark.list-group-item-action:hover, .list-group-item-dark.list-group-item-action:focus { + color: #1f2326; + background-color: #c1c2c3; } + .list-group-item-dark.list-group-item-action.active { + color: #fff; + background-color: #1f2326; + border-color: #1f2326; } + +.list-group-item-purple { + color: #342c6a; + background-color: #dddbef; } + .list-group-item-purple.list-group-item-action:hover, .list-group-item-purple.list-group-item-action:focus { + color: #342c6a; + background-color: #c7c5d7; } + .list-group-item-purple.list-group-item-action.active { + color: #fff; + background-color: #342c6a; + border-color: #342c6a; } + +.btn-close { + -webkit-box-sizing: content-box; + box-sizing: content-box; + width: 1em; + height: 1em; + padding: 0.25em 0.25em; + color: #000; + background: transparent url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23000'%3e%3cpath d='M.293.293a1 1 0 011.414 0L8 6.586 14.293.293a1 1 0 111.414 1.414L9.414 8l6.293 6.293a1 1 0 01-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 01-1.414-1.414L6.586 8 .293 1.707a1 1 0 010-1.414z'/%3e%3c/svg%3e") center/1em auto no-repeat; + border: 0; + border-radius: 0.25rem; + opacity: 0.5; } + .btn-close:hover { + color: #000; + text-decoration: none; + opacity: 0.75; } + .btn-close:focus { + outline: 0; + -webkit-box-shadow: none; + box-shadow: none; + opacity: 1; } + .btn-close:disabled, .btn-close.disabled { + pointer-events: none; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + opacity: 0.25; } + +.btn-close-white { + -webkit-filter: invert(1) grayscale(100%) brightness(200%); + filter: invert(1) grayscale(100%) brightness(200%); } + +.toast { + width: 350px; + max-width: 100%; + font-size: 0.875rem; + pointer-events: auto; + background-color: rgba(255, 255, 255, 0.85); + background-clip: padding-box; + border: 1px solid rgba(0, 0, 0, 0.1); + -webkit-box-shadow: 0 2px 3px rgba(52, 58, 64, 0.06); + box-shadow: 0 2px 3px rgba(52, 58, 64, 0.06); + border-radius: 0.25rem; } + .toast.showing { + opacity: 0; } + .toast:not(.show) { + display: none; } + +.toast-container { + width: -webkit-max-content; + width: -moz-max-content; + width: max-content; + max-width: 100%; + pointer-events: none; } + .toast-container > :not(:last-child) { + margin-bottom: 10px; } + +.toast-header { + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + padding: 0.5rem 0.75rem; + color: #74788d; + background-color: rgba(255, 255, 255, 0.85); + background-clip: padding-box; + border-bottom: 1px solid rgba(0, 0, 0, 0.05); + border-top-left-radius: calc(0.25rem - 1px); + border-top-right-radius: calc(0.25rem - 1px); } + .toast-header .btn-close { + margin-right: -0.375rem; + margin-left: 0.75rem; } + +.toast-body { + padding: 0.75rem; + word-wrap: break-word; } + +.modal { + position: fixed; + top: 0; + left: 0; + z-index: 1055; + display: none; + width: 100%; + height: 100%; + overflow-x: hidden; + overflow-y: auto; + outline: 0; } + +.modal-dialog { + position: relative; + width: auto; + margin: 0.5rem; + pointer-events: none; } + .modal.fade .modal-dialog { + -webkit-transition: -webkit-transform 0.3s ease-out; + transition: -webkit-transform 0.3s ease-out; + transition: transform 0.3s ease-out; + transition: transform 0.3s ease-out, -webkit-transform 0.3s ease-out; + -webkit-transform: translate(0, -50px); + transform: translate(0, -50px); } + @media (prefers-reduced-motion: reduce) { + .modal.fade .modal-dialog { + -webkit-transition: none; + transition: none; } } + .modal.show .modal-dialog { + -webkit-transform: none; + transform: none; } + .modal.modal-static .modal-dialog { + -webkit-transform: scale(1.02); + transform: scale(1.02); } + +.modal-dialog-scrollable { + height: calc(100% - 1rem); } + .modal-dialog-scrollable .modal-content { + max-height: 100%; + overflow: hidden; } + .modal-dialog-scrollable .modal-body { + overflow-y: auto; } + +.modal-dialog-centered { + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + min-height: calc(100% - 1rem); } + +.modal-content { + position: relative; + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-orient: vertical; + -webkit-box-direction: normal; + -ms-flex-direction: column; + flex-direction: column; + width: 100%; + pointer-events: auto; + background-color: #fff; + background-clip: padding-box; + border: 1px solid #eff0f2; + border-radius: 0.4rem; + outline: 0; } + +.modal-backdrop { + position: fixed; + top: 0; + left: 0; + z-index: 1050; + width: 100vw; + height: 100vh; + background-color: #000; } + .modal-backdrop.fade { + opacity: 0; } + .modal-backdrop.show { + opacity: 0.5; } + +.modal-header { + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -ms-flex-negative: 0; + flex-shrink: 0; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: justify; + -ms-flex-pack: justify; + justify-content: space-between; + padding: 1rem 1rem; + border-bottom: 1px solid #eff0f2; + border-top-left-radius: calc(0.4rem - 1px); + border-top-right-radius: calc(0.4rem - 1px); } + .modal-header .btn-close { + padding: 0.5rem 0.5rem; + margin: -0.5rem -0.5rem -0.5rem auto; } + +.modal-title { + margin-bottom: 0; + line-height: 1.5; } + +.modal-body { + position: relative; + -webkit-box-flex: 1; + -ms-flex: 1 1 auto; + flex: 1 1 auto; + padding: 1rem; } + +.modal-footer { + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -ms-flex-wrap: wrap; + flex-wrap: wrap; + -ms-flex-negative: 0; + flex-shrink: 0; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: end; + -ms-flex-pack: end; + justify-content: flex-end; + padding: 0.75rem; + border-top: 1px solid #eff0f2; + border-bottom-right-radius: calc(0.4rem - 1px); + border-bottom-left-radius: calc(0.4rem - 1px); } + .modal-footer > * { + margin: 0.25rem; } + +@media (min-width: 576px) { + .modal-dialog { + max-width: 500px; + margin: 1.75rem auto; } + .modal-dialog-scrollable { + height: calc(100% - 3.5rem); } + .modal-dialog-centered { + min-height: calc(100% - 3.5rem); } + .modal-sm { + max-width: 300px; } } + +@media (min-width: 992px) { + .modal-lg, + .modal-xl { + max-width: 800px; } } + +@media (min-width: 1200px) { + .modal-xl { + max-width: 1140px; } } + +.modal-fullscreen { + width: 100vw; + max-width: none; + height: 100%; + margin: 0; } + .modal-fullscreen .modal-content { + height: 100%; + border: 0; + border-radius: 0; } + .modal-fullscreen .modal-header { + border-radius: 0; } + .modal-fullscreen .modal-body { + overflow-y: auto; } + .modal-fullscreen .modal-footer { + border-radius: 0; } + +@media (max-width: 575.98px) { + .modal-fullscreen-sm-down { + width: 100vw; + max-width: none; + height: 100%; + margin: 0; } + .modal-fullscreen-sm-down .modal-content { + height: 100%; + border: 0; + border-radius: 0; } + .modal-fullscreen-sm-down .modal-header { + border-radius: 0; } + .modal-fullscreen-sm-down .modal-body { + overflow-y: auto; } + .modal-fullscreen-sm-down .modal-footer { + border-radius: 0; } } + +@media (max-width: 767.98px) { + .modal-fullscreen-md-down { + width: 100vw; + max-width: none; + height: 100%; + margin: 0; } + .modal-fullscreen-md-down .modal-content { + height: 100%; + border: 0; + border-radius: 0; } + .modal-fullscreen-md-down .modal-header { + border-radius: 0; } + .modal-fullscreen-md-down .modal-body { + overflow-y: auto; } + .modal-fullscreen-md-down .modal-footer { + border-radius: 0; } } + +@media (max-width: 991.98px) { + .modal-fullscreen-lg-down { + width: 100vw; + max-width: none; + height: 100%; + margin: 0; } + .modal-fullscreen-lg-down .modal-content { + height: 100%; + border: 0; + border-radius: 0; } + .modal-fullscreen-lg-down .modal-header { + border-radius: 0; } + .modal-fullscreen-lg-down .modal-body { + overflow-y: auto; } + .modal-fullscreen-lg-down .modal-footer { + border-radius: 0; } } + +@media (max-width: 1199.98px) { + .modal-fullscreen-xl-down { + width: 100vw; + max-width: none; + height: 100%; + margin: 0; } + .modal-fullscreen-xl-down .modal-content { + height: 100%; + border: 0; + border-radius: 0; } + .modal-fullscreen-xl-down .modal-header { + border-radius: 0; } + .modal-fullscreen-xl-down .modal-body { + overflow-y: auto; } + .modal-fullscreen-xl-down .modal-footer { + border-radius: 0; } } + +@media (max-width: 1399.98px) { + .modal-fullscreen-xxl-down { + width: 100vw; + max-width: none; + height: 100%; + margin: 0; } + .modal-fullscreen-xxl-down .modal-content { + height: 100%; + border: 0; + border-radius: 0; } + .modal-fullscreen-xxl-down .modal-header { + border-radius: 0; } + .modal-fullscreen-xxl-down .modal-body { + overflow-y: auto; } + .modal-fullscreen-xxl-down .modal-footer { + border-radius: 0; } } + +.tooltip { + position: absolute; + z-index: 999; + display: block; + margin: 0; + font-family: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", "Liberation Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; + font-style: normal; + font-weight: 400; + line-height: 1.5; + text-align: left; + text-align: start; + text-decoration: none; + text-shadow: none; + text-transform: none; + letter-spacing: normal; + word-break: normal; + word-spacing: normal; + white-space: normal; + line-break: auto; + font-size: 0.7875rem; + word-wrap: break-word; + opacity: 0; } + .tooltip.show { + opacity: 0.9; } + .tooltip .tooltip-arrow { + position: absolute; + display: block; + width: 0.8rem; + height: 0.4rem; } + .tooltip .tooltip-arrow::before { + position: absolute; + content: ""; + border-color: transparent; + border-style: solid; } + +.bs-tooltip-top, .bs-tooltip-auto[data-popper-placement^="top"] { + padding: 0.4rem 0; } + .bs-tooltip-top .tooltip-arrow, .bs-tooltip-auto[data-popper-placement^="top"] .tooltip-arrow { + bottom: 0; } + .bs-tooltip-top .tooltip-arrow::before, .bs-tooltip-auto[data-popper-placement^="top"] .tooltip-arrow::before { + top: -1px; + border-width: 0.4rem 0.4rem 0; + border-top-color: #000; } + +.bs-tooltip-end, .bs-tooltip-auto[data-popper-placement^="right"] { + padding: 0 0.4rem; } + .bs-tooltip-end .tooltip-arrow, .bs-tooltip-auto[data-popper-placement^="right"] .tooltip-arrow { + left: 0; + width: 0.4rem; + height: 0.8rem; } + .bs-tooltip-end .tooltip-arrow::before, .bs-tooltip-auto[data-popper-placement^="right"] .tooltip-arrow::before { + right: -1px; + border-width: 0.4rem 0.4rem 0.4rem 0; + border-right-color: #000; } + +.bs-tooltip-bottom, .bs-tooltip-auto[data-popper-placement^="bottom"] { + padding: 0.4rem 0; } + .bs-tooltip-bottom .tooltip-arrow, .bs-tooltip-auto[data-popper-placement^="bottom"] .tooltip-arrow { + top: 0; } + .bs-tooltip-bottom .tooltip-arrow::before, .bs-tooltip-auto[data-popper-placement^="bottom"] .tooltip-arrow::before { + bottom: -1px; + border-width: 0 0.4rem 0.4rem; + border-bottom-color: #000; } + +.bs-tooltip-start, .bs-tooltip-auto[data-popper-placement^="left"] { + padding: 0 0.4rem; } + .bs-tooltip-start .tooltip-arrow, .bs-tooltip-auto[data-popper-placement^="left"] .tooltip-arrow { + right: 0; + width: 0.4rem; + height: 0.8rem; } + .bs-tooltip-start .tooltip-arrow::before, .bs-tooltip-auto[data-popper-placement^="left"] .tooltip-arrow::before { + left: -1px; + border-width: 0.4rem 0 0.4rem 0.4rem; + border-left-color: #000; } + +.tooltip-inner { + max-width: 200px; + padding: 0.4rem 0.7rem; + color: #fff; + text-align: center; + background-color: #000; + border-radius: 0.25rem; } + +.popover { + position: absolute; + top: 0; + left: 0 /* rtl:ignore */; + z-index: 999; + display: block; + max-width: 276px; + font-family: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", "Liberation Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; + font-style: normal; + font-weight: 400; + line-height: 1.5; + text-align: left; + text-align: start; + text-decoration: none; + text-shadow: none; + text-transform: none; + letter-spacing: normal; + word-break: normal; + word-spacing: normal; + white-space: normal; + line-break: auto; + font-size: 0.7875rem; + word-wrap: break-word; + background-color: #fff; + background-clip: padding-box; + border: 1px solid #e2e5e8; + border-radius: 0.4rem; } + .popover .popover-arrow { + position: absolute; + display: block; + width: 1rem; + height: 0.5rem; } + .popover .popover-arrow::before, .popover .popover-arrow::after { + position: absolute; + display: block; + content: ""; + border-color: transparent; + border-style: solid; } + +.bs-popover-top > .popover-arrow, .bs-popover-auto[data-popper-placement^="top"] > .popover-arrow { + bottom: calc(-0.5rem - 1px); } + .bs-popover-top > .popover-arrow::before, .bs-popover-auto[data-popper-placement^="top"] > .popover-arrow::before { + bottom: 0; + border-width: 0.5rem 0.5rem 0; + border-top-color: #e2e5e8; } + .bs-popover-top > .popover-arrow::after, .bs-popover-auto[data-popper-placement^="top"] > .popover-arrow::after { + bottom: 1px; + border-width: 0.5rem 0.5rem 0; + border-top-color: #fff; } + +.bs-popover-end > .popover-arrow, .bs-popover-auto[data-popper-placement^="right"] > .popover-arrow { + left: calc(-0.5rem - 1px); + width: 0.5rem; + height: 1rem; } + .bs-popover-end > .popover-arrow::before, .bs-popover-auto[data-popper-placement^="right"] > .popover-arrow::before { + left: 0; + border-width: 0.5rem 0.5rem 0.5rem 0; + border-right-color: #e2e5e8; } + .bs-popover-end > .popover-arrow::after, .bs-popover-auto[data-popper-placement^="right"] > .popover-arrow::after { + left: 1px; + border-width: 0.5rem 0.5rem 0.5rem 0; + border-right-color: #fff; } + +.bs-popover-bottom > .popover-arrow, .bs-popover-auto[data-popper-placement^="bottom"] > .popover-arrow { + top: calc(-0.5rem - 1px); } + .bs-popover-bottom > .popover-arrow::before, .bs-popover-auto[data-popper-placement^="bottom"] > .popover-arrow::before { + top: 0; + border-width: 0 0.5rem 0.5rem 0.5rem; + border-bottom-color: #e2e5e8; } + .bs-popover-bottom > .popover-arrow::after, .bs-popover-auto[data-popper-placement^="bottom"] > .popover-arrow::after { + top: 1px; + border-width: 0 0.5rem 0.5rem 0.5rem; + border-bottom-color: #fff; } + +.bs-popover-bottom .popover-header::before, .bs-popover-auto[data-popper-placement^="bottom"] .popover-header::before { + position: absolute; + top: 0; + left: 50%; + display: block; + width: 1rem; + margin-left: -0.5rem; + content: ""; + border-bottom: 1px solid #f0f0f0; } + +.bs-popover-start > .popover-arrow, .bs-popover-auto[data-popper-placement^="left"] > .popover-arrow { + right: calc(-0.5rem - 1px); + width: 0.5rem; + height: 1rem; } + .bs-popover-start > .popover-arrow::before, .bs-popover-auto[data-popper-placement^="left"] > .popover-arrow::before { + right: 0; + border-width: 0.5rem 0 0.5rem 0.5rem; + border-left-color: #e2e5e8; } + .bs-popover-start > .popover-arrow::after, .bs-popover-auto[data-popper-placement^="left"] > .popover-arrow::after { + right: 1px; + border-width: 0.5rem 0 0.5rem 0.5rem; + border-left-color: #fff; } + +.popover-header { + padding: 0.5rem 0.75rem; + margin-bottom: 0; + font-size: 0.9rem; + background-color: #f0f0f0; + border-bottom: 1px solid #e2e5e8; + border-top-left-radius: calc(0.4rem - 1px); + border-top-right-radius: calc(0.4rem - 1px); } + .popover-header:empty { + display: none; } + +.popover-body { + padding: 0.5rem 0.75rem; + color: #495057; } + +.carousel { + position: relative; } + +.carousel.pointer-event { + -ms-touch-action: pan-y; + touch-action: pan-y; } + +.carousel-inner { + position: relative; + width: 100%; + overflow: hidden; } + .carousel-inner::after { + display: block; + clear: both; + content: ""; } + +.carousel-item { + position: relative; + display: none; + float: left; + width: 100%; + margin-right: -100%; + -webkit-backface-visibility: hidden; + backface-visibility: hidden; + -webkit-transition: -webkit-transform 0.6s ease-in-out; + transition: -webkit-transform 0.6s ease-in-out; + transition: transform 0.6s ease-in-out; + transition: transform 0.6s ease-in-out, -webkit-transform 0.6s ease-in-out; } + @media (prefers-reduced-motion: reduce) { + .carousel-item { + -webkit-transition: none; + transition: none; } } + +.carousel-item.active, +.carousel-item-next, +.carousel-item-prev { + display: block; } + +/* rtl:begin:ignore */ +.carousel-item-next:not(.carousel-item-start), +.active.carousel-item-end { + -webkit-transform: translateX(100%); + transform: translateX(100%); } + +.carousel-item-prev:not(.carousel-item-end), +.active.carousel-item-start { + -webkit-transform: translateX(-100%); + transform: translateX(-100%); } + +/* rtl:end:ignore */ +.carousel-fade .carousel-item { + opacity: 0; + -webkit-transition-property: opacity; + transition-property: opacity; + -webkit-transform: none; + transform: none; } + +.carousel-fade .carousel-item.active, +.carousel-fade .carousel-item-next.carousel-item-start, +.carousel-fade .carousel-item-prev.carousel-item-end { + z-index: 1; + opacity: 1; } + +.carousel-fade .active.carousel-item-start, +.carousel-fade .active.carousel-item-end { + z-index: 0; + opacity: 0; + -webkit-transition: opacity 0s 0.6s; + transition: opacity 0s 0.6s; } + @media (prefers-reduced-motion: reduce) { + .carousel-fade .active.carousel-item-start, + .carousel-fade .active.carousel-item-end { + -webkit-transition: none; + transition: none; } } + +.carousel-control-prev, +.carousel-control-next { + position: absolute; + top: 0; + bottom: 0; + z-index: 1; + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: center; + -ms-flex-pack: center; + justify-content: center; + width: 15%; + padding: 0; + color: #fff; + text-align: center; + background: none; + border: 0; + opacity: 0.5; + -webkit-transition: opacity 0.15s ease; + transition: opacity 0.15s ease; } + @media (prefers-reduced-motion: reduce) { + .carousel-control-prev, + .carousel-control-next { + -webkit-transition: none; + transition: none; } } + .carousel-control-prev:hover, .carousel-control-prev:focus, + .carousel-control-next:hover, + .carousel-control-next:focus { + color: #fff; + text-decoration: none; + outline: 0; + opacity: 0.9; } + +.carousel-control-prev { + left: 0; } + +.carousel-control-next { + right: 0; } + +.carousel-control-prev-icon, +.carousel-control-next-icon { + display: inline-block; + width: 1rem; + height: 1rem; + background-repeat: no-repeat; + background-position: 50%; + background-size: 100% 100%; } + +/* rtl:options: { + "autoRename": true, + "stringMap":[ { + "name" : "prev-next", + "search" : "prev", + "replace" : "next" + } ] +} */ +.carousel-control-prev-icon { + background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M11.354 1.646a.5.5 0 0 1 0 .708L5.707 8l5.647 5.646a.5.5 0 0 1-.708.708l-6-6a.5.5 0 0 1 0-.708l6-6a.5.5 0 0 1 .708 0z'/%3e%3c/svg%3e"); } + +.carousel-control-next-icon { + background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e"); } + +.carousel-indicators { + position: absolute; + right: 0; + bottom: 0; + left: 0; + z-index: 2; + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-pack: center; + -ms-flex-pack: center; + justify-content: center; + padding: 0; + margin-right: 15%; + margin-bottom: 1rem; + margin-left: 15%; + list-style: none; } + .carousel-indicators [data-bs-target] { + -webkit-box-sizing: content-box; + box-sizing: content-box; + -webkit-box-flex: 0; + -ms-flex: 0 1 auto; + flex: 0 1 auto; + width: 30px; + height: 3px; + padding: 0; + margin-right: 3px; + margin-left: 3px; + text-indent: -999px; + cursor: pointer; + background-color: #fff; + background-clip: padding-box; + border: 0; + border-top: 10px solid transparent; + border-bottom: 10px solid transparent; + opacity: 0.5; + -webkit-transition: opacity 0.6s ease; + transition: opacity 0.6s ease; } + @media (prefers-reduced-motion: reduce) { + .carousel-indicators [data-bs-target] { + -webkit-transition: none; + transition: none; } } + .carousel-indicators .active { + opacity: 1; } + +.carousel-caption { + position: absolute; + right: 15%; + bottom: 1.25rem; + left: 15%; + padding-top: 1.25rem; + padding-bottom: 1.25rem; + color: #fff; + text-align: center; } + +.carousel-dark .carousel-control-prev-icon, +.carousel-dark .carousel-control-next-icon { + -webkit-filter: invert(1) grayscale(100); + filter: invert(1) grayscale(100); } + +.carousel-dark .carousel-indicators [data-bs-target] { + background-color: #000; } + +.carousel-dark .carousel-caption { + color: #000; } + +@-webkit-keyframes spinner-border { + to { + -webkit-transform: rotate(360deg) /* rtl:ignore */; + transform: rotate(360deg) /* rtl:ignore */; } } + +@keyframes spinner-border { + to { + -webkit-transform: rotate(360deg) /* rtl:ignore */; + transform: rotate(360deg) /* rtl:ignore */; } } + +.spinner-border { + display: inline-block; + width: 2rem; + height: 2rem; + vertical-align: -0.125em; + border: 0.25em solid currentColor; + border-right-color: transparent; + border-radius: 50%; + -webkit-animation: 0.75s linear infinite spinner-border; + animation: 0.75s linear infinite spinner-border; } + +.spinner-border-sm { + width: 1rem; + height: 1rem; + border-width: 0.2em; } + +@-webkit-keyframes spinner-grow { + 0% { + -webkit-transform: scale(0); + transform: scale(0); } + 50% { + opacity: 1; + -webkit-transform: none; + transform: none; } } + +@keyframes spinner-grow { + 0% { + -webkit-transform: scale(0); + transform: scale(0); } + 50% { + opacity: 1; + -webkit-transform: none; + transform: none; } } + +.spinner-grow { + display: inline-block; + width: 2rem; + height: 2rem; + vertical-align: -0.125em; + background-color: currentColor; + border-radius: 50%; + opacity: 0; + -webkit-animation: 0.75s linear infinite spinner-grow; + animation: 0.75s linear infinite spinner-grow; } + +.spinner-grow-sm { + width: 1rem; + height: 1rem; } + +@media (prefers-reduced-motion: reduce) { + .spinner-border, + .spinner-grow { + -webkit-animation-duration: 1.5s; + animation-duration: 1.5s; } } + +.offcanvas { + position: fixed; + bottom: 0; + z-index: 1045; + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-orient: vertical; + -webkit-box-direction: normal; + -ms-flex-direction: column; + flex-direction: column; + max-width: 100%; + visibility: hidden; + background-color: #fff; + background-clip: padding-box; + outline: 0; + -webkit-transition: -webkit-transform 0.3s ease-in-out; + transition: -webkit-transform 0.3s ease-in-out; + transition: transform 0.3s ease-in-out; + transition: transform 0.3s ease-in-out, -webkit-transform 0.3s ease-in-out; } + @media (prefers-reduced-motion: reduce) { + .offcanvas { + -webkit-transition: none; + transition: none; } } + +.offcanvas-backdrop { + position: fixed; + top: 0; + left: 0; + z-index: 1040; + width: 100vw; + height: 100vh; + background-color: #000; } + .offcanvas-backdrop.fade { + opacity: 0; } + .offcanvas-backdrop.show { + opacity: 0.5; } + +.offcanvas-header { + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: justify; + -ms-flex-pack: justify; + justify-content: space-between; + padding: 1rem 1rem; } + .offcanvas-header .btn-close { + padding: 0.5rem 0.5rem; + margin-top: -0.5rem; + margin-right: -0.5rem; + margin-bottom: -0.5rem; } + +.offcanvas-title { + margin-bottom: 0; + line-height: 1.5; } + +.offcanvas-body { + -webkit-box-flex: 1; + -ms-flex-positive: 1; + flex-grow: 1; + padding: 1rem 1rem; + overflow-y: auto; } + +.offcanvas-start { + top: 0; + left: 0; + width: 400px; + border-right: 1px solid #eff0f2; + -webkit-transform: translateX(-100%); + transform: translateX(-100%); } + +.offcanvas-end { + top: 0; + right: 0; + width: 400px; + border-left: 1px solid #eff0f2; + -webkit-transform: translateX(100%); + transform: translateX(100%); } + +.offcanvas-top { + top: 0; + right: 0; + left: 0; + height: 30vh; + max-height: 100%; + border-bottom: 1px solid #eff0f2; + -webkit-transform: translateY(-100%); + transform: translateY(-100%); } + +.offcanvas-bottom { + right: 0; + left: 0; + height: 30vh; + max-height: 100%; + border-top: 1px solid #eff0f2; + -webkit-transform: translateY(100%); + transform: translateY(100%); +} + +.fullPageH { + height: 100% !important; +} + +.fullPageW { + width: 100% !important; +} +.offcanvas.show { + -webkit-transform: none; + transform: none; } + +.placeholder { + display: inline-block; + min-height: 1em; + vertical-align: middle; + cursor: wait; + background-color: currentColor; + opacity: 0.5; } + .placeholder.btn::before { + display: inline-block; + content: ""; } + +.placeholder-xs { + min-height: .6em; } + +.placeholder-sm { + min-height: .8em; } + +.placeholder-lg { + min-height: 1.2em; } + +.placeholder-glow .placeholder { + -webkit-animation: placeholder-glow 2s ease-in-out infinite; + animation: placeholder-glow 2s ease-in-out infinite; } + +@-webkit-keyframes placeholder-glow { + 50% { + opacity: 0.2; } } + +@keyframes placeholder-glow { + 50% { + opacity: 0.2; } } + +.placeholder-wave { + -webkit-mask-image: linear-gradient(130deg, #000 55%, rgba(0, 0, 0, 0.8) 75%, #000 95%); + mask-image: linear-gradient(130deg, #000 55%, rgba(0, 0, 0, 0.8) 75%, #000 95%); + -webkit-mask-size: 200% 100%; + mask-size: 200% 100%; + -webkit-animation: placeholder-wave 2s linear infinite; + animation: placeholder-wave 2s linear infinite; } + +@-webkit-keyframes placeholder-wave { + 100% { + -webkit-mask-position: -200% 0%; + mask-position: -200% 0%; } } + +@keyframes placeholder-wave { + 100% { + -webkit-mask-position: -200% 0%; + mask-position: -200% 0%; } } + +.clearfix::after { + display: block; + clear: both; + content: ""; } + +.link-primary { + color: #2e86de; } + .link-primary:hover, .link-primary:focus { + color: #0272b0; } + +.link-secondary { + color: #74788d; } + .link-secondary:hover, .link-secondary:focus { + color: #5d6071; } + +.link-success { + color: #51d28c; } + .link-success:hover, .link-success:focus { + color: #41a870; } + +.link-info { + color: #5fd0f3; } + .link-info:hover, .link-info:focus { + color: #4ca6c2; } + +.link-warning { + color: #f7cc53; } + .link-warning:hover, .link-warning:focus { + color: #f9d675; } + +.link-danger { + color: #f34e4e; } + .link-danger:hover, .link-danger:focus { + color: #c23e3e; } + +.link-pink { + color: #e83e8c; } + .link-pink:hover, .link-pink:focus { + color: #ba3270; } + +.link-light { + color: #f5f6f8; } + .link-light:hover, .link-light:focus { + color: #f7f8f9; } + +.link-dark { + color: #343a40; } + .link-dark:hover, .link-dark:focus { + color: #2a2e33; } + +.link-purple { + color: #564ab1; } + .link-purple:hover, .link-purple:focus { + color: #453b8e; } + +.ratio { + position: relative; + width: 100%; } + .ratio::before { + display: block; + padding-top: var(--bs-aspect-ratio); + content: ""; } + .ratio > * { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; } + +.ratio-1x1 { + --bs-aspect-ratio: 100%; } + +.ratio-4x3 { + --bs-aspect-ratio: calc(3 / 4 * 100%); } + +.ratio-16x9 { + --bs-aspect-ratio: calc(9 / 16 * 100%); } + +.ratio-21x9 { + --bs-aspect-ratio: calc(9 / 21 * 100%); } + +.fixed-top { + position: fixed; + top: 0; + right: 0; + left: 0; + z-index: 1030; } + +.fixed-bottom { + position: fixed; + right: 0; + bottom: 0; + left: 0; + z-index: 1030; } + +.sticky-top { + position: sticky; + top: 0; + z-index: 1020; } + +@media (min-width: 576px) { + .sticky-sm-top { + position: sticky; + top: 0; + z-index: 1020; } } + +@media (min-width: 768px) { + .sticky-md-top { + position: sticky; + top: 0; + z-index: 1020; } } + +@media (min-width: 992px) { + .sticky-lg-top { + position: sticky; + top: 0; + z-index: 1020; } } + +@media (min-width: 1200px) { + .sticky-xl-top { + position: sticky; + top: 0; + z-index: 1020; } } + +@media (min-width: 1400px) { + .sticky-xxl-top { + position: sticky; + top: 0; + z-index: 1020; } } + +.hstack { + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-orient: horizontal; + -webkit-box-direction: normal; + -ms-flex-direction: row; + flex-direction: row; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -ms-flex-item-align: stretch; + align-self: stretch; } + +.vstack { + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-flex: 1; + -ms-flex: 1 1 auto; + flex: 1 1 auto; + -webkit-box-orient: vertical; + -webkit-box-direction: normal; + -ms-flex-direction: column; + flex-direction: column; + -ms-flex-item-align: stretch; + align-self: stretch; } + +.visually-hidden, +.visually-hidden-focusable:not(:focus):not(:focus-within) { + position: absolute !important; + width: 1px !important; + height: 1px !important; + padding: 0 !important; + margin: -1px !important; + overflow: hidden !important; + clip: rect(0, 0, 0, 0) !important; + white-space: nowrap !important; + border: 0 !important; } + +.stretched-link::after { + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + z-index: 1; + content: ""; } + +.text-truncate { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; } + +.vr { + display: inline-block; + -ms-flex-item-align: stretch; + align-self: stretch; + width: 1px; + min-height: 1em; + background-color: currentColor; + opacity: 0.25; } + +.align-baseline { + vertical-align: baseline !important; } + +.align-top { + vertical-align: top !important; } + +.align-middle { + vertical-align: middle !important; } + +.align-bottom { + vertical-align: bottom !important; } + +.align-text-bottom { + vertical-align: text-bottom !important; } + +.align-text-top { + vertical-align: text-top !important; } + +.float-start { + float: left !important; } + +.float-end { + float: right !important; } + +.float-none { + float: none !important; } + +.opacity-0 { + opacity: 0 !important; } + +.opacity-25 { + opacity: 0.25 !important; } + +.opacity-50 { + opacity: 0.5 !important; } + +.opacity-75 { + opacity: 0.75 !important; } + +.opacity-100 { + opacity: 1 !important; } + +.overflow-auto { + overflow: auto !important; } + +.overflow-hidden { + overflow: hidden !important; } + +.overflow-visible { + overflow: visible !important; } + +.overflow-scroll { + overflow: scroll !important; } + +.d-inline { + display: inline !important; } + +.d-inline-block { + display: inline-block !important; } + +.d-block { + display: block !important; } + +.d-grid { + display: grid !important; } + +.d-table { + display: table !important; } + +.d-table-row { + display: table-row !important; } + +.d-table-cell { + display: table-cell !important; } + +.d-flex { + display: -webkit-box !important; + display: -ms-flexbox !important; + display: flex !important; } + +.d-inline-flex { + display: -webkit-inline-box !important; + display: -ms-inline-flexbox !important; + display: inline-flex !important; } + +.d-none { + display: none !important; } + +.shadow { + -webkit-box-shadow: 0 2px 3px rgba(52, 58, 64, 0.06) !important; + box-shadow: 0 2px 3px rgba(52, 58, 64, 0.06) !important; } + +.shadow-sm { + -webkit-box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075) !important; + box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075) !important; } + +.shadow-lg { + -webkit-box-shadow: 0 5px 6px rgba(52, 58, 64, 0.1) !important; + box-shadow: 0 5px 6px rgba(52, 58, 64, 0.1) !important; } + +.shadow-none { + -webkit-box-shadow: none !important; + box-shadow: none !important; } + +.position-static { + position: static !important; } + +.position-relative { + position: relative !important; } + +.position-absolute { + position: absolute !important; } + +.position-fixed { + position: fixed !important; } + +.position-sticky { + position: sticky !important; } + +.top-0 { + top: 0 !important; } + +.top-50 { + top: 50% !important; } + +.top-100 { + top: 100% !important; } + +.bottom-0 { + bottom: 0 !important; } + +.bottom-50 { + bottom: 50% !important; } + +.bottom-100 { + bottom: 100% !important; } + +.start-0 { + left: 0 !important; } + +.start-50 { + left: 50% !important; } + +.start-100 { + left: 100% !important; } + +.end-0 { + right: 0 !important; } + +.end-50 { + right: 50% !important; } + +.end-100 { + right: 100% !important; } + +.translate-middle { + -webkit-transform: translate(-50%, -50%) !important; + transform: translate(-50%, -50%) !important; } + +.translate-middle-x { + -webkit-transform: translateX(-50%) !important; + transform: translateX(-50%) !important; } + +.translate-middle-y { + -webkit-transform: translateY(-50%) !important; + transform: translateY(-50%) !important; } + +.border { + border: 1px solid #eff0f2 !important; } + +.border-0 { + border: 0 !important; } + +.border-top { + border-top: 1px solid #eff0f2 !important; } + +.border-top-0 { + border-top: 0 !important; } + +.border-end { + border-right: 1px solid #eff0f2 !important; } + +.border-end-0 { + border-right: 0 !important; } + +.border-bottom { + border-bottom: 1px solid #eff0f2 !important; } + +.border-bottom-0 { + border-bottom: 0 !important; } + +.border-start { + border-left: 1px solid #eff0f2 !important; } + +.border-start-0 { + border-left: 0 !important; } + +.border-primary { + border-color: #2e86de !important; } + +.border-secondary { + border-color: #74788d !important; } + +.border-success { + border-color: #51d28c !important; } + +.border-info { + border-color: #5fd0f3 !important; } + +.border-warning { + border-color: #f7cc53 !important; } + +.border-danger { + border-color: #f34e4e !important; } + +.border-pink { + border-color: #e83e8c !important; } + +.border-light { + border-color: #f5f6f8 !important; } + +.border-dark { + border-color: #343a40 !important; } + +.border-purple { + border-color: #564ab1 !important; } + +.border-white { + border-color: #fff !important; } + +.border-1 { + border-width: 1px !important; } + +.border-2 { + border-width: 2px !important; } + +.border-3 { + border-width: 3px !important; } + +.border-4 { + border-width: 4px !important; } + +.border-5 { + border-width: 5px !important; } + +.w-25 { + width: 25% !important; } + +.w-50 { + width: 50% !important; } + +.w-75 { + width: 75% !important; } + +.w-100 { + width: 100% !important; } + +.w-auto { + width: auto !important; } + +.mw-100 { + max-width: 100% !important; } + +.vw-100 { + width: 100vw !important; } + +.min-vw-100 { + min-width: 100vw !important; } + +.h-25 { + height: 25% !important; } + +.h-50 { + height: 50% !important; } + +.h-75 { + height: 75% !important; } + +.h-100 { + height: 100% !important; } + +.h-auto { + height: auto !important; } + +.mh-100 { + max-height: 100% !important; } + +.vh-100 { + height: 100vh !important; } + +.min-vh-100 { + min-height: 100vh !important; } + +.flex-fill { + -webkit-box-flex: 1 !important; + -ms-flex: 1 1 auto !important; + flex: 1 1 auto !important; } + +.flex-row { + -webkit-box-orient: horizontal !important; + -webkit-box-direction: normal !important; + -ms-flex-direction: row !important; + flex-direction: row !important; } + +.flex-column { + -webkit-box-orient: vertical !important; + -webkit-box-direction: normal !important; + -ms-flex-direction: column !important; + flex-direction: column !important; } + +.flex-row-reverse { + -webkit-box-orient: horizontal !important; + -webkit-box-direction: reverse !important; + -ms-flex-direction: row-reverse !important; + flex-direction: row-reverse !important; } + +.flex-column-reverse { + -webkit-box-orient: vertical !important; + -webkit-box-direction: reverse !important; + -ms-flex-direction: column-reverse !important; + flex-direction: column-reverse !important; } + +.flex-grow-0 { + -webkit-box-flex: 0 !important; + -ms-flex-positive: 0 !important; + flex-grow: 0 !important; } + +.flex-grow-1 { + -webkit-box-flex: 1 !important; + -ms-flex-positive: 1 !important; + flex-grow: 1 !important; } + +.flex-shrink-0 { + -ms-flex-negative: 0 !important; + flex-shrink: 0 !important; } + +.flex-shrink-1 { + -ms-flex-negative: 1 !important; + flex-shrink: 1 !important; } + +.flex-wrap { + -ms-flex-wrap: wrap !important; + flex-wrap: wrap !important; } + +.flex-nowrap { + -ms-flex-wrap: nowrap !important; + flex-wrap: nowrap !important; } + +.flex-wrap-reverse { + -ms-flex-wrap: wrap-reverse !important; + flex-wrap: wrap-reverse !important; } + +.gap-0 { + gap: 0 !important; } + +.gap-1 { + gap: 0.25rem !important; } + +.gap-2 { + gap: 0.5rem !important; } + +.gap-3 { + gap: 1rem !important; } + +.gap-4 { + gap: 1.5rem !important; } + +.gap-5 { + gap: 3rem !important; } + +.justify-content-start { + -webkit-box-pack: start !important; + -ms-flex-pack: start !important; + justify-content: flex-start !important; } + +.justify-content-end { + -webkit-box-pack: end !important; + -ms-flex-pack: end !important; + justify-content: flex-end !important; } + +.justify-content-center { + -webkit-box-pack: center !important; + -ms-flex-pack: center !important; + justify-content: center !important; } + +.justify-content-between { + -webkit-box-pack: justify !important; + -ms-flex-pack: justify !important; + justify-content: space-between !important; } + +.justify-content-around { + -ms-flex-pack: distribute !important; + justify-content: space-around !important; } + +.justify-content-evenly { + -webkit-box-pack: space-evenly !important; + -ms-flex-pack: space-evenly !important; + justify-content: space-evenly !important; } + +.align-items-start { + -webkit-box-align: start !important; + -ms-flex-align: start !important; + align-items: flex-start !important; } + +.align-items-end { + -webkit-box-align: end !important; + -ms-flex-align: end !important; + align-items: flex-end !important; } + +.align-items-center { + -webkit-box-align: center !important; + -ms-flex-align: center !important; + align-items: center !important; } + +.align-items-baseline { + -webkit-box-align: baseline !important; + -ms-flex-align: baseline !important; + align-items: baseline !important; } + +.align-items-stretch { + -webkit-box-align: stretch !important; + -ms-flex-align: stretch !important; + align-items: stretch !important; } + +.align-content-start { + -ms-flex-line-pack: start !important; + align-content: flex-start !important; } + +.align-content-end { + -ms-flex-line-pack: end !important; + align-content: flex-end !important; } + +.align-content-center { + -ms-flex-line-pack: center !important; + align-content: center !important; } + +.align-content-between { + -ms-flex-line-pack: justify !important; + align-content: space-between !important; } + +.align-content-around { + -ms-flex-line-pack: distribute !important; + align-content: space-around !important; } + +.align-content-stretch { + -ms-flex-line-pack: stretch !important; + align-content: stretch !important; } + +.align-self-auto { + -ms-flex-item-align: auto !important; + align-self: auto !important; } + +.align-self-start { + -ms-flex-item-align: start !important; + align-self: flex-start !important; } + +.align-self-end { + -ms-flex-item-align: end !important; + align-self: flex-end !important; } + +.align-self-center { + -ms-flex-item-align: center !important; + align-self: center !important; } + +.align-self-baseline { + -ms-flex-item-align: baseline !important; + align-self: baseline !important; } + +.align-self-stretch { + -ms-flex-item-align: stretch !important; + align-self: stretch !important; } + +.order-first { + -webkit-box-ordinal-group: 0 !important; + -ms-flex-order: -1 !important; + order: -1 !important; } + +.order-0 { + -webkit-box-ordinal-group: 1 !important; + -ms-flex-order: 0 !important; + order: 0 !important; } + +.order-1 { + -webkit-box-ordinal-group: 2 !important; + -ms-flex-order: 1 !important; + order: 1 !important; } + +.order-2 { + -webkit-box-ordinal-group: 3 !important; + -ms-flex-order: 2 !important; + order: 2 !important; } + +.order-3 { + -webkit-box-ordinal-group: 4 !important; + -ms-flex-order: 3 !important; + order: 3 !important; } + +.order-4 { + -webkit-box-ordinal-group: 5 !important; + -ms-flex-order: 4 !important; + order: 4 !important; } + +.order-5 { + -webkit-box-ordinal-group: 6 !important; + -ms-flex-order: 5 !important; + order: 5 !important; } + +.order-last { + -webkit-box-ordinal-group: 7 !important; + -ms-flex-order: 6 !important; + order: 6 !important; } + +.m-0 { + margin: 0 !important; } + +.m-1 { + margin: 0.25rem !important; } + +.m-2 { + margin: 0.5rem !important; } + +.m-3 { + margin: 1rem !important; } + +.m-4 { + margin: 1.5rem !important; } + +.m-5 { + margin: 3rem !important; } + +.m-auto { + margin: auto !important; } + +.mx-0 { + margin-right: 0 !important; + margin-left: 0 !important; } + +.mx-1 { + margin-right: 0.25rem !important; + margin-left: 0.25rem !important; } + +.mx-2 { + margin-right: 0.5rem !important; + margin-left: 0.5rem !important; } + +.mx-3 { + margin-right: 1rem !important; + margin-left: 1rem !important; } + +.mx-4 { + margin-right: 1.5rem !important; + margin-left: 1.5rem !important; } + +.mx-5 { + margin-right: 3rem !important; + margin-left: 3rem !important; } + +.mx-auto { + margin-right: auto !important; + margin-left: auto !important; } + +.my-0 { + margin-top: 0 !important; + margin-bottom: 0 !important; } + +.my-1 { + margin-top: 0.25rem !important; + margin-bottom: 0.25rem !important; } + +.my-2 { + margin-top: 0.5rem !important; + margin-bottom: 0.5rem !important; } + +.my-3 { + margin-top: 1rem !important; + margin-bottom: 1rem !important; } + +.my-4 { + margin-top: 1.5rem !important; + margin-bottom: 1.5rem !important; } + +.my-5 { + margin-top: 3rem !important; + margin-bottom: 3rem !important; } + +.my-auto { + margin-top: auto !important; + margin-bottom: auto !important; } + +.mt-0 { + margin-top: 0 !important; } + +.mt-1 { + margin-top: 0.25rem !important; } + +.mt-2 { + margin-top: 0.5rem !important; } + +.mt-3 { + margin-top: 1rem !important; } + +.mt-4 { + margin-top: 1.5rem !important; } + +.mt-5 { + margin-top: 3rem !important; } + +.mt-auto { + margin-top: auto !important; } + +.me-0 { + margin-right: 0 !important; } + +.me-1 { + margin-right: 0.25rem !important; } + +.me-2 { + margin-right: 0.5rem !important; } + +.me-3 { + margin-right: 1rem !important; } + +.me-4 { + margin-right: 1.5rem !important; } + +.me-5 { + margin-right: 3rem !important; } + +.me-auto { + margin-right: auto !important; } + +.mb-0 { + margin-bottom: 0 !important; } + +.mb-1 { + margin-bottom: 0.25rem !important; } + +.mb-2 { + margin-bottom: 0.5rem !important; } + +.mb-3 { + margin-bottom: 1rem !important; } + +.mb-4 { + margin-bottom: 1.5rem !important; } + +.mb-5 { + margin-bottom: 3rem !important; } + +.mb-auto { + margin-bottom: auto !important; } + +.ms-0 { + margin-left: 0 !important; } + +.ms-1 { + margin-left: 0.25rem !important; } + +.ms-2 { + margin-left: 0.5rem !important; } + +.ms-3 { + margin-left: 1rem !important; } + +.ms-4 { + margin-left: 1.5rem !important; } + +.ms-5 { + margin-left: 3rem !important; } + +.ms-auto { + margin-left: auto !important; } + +.m-n1 { + margin: -0.25rem !important; } + +.m-n2 { + margin: -0.5rem !important; } + +.m-n3 { + margin: -1rem !important; } + +.m-n4 { + margin: -1.5rem !important; } + +.m-n5 { + margin: -3rem !important; } + +.mx-n1 { + margin-right: -0.25rem !important; + margin-left: -0.25rem !important; } + +.mx-n2 { + margin-right: -0.5rem !important; + margin-left: -0.5rem !important; } + +.mx-n3 { + margin-right: -1rem !important; + margin-left: -1rem !important; } + +.mx-n4 { + margin-right: -1.5rem !important; + margin-left: -1.5rem !important; } + +.mx-n5 { + margin-right: -3rem !important; + margin-left: -3rem !important; } + +.my-n1 { + margin-top: -0.25rem !important; + margin-bottom: -0.25rem !important; } + +.my-n2 { + margin-top: -0.5rem !important; + margin-bottom: -0.5rem !important; } + +.my-n3 { + margin-top: -1rem !important; + margin-bottom: -1rem !important; } + +.my-n4 { + margin-top: -1.5rem !important; + margin-bottom: -1.5rem !important; } + +.my-n5 { + margin-top: -3rem !important; + margin-bottom: -3rem !important; } + +.mt-n1 { + margin-top: -0.25rem !important; } + +.mt-n2 { + margin-top: -0.5rem !important; } + +.mt-n3 { + margin-top: -1rem !important; } + +.mt-n4 { + margin-top: -1.5rem !important; } + +.mt-n5 { + margin-top: -3rem !important; } + +.me-n1 { + margin-right: -0.25rem !important; } + +.me-n2 { + margin-right: -0.5rem !important; } + +.me-n3 { + margin-right: -1rem !important; } + +.me-n4 { + margin-right: -1.5rem !important; } + +.me-n5 { + margin-right: -3rem !important; } + +.mb-n1 { + margin-bottom: -0.25rem !important; } + +.mb-n2 { + margin-bottom: -0.5rem !important; } + +.mb-n3 { + margin-bottom: -1rem !important; } + +.mb-n4 { + margin-bottom: -1.5rem !important; } + +.mb-n5 { + margin-bottom: -3rem !important; } + +.ms-n1 { + margin-left: -0.25rem !important; } + +.ms-n2 { + margin-left: -0.5rem !important; } + +.ms-n3 { + margin-left: -1rem !important; } + +.ms-n4 { + margin-left: -1.5rem !important; } + +.ms-n5 { + margin-left: -3rem !important; } + +.p-0 { + padding: 0 !important; } + +.p-1 { + padding: 0.25rem !important; } + +.p-2 { + padding: 0.5rem !important; } + +.p-3 { + padding: 1rem !important; } + +.p-4 { + padding: 1.5rem !important; } + +.p-5 { + padding: 3rem !important; } + +.px-0 { + padding-right: 0 !important; + padding-left: 0 !important; } + +.px-1 { + padding-right: 0.25rem !important; + padding-left: 0.25rem !important; } + +.px-2 { + padding-right: 0.5rem !important; + padding-left: 0.5rem !important; } + +.px-3 { + padding-right: 1rem !important; + padding-left: 1rem !important; } + +.px-4 { + padding-right: 1.5rem !important; + padding-left: 1.5rem !important; } + +.px-5 { + padding-right: 3rem !important; + padding-left: 3rem !important; } + +.py-0 { + padding-top: 0 !important; + padding-bottom: 0 !important; } + +.py-1 { + padding-top: 0.25rem !important; + padding-bottom: 0.25rem !important; } + +.py-2 { + padding-top: 0.5rem !important; + padding-bottom: 0.5rem !important; } + +.py-3 { + padding-top: 1rem !important; + padding-bottom: 1rem !important; } + +.py-4 { + padding-top: 1.5rem !important; + padding-bottom: 1.5rem !important; } + +.py-5 { + padding-top: 3rem !important; + padding-bottom: 3rem !important; } + +.pt-0 { + padding-top: 0 !important; } + +.pt-1 { + padding-top: 0.25rem !important; } + +.pt-2 { + padding-top: 0.5rem !important; } + +.pt-3 { + padding-top: 1rem !important; } + +.pt-4 { + padding-top: 1.5rem !important; } + +.pt-5 { + padding-top: 3rem !important; } + +.pe-0 { + padding-right: 0 !important; } + +.pe-1 { + padding-right: 0.25rem !important; } + +.pe-2 { + padding-right: 0.5rem !important; } + +.pe-3 { + padding-right: 1rem !important; } + +.pe-4 { + padding-right: 1.5rem !important; } + +.pe-5 { + padding-right: 3rem !important; } + +.pb-0 { + padding-bottom: 0 !important; } + +.pb-1 { + padding-bottom: 0.25rem !important; } + +.pb-2 { + padding-bottom: 0.5rem !important; } + +.pb-3 { + padding-bottom: 1rem !important; } + +.pb-4 { + padding-bottom: 1.5rem !important; } + +.pb-5 { + padding-bottom: 3rem !important; } + +.ps-0 { + padding-left: 0 !important; } + +.ps-1 { + padding-left: 0.25rem !important; } + +.ps-2 { + padding-left: 0.5rem !important; } + +.ps-3 { + padding-left: 1rem !important; } + +.ps-4 { + padding-left: 1.5rem !important; } + +.ps-5 { + padding-left: 3rem !important; } + +.font-monospace { + font-family: var(--bs-font-monospace) !important; } + +.fs-1 { + font-size: calc(1.35rem + 1.2vw) !important; } + +.fs-2 { + font-size: calc(1.305rem + 0.66vw) !important; } + +.fs-3 { + font-size: calc(1.2825rem + 0.39vw) !important; } + +.fs-4 { + font-size: calc(1.26rem + 0.12vw) !important; } + +.fs-5 { + font-size: 1.125rem !important; } + +.fs-6 { + font-size: 0.9rem !important; } + +.fst-italic { + font-style: italic !important; } + +.fst-normal { + font-style: normal !important; } + +.fw-light { + font-weight: 300 !important; } + +.fw-lighter { + font-weight: lighter !important; } + +.fw-normal { + font-weight: 400 !important; } + +.fw-bold { + font-weight: 700 !important; } + +.fw-bolder { + font-weight: bolder !important; } + +.lh-1 { + line-height: 1 !important; } + +.lh-sm { + line-height: 1.25 !important; } + +.lh-base { + line-height: 1.5 !important; } + +.lh-lg { + line-height: 2 !important; } + +.text-start { + text-align: left !important; } + +.text-end { + text-align: right !important; } + +.text-center { + text-align: center !important; } + +.text-decoration-none { + text-decoration: none !important; } + +.text-decoration-underline { + text-decoration: underline !important; } + +.text-decoration-line-through { + text-decoration: line-through !important; } + +.text-lowercase { + text-transform: lowercase !important; } + +.text-uppercase { + text-transform: uppercase !important; } + +.text-capitalize { + text-transform: capitalize !important; } + +.text-wrap { + white-space: normal !important; } + +.text-nowrap { + white-space: nowrap !important; } + +/* rtl:begin:remove */ +.text-break { + word-wrap: break-word !important; + word-break: break-word !important; } + +/* rtl:end:remove */ +.text-primary { + --bs-text-opacity: 1; + color: rgba(var(--bs-primary-rgb), var(--bs-text-opacity)) !important; } + +.text-secondary { + --bs-text-opacity: 1; + color: rgba(var(--bs-secondary-rgb), var(--bs-text-opacity)) !important; } + +.text-success { + --bs-text-opacity: 1; + color: rgba(var(--bs-success-rgb), var(--bs-text-opacity)) !important; } + +.text-info { + --bs-text-opacity: 1; + color: rgba(var(--bs-info-rgb), var(--bs-text-opacity)) !important; } + +.text-warning { + --bs-text-opacity: 1; + color: rgba(var(--bs-warning-rgb), var(--bs-text-opacity)) !important; } + +.text-danger { + --bs-text-opacity: 1; + color: rgba(var(--bs-danger-rgb), var(--bs-text-opacity)) !important; } + +.text-pink { + --bs-text-opacity: 1; + color: rgba(var(--bs-pink-rgb), var(--bs-text-opacity)) !important; } + +.text-light { + --bs-text-opacity: 1; + color: rgba(var(--bs-light-rgb), var(--bs-text-opacity)) !important; } + +.text-dark { + --bs-text-opacity: 1; + color: rgba(var(--bs-dark-rgb), var(--bs-text-opacity)) !important; } + +.text-purple { + --bs-text-opacity: 1; + color: rgba(var(--bs-purple-rgb), var(--bs-text-opacity)) !important; } + +.text-black { + --bs-text-opacity: 1; + color: rgba(var(--bs-black-rgb), var(--bs-text-opacity)) !important; } + +.text-white { + --bs-text-opacity: 1; + color: rgba(var(--bs-white-rgb), var(--bs-text-opacity)) !important; } + +.text-body { + --bs-text-opacity: 1; + color: rgba(var(--bs-body-color-rgb), var(--bs-text-opacity)) !important; } + +.text-muted { + --bs-text-opacity: 1; + color: #74788d !important; } + +.text-black-50 { + --bs-text-opacity: 1; + color: rgba(0, 0, 0, 0.5) !important; } + +.text-white-50 { + --bs-text-opacity: 1; + color: rgba(255, 255, 255, 0.5) !important; } + +.text-reset { + --bs-text-opacity: 1; + color: inherit !important; } + +.text-opacity-25 { + --bs-text-opacity: 0.25; } + +.text-opacity-50 { + --bs-text-opacity: 0.5; } + +.text-opacity-75 { + --bs-text-opacity: 0.75; } + +.text-opacity-100 { + --bs-text-opacity: 1; } + +.bg-primary { + --bs-bg-opacity: 1; + background-color: rgba(var(--bs-primary-rgb), var(--bs-bg-opacity)) !important; } + +.bg-secondary { + --bs-bg-opacity: 1; + background-color: rgba(var(--bs-secondary-rgb), var(--bs-bg-opacity)) !important; } + +.bg-success { + --bs-bg-opacity: 1; + background-color: rgba(var(--bs-success-rgb), var(--bs-bg-opacity)) !important; } + +.bg-info { + --bs-bg-opacity: 1; + background-color: rgba(var(--bs-info-rgb), var(--bs-bg-opacity)) !important; } + +.bg-warning { + --bs-bg-opacity: 1; + background-color: rgba(var(--bs-warning-rgb), var(--bs-bg-opacity)) !important; } + +.bg-danger { + --bs-bg-opacity: 1; + background-color: rgba(var(--bs-danger-rgb), var(--bs-bg-opacity)) !important; } + +.bg-pink { + --bs-bg-opacity: 1; + background-color: rgba(var(--bs-pink-rgb), var(--bs-bg-opacity)) !important; } + +.bg-light { + --bs-bg-opacity: 1; + background-color: rgba(var(--bs-light-rgb), var(--bs-bg-opacity)) !important; } + +.bg-dark { + --bs-bg-opacity: 1; + background-color: rgba(var(--bs-dark-rgb), var(--bs-bg-opacity)) !important; } + +.bg-purple { + --bs-bg-opacity: 1; + background-color: rgba(var(--bs-purple-rgb), var(--bs-bg-opacity)) !important; } + +.bg-black { + --bs-bg-opacity: 1; + background-color: rgba(var(--bs-black-rgb), var(--bs-bg-opacity)) !important; } + +.bg-white { + --bs-bg-opacity: 1; + background-color: rgba(var(--bs-white-rgb), var(--bs-bg-opacity)) !important; } + +.bg-body { + --bs-bg-opacity: 1; + background-color: rgba(var(--bs-body-bg-rgb), var(--bs-bg-opacity)) !important; } + +.bg-transparent { + --bs-bg-opacity: 1; + background-color: transparent !important; } + +.bg-opacity-10 { + --bs-bg-opacity: 0.1; } + +.bg-opacity-25 { + --bs-bg-opacity: 0.25; } + +.bg-opacity-50 { + --bs-bg-opacity: 0.5; } + +.bg-opacity-75 { + --bs-bg-opacity: 0.75; } + +.bg-opacity-100 { + --bs-bg-opacity: 1; } + +.bg-gradient { + background-image: var(--bs-gradient) !important; } + +.user-select-all { + -webkit-user-select: all !important; + -moz-user-select: all !important; + -ms-user-select: all !important; + user-select: all !important; } + +.user-select-auto { + -webkit-user-select: auto !important; + -moz-user-select: auto !important; + -ms-user-select: auto !important; + user-select: auto !important; } + +.user-select-none { + -webkit-user-select: none !important; + -moz-user-select: none !important; + -ms-user-select: none !important; + user-select: none !important; } + +.pe-none { + pointer-events: none !important; } + +.pe-auto { + pointer-events: auto !important; } + +.rounded { + border-radius: 0.25rem !important; } + +.rounded-0 { + border-radius: 0 !important; } + +.rounded-1 { + border-radius: 0.2rem !important; } + +.rounded-2 { + border-radius: 0.25rem !important; } + +.rounded-3 { + border-radius: 0.4rem !important; } + +.rounded-circle { + border-radius: 50% !important; } + +.rounded-pill { + border-radius: 50rem !important; } + +.rounded-top { + border-top-left-radius: 0.25rem !important; + border-top-right-radius: 0.25rem !important; } + +.rounded-end { + border-top-right-radius: 0.25rem !important; + border-bottom-right-radius: 0.25rem !important; } + +.rounded-bottom { + border-bottom-right-radius: 0.25rem !important; + border-bottom-left-radius: 0.25rem !important; } + +.rounded-start { + border-bottom-left-radius: 0.25rem !important; + border-top-left-radius: 0.25rem !important; } + +.visible { + visibility: visible !important; } + +.invisible { + visibility: hidden !important; } + +@media (min-width: 576px) { + .float-sm-start { + float: left !important; } + .float-sm-end { + float: right !important; } + .float-sm-none { + float: none !important; } + .d-sm-inline { + display: inline !important; } + .d-sm-inline-block { + display: inline-block !important; } + .d-sm-block { + display: block !important; } + .d-sm-grid { + display: grid !important; } + .d-sm-table { + display: table !important; } + .d-sm-table-row { + display: table-row !important; } + .d-sm-table-cell { + display: table-cell !important; } + .d-sm-flex { + display: -webkit-box !important; + display: -ms-flexbox !important; + display: flex !important; } + .d-sm-inline-flex { + display: -webkit-inline-box !important; + display: -ms-inline-flexbox !important; + display: inline-flex !important; } + .d-sm-none { + display: none !important; } + .flex-sm-fill { + -webkit-box-flex: 1 !important; + -ms-flex: 1 1 auto !important; + flex: 1 1 auto !important; } + .flex-sm-row { + -webkit-box-orient: horizontal !important; + -webkit-box-direction: normal !important; + -ms-flex-direction: row !important; + flex-direction: row !important; } + .flex-sm-column { + -webkit-box-orient: vertical !important; + -webkit-box-direction: normal !important; + -ms-flex-direction: column !important; + flex-direction: column !important; } + .flex-sm-row-reverse { + -webkit-box-orient: horizontal !important; + -webkit-box-direction: reverse !important; + -ms-flex-direction: row-reverse !important; + flex-direction: row-reverse !important; } + .flex-sm-column-reverse { + -webkit-box-orient: vertical !important; + -webkit-box-direction: reverse !important; + -ms-flex-direction: column-reverse !important; + flex-direction: column-reverse !important; } + .flex-sm-grow-0 { + -webkit-box-flex: 0 !important; + -ms-flex-positive: 0 !important; + flex-grow: 0 !important; } + .flex-sm-grow-1 { + -webkit-box-flex: 1 !important; + -ms-flex-positive: 1 !important; + flex-grow: 1 !important; } + .flex-sm-shrink-0 { + -ms-flex-negative: 0 !important; + flex-shrink: 0 !important; } + .flex-sm-shrink-1 { + -ms-flex-negative: 1 !important; + flex-shrink: 1 !important; } + .flex-sm-wrap { + -ms-flex-wrap: wrap !important; + flex-wrap: wrap !important; } + .flex-sm-nowrap { + -ms-flex-wrap: nowrap !important; + flex-wrap: nowrap !important; } + .flex-sm-wrap-reverse { + -ms-flex-wrap: wrap-reverse !important; + flex-wrap: wrap-reverse !important; } + .gap-sm-0 { + gap: 0 !important; } + .gap-sm-1 { + gap: 0.25rem !important; } + .gap-sm-2 { + gap: 0.5rem !important; } + .gap-sm-3 { + gap: 1rem !important; } + .gap-sm-4 { + gap: 1.5rem !important; } + .gap-sm-5 { + gap: 3rem !important; } + .justify-content-sm-start { + -webkit-box-pack: start !important; + -ms-flex-pack: start !important; + justify-content: flex-start !important; } + .justify-content-sm-end { + -webkit-box-pack: end !important; + -ms-flex-pack: end !important; + justify-content: flex-end !important; } + .justify-content-sm-center { + -webkit-box-pack: center !important; + -ms-flex-pack: center !important; + justify-content: center !important; } + .justify-content-sm-between { + -webkit-box-pack: justify !important; + -ms-flex-pack: justify !important; + justify-content: space-between !important; } + .justify-content-sm-around { + -ms-flex-pack: distribute !important; + justify-content: space-around !important; } + .justify-content-sm-evenly { + -webkit-box-pack: space-evenly !important; + -ms-flex-pack: space-evenly !important; + justify-content: space-evenly !important; } + .align-items-sm-start { + -webkit-box-align: start !important; + -ms-flex-align: start !important; + align-items: flex-start !important; } + .align-items-sm-end { + -webkit-box-align: end !important; + -ms-flex-align: end !important; + align-items: flex-end !important; } + .align-items-sm-center { + -webkit-box-align: center !important; + -ms-flex-align: center !important; + align-items: center !important; } + .align-items-sm-baseline { + -webkit-box-align: baseline !important; + -ms-flex-align: baseline !important; + align-items: baseline !important; } + .align-items-sm-stretch { + -webkit-box-align: stretch !important; + -ms-flex-align: stretch !important; + align-items: stretch !important; } + .align-content-sm-start { + -ms-flex-line-pack: start !important; + align-content: flex-start !important; } + .align-content-sm-end { + -ms-flex-line-pack: end !important; + align-content: flex-end !important; } + .align-content-sm-center { + -ms-flex-line-pack: center !important; + align-content: center !important; } + .align-content-sm-between { + -ms-flex-line-pack: justify !important; + align-content: space-between !important; } + .align-content-sm-around { + -ms-flex-line-pack: distribute !important; + align-content: space-around !important; } + .align-content-sm-stretch { + -ms-flex-line-pack: stretch !important; + align-content: stretch !important; } + .align-self-sm-auto { + -ms-flex-item-align: auto !important; + align-self: auto !important; } + .align-self-sm-start { + -ms-flex-item-align: start !important; + align-self: flex-start !important; } + .align-self-sm-end { + -ms-flex-item-align: end !important; + align-self: flex-end !important; } + .align-self-sm-center { + -ms-flex-item-align: center !important; + align-self: center !important; } + .align-self-sm-baseline { + -ms-flex-item-align: baseline !important; + align-self: baseline !important; } + .align-self-sm-stretch { + -ms-flex-item-align: stretch !important; + align-self: stretch !important; } + .order-sm-first { + -webkit-box-ordinal-group: 0 !important; + -ms-flex-order: -1 !important; + order: -1 !important; } + .order-sm-0 { + -webkit-box-ordinal-group: 1 !important; + -ms-flex-order: 0 !important; + order: 0 !important; } + .order-sm-1 { + -webkit-box-ordinal-group: 2 !important; + -ms-flex-order: 1 !important; + order: 1 !important; } + .order-sm-2 { + -webkit-box-ordinal-group: 3 !important; + -ms-flex-order: 2 !important; + order: 2 !important; } + .order-sm-3 { + -webkit-box-ordinal-group: 4 !important; + -ms-flex-order: 3 !important; + order: 3 !important; } + .order-sm-4 { + -webkit-box-ordinal-group: 5 !important; + -ms-flex-order: 4 !important; + order: 4 !important; } + .order-sm-5 { + -webkit-box-ordinal-group: 6 !important; + -ms-flex-order: 5 !important; + order: 5 !important; } + .order-sm-last { + -webkit-box-ordinal-group: 7 !important; + -ms-flex-order: 6 !important; + order: 6 !important; } + .m-sm-0 { + margin: 0 !important; } + .m-sm-1 { + margin: 0.25rem !important; } + .m-sm-2 { + margin: 0.5rem !important; } + .m-sm-3 { + margin: 1rem !important; } + .m-sm-4 { + margin: 1.5rem !important; } + .m-sm-5 { + margin: 3rem !important; } + .m-sm-auto { + margin: auto !important; } + .mx-sm-0 { + margin-right: 0 !important; + margin-left: 0 !important; } + .mx-sm-1 { + margin-right: 0.25rem !important; + margin-left: 0.25rem !important; } + .mx-sm-2 { + margin-right: 0.5rem !important; + margin-left: 0.5rem !important; } + .mx-sm-3 { + margin-right: 1rem !important; + margin-left: 1rem !important; } + .mx-sm-4 { + margin-right: 1.5rem !important; + margin-left: 1.5rem !important; } + .mx-sm-5 { + margin-right: 3rem !important; + margin-left: 3rem !important; } + .mx-sm-auto { + margin-right: auto !important; + margin-left: auto !important; } + .my-sm-0 { + margin-top: 0 !important; + margin-bottom: 0 !important; } + .my-sm-1 { + margin-top: 0.25rem !important; + margin-bottom: 0.25rem !important; } + .my-sm-2 { + margin-top: 0.5rem !important; + margin-bottom: 0.5rem !important; } + .my-sm-3 { + margin-top: 1rem !important; + margin-bottom: 1rem !important; } + .my-sm-4 { + margin-top: 1.5rem !important; + margin-bottom: 1.5rem !important; } + .my-sm-5 { + margin-top: 3rem !important; + margin-bottom: 3rem !important; } + .my-sm-auto { + margin-top: auto !important; + margin-bottom: auto !important; } + .mt-sm-0 { + margin-top: 0 !important; } + .mt-sm-1 { + margin-top: 0.25rem !important; } + .mt-sm-2 { + margin-top: 0.5rem !important; } + .mt-sm-3 { + margin-top: 1rem !important; } + .mt-sm-4 { + margin-top: 1.5rem !important; } + .mt-sm-5 { + margin-top: 3rem !important; } + .mt-sm-auto { + margin-top: auto !important; } + .me-sm-0 { + margin-right: 0 !important; } + .me-sm-1 { + margin-right: 0.25rem !important; } + .me-sm-2 { + margin-right: 0.5rem !important; } + .me-sm-3 { + margin-right: 1rem !important; } + .me-sm-4 { + margin-right: 1.5rem !important; } + .me-sm-5 { + margin-right: 3rem !important; } + .me-sm-auto { + margin-right: auto !important; } + .mb-sm-0 { + margin-bottom: 0 !important; } + .mb-sm-1 { + margin-bottom: 0.25rem !important; } + .mb-sm-2 { + margin-bottom: 0.5rem !important; } + .mb-sm-3 { + margin-bottom: 1rem !important; } + .mb-sm-4 { + margin-bottom: 1.5rem !important; } + .mb-sm-5 { + margin-bottom: 3rem !important; } + .mb-sm-auto { + margin-bottom: auto !important; } + .ms-sm-0 { + margin-left: 0 !important; } + .ms-sm-1 { + margin-left: 0.25rem !important; } + .ms-sm-2 { + margin-left: 0.5rem !important; } + .ms-sm-3 { + margin-left: 1rem !important; } + .ms-sm-4 { + margin-left: 1.5rem !important; } + .ms-sm-5 { + margin-left: 3rem !important; } + .ms-sm-auto { + margin-left: auto !important; } + .m-sm-n1 { + margin: -0.25rem !important; } + .m-sm-n2 { + margin: -0.5rem !important; } + .m-sm-n3 { + margin: -1rem !important; } + .m-sm-n4 { + margin: -1.5rem !important; } + .m-sm-n5 { + margin: -3rem !important; } + .mx-sm-n1 { + margin-right: -0.25rem !important; + margin-left: -0.25rem !important; } + .mx-sm-n2 { + margin-right: -0.5rem !important; + margin-left: -0.5rem !important; } + .mx-sm-n3 { + margin-right: -1rem !important; + margin-left: -1rem !important; } + .mx-sm-n4 { + margin-right: -1.5rem !important; + margin-left: -1.5rem !important; } + .mx-sm-n5 { + margin-right: -3rem !important; + margin-left: -3rem !important; } + .my-sm-n1 { + margin-top: -0.25rem !important; + margin-bottom: -0.25rem !important; } + .my-sm-n2 { + margin-top: -0.5rem !important; + margin-bottom: -0.5rem !important; } + .my-sm-n3 { + margin-top: -1rem !important; + margin-bottom: -1rem !important; } + .my-sm-n4 { + margin-top: -1.5rem !important; + margin-bottom: -1.5rem !important; } + .my-sm-n5 { + margin-top: -3rem !important; + margin-bottom: -3rem !important; } + .mt-sm-n1 { + margin-top: -0.25rem !important; } + .mt-sm-n2 { + margin-top: -0.5rem !important; } + .mt-sm-n3 { + margin-top: -1rem !important; } + .mt-sm-n4 { + margin-top: -1.5rem !important; } + .mt-sm-n5 { + margin-top: -3rem !important; } + .me-sm-n1 { + margin-right: -0.25rem !important; } + .me-sm-n2 { + margin-right: -0.5rem !important; } + .me-sm-n3 { + margin-right: -1rem !important; } + .me-sm-n4 { + margin-right: -1.5rem !important; } + .me-sm-n5 { + margin-right: -3rem !important; } + .mb-sm-n1 { + margin-bottom: -0.25rem !important; } + .mb-sm-n2 { + margin-bottom: -0.5rem !important; } + .mb-sm-n3 { + margin-bottom: -1rem !important; } + .mb-sm-n4 { + margin-bottom: -1.5rem !important; } + .mb-sm-n5 { + margin-bottom: -3rem !important; } + .ms-sm-n1 { + margin-left: -0.25rem !important; } + .ms-sm-n2 { + margin-left: -0.5rem !important; } + .ms-sm-n3 { + margin-left: -1rem !important; } + .ms-sm-n4 { + margin-left: -1.5rem !important; } + .ms-sm-n5 { + margin-left: -3rem !important; } + .p-sm-0 { + padding: 0 !important; } + .p-sm-1 { + padding: 0.25rem !important; } + .p-sm-2 { + padding: 0.5rem !important; } + .p-sm-3 { + padding: 1rem !important; } + .p-sm-4 { + padding: 1.5rem !important; } + .p-sm-5 { + padding: 3rem !important; } + .px-sm-0 { + padding-right: 0 !important; + padding-left: 0 !important; } + .px-sm-1 { + padding-right: 0.25rem !important; + padding-left: 0.25rem !important; } + .px-sm-2 { + padding-right: 0.5rem !important; + padding-left: 0.5rem !important; } + .px-sm-3 { + padding-right: 1rem !important; + padding-left: 1rem !important; } + .px-sm-4 { + padding-right: 1.5rem !important; + padding-left: 1.5rem !important; } + .px-sm-5 { + padding-right: 3rem !important; + padding-left: 3rem !important; } + .py-sm-0 { + padding-top: 0 !important; + padding-bottom: 0 !important; } + .py-sm-1 { + padding-top: 0.25rem !important; + padding-bottom: 0.25rem !important; } + .py-sm-2 { + padding-top: 0.5rem !important; + padding-bottom: 0.5rem !important; } + .py-sm-3 { + padding-top: 1rem !important; + padding-bottom: 1rem !important; } + .py-sm-4 { + padding-top: 1.5rem !important; + padding-bottom: 1.5rem !important; } + .py-sm-5 { + padding-top: 3rem !important; + padding-bottom: 3rem !important; } + .pt-sm-0 { + padding-top: 0 !important; } + .pt-sm-1 { + padding-top: 0.25rem !important; } + .pt-sm-2 { + padding-top: 0.5rem !important; } + .pt-sm-3 { + padding-top: 1rem !important; } + .pt-sm-4 { + padding-top: 1.5rem !important; } + .pt-sm-5 { + padding-top: 3rem !important; } + .pe-sm-0 { + padding-right: 0 !important; } + .pe-sm-1 { + padding-right: 0.25rem !important; } + .pe-sm-2 { + padding-right: 0.5rem !important; } + .pe-sm-3 { + padding-right: 1rem !important; } + .pe-sm-4 { + padding-right: 1.5rem !important; } + .pe-sm-5 { + padding-right: 3rem !important; } + .pb-sm-0 { + padding-bottom: 0 !important; } + .pb-sm-1 { + padding-bottom: 0.25rem !important; } + .pb-sm-2 { + padding-bottom: 0.5rem !important; } + .pb-sm-3 { + padding-bottom: 1rem !important; } + .pb-sm-4 { + padding-bottom: 1.5rem !important; } + .pb-sm-5 { + padding-bottom: 3rem !important; } + .ps-sm-0 { + padding-left: 0 !important; } + .ps-sm-1 { + padding-left: 0.25rem !important; } + .ps-sm-2 { + padding-left: 0.5rem !important; } + .ps-sm-3 { + padding-left: 1rem !important; } + .ps-sm-4 { + padding-left: 1.5rem !important; } + .ps-sm-5 { + padding-left: 3rem !important; } + .text-sm-start { + text-align: left !important; } + .text-sm-end { + text-align: right !important; } + .text-sm-center { + text-align: center !important; } } + +@media (min-width: 768px) { + .float-md-start { + float: left !important; } + .float-md-end { + float: right !important; } + .float-md-none { + float: none !important; } + .d-md-inline { + display: inline !important; } + .d-md-inline-block { + display: inline-block !important; } + .d-md-block { + display: block !important; } + .d-md-grid { + display: grid !important; } + .d-md-table { + display: table !important; } + .d-md-table-row { + display: table-row !important; } + .d-md-table-cell { + display: table-cell !important; } + .d-md-flex { + display: -webkit-box !important; + display: -ms-flexbox !important; + display: flex !important; } + .d-md-inline-flex { + display: -webkit-inline-box !important; + display: -ms-inline-flexbox !important; + display: inline-flex !important; } + .d-md-none { + display: none !important; } + .flex-md-fill { + -webkit-box-flex: 1 !important; + -ms-flex: 1 1 auto !important; + flex: 1 1 auto !important; } + .flex-md-row { + -webkit-box-orient: horizontal !important; + -webkit-box-direction: normal !important; + -ms-flex-direction: row !important; + flex-direction: row !important; } + .flex-md-column { + -webkit-box-orient: vertical !important; + -webkit-box-direction: normal !important; + -ms-flex-direction: column !important; + flex-direction: column !important; } + .flex-md-row-reverse { + -webkit-box-orient: horizontal !important; + -webkit-box-direction: reverse !important; + -ms-flex-direction: row-reverse !important; + flex-direction: row-reverse !important; } + .flex-md-column-reverse { + -webkit-box-orient: vertical !important; + -webkit-box-direction: reverse !important; + -ms-flex-direction: column-reverse !important; + flex-direction: column-reverse !important; } + .flex-md-grow-0 { + -webkit-box-flex: 0 !important; + -ms-flex-positive: 0 !important; + flex-grow: 0 !important; } + .flex-md-grow-1 { + -webkit-box-flex: 1 !important; + -ms-flex-positive: 1 !important; + flex-grow: 1 !important; } + .flex-md-shrink-0 { + -ms-flex-negative: 0 !important; + flex-shrink: 0 !important; } + .flex-md-shrink-1 { + -ms-flex-negative: 1 !important; + flex-shrink: 1 !important; } + .flex-md-wrap { + -ms-flex-wrap: wrap !important; + flex-wrap: wrap !important; } + .flex-md-nowrap { + -ms-flex-wrap: nowrap !important; + flex-wrap: nowrap !important; } + .flex-md-wrap-reverse { + -ms-flex-wrap: wrap-reverse !important; + flex-wrap: wrap-reverse !important; } + .gap-md-0 { + gap: 0 !important; } + .gap-md-1 { + gap: 0.25rem !important; } + .gap-md-2 { + gap: 0.5rem !important; } + .gap-md-3 { + gap: 1rem !important; } + .gap-md-4 { + gap: 1.5rem !important; } + .gap-md-5 { + gap: 3rem !important; } + .justify-content-md-start { + -webkit-box-pack: start !important; + -ms-flex-pack: start !important; + justify-content: flex-start !important; } + .justify-content-md-end { + -webkit-box-pack: end !important; + -ms-flex-pack: end !important; + justify-content: flex-end !important; } + .justify-content-md-center { + -webkit-box-pack: center !important; + -ms-flex-pack: center !important; + justify-content: center !important; } + .justify-content-md-between { + -webkit-box-pack: justify !important; + -ms-flex-pack: justify !important; + justify-content: space-between !important; } + .justify-content-md-around { + -ms-flex-pack: distribute !important; + justify-content: space-around !important; } + .justify-content-md-evenly { + -webkit-box-pack: space-evenly !important; + -ms-flex-pack: space-evenly !important; + justify-content: space-evenly !important; } + .align-items-md-start { + -webkit-box-align: start !important; + -ms-flex-align: start !important; + align-items: flex-start !important; } + .align-items-md-end { + -webkit-box-align: end !important; + -ms-flex-align: end !important; + align-items: flex-end !important; } + .align-items-md-center { + -webkit-box-align: center !important; + -ms-flex-align: center !important; + align-items: center !important; } + .align-items-md-baseline { + -webkit-box-align: baseline !important; + -ms-flex-align: baseline !important; + align-items: baseline !important; } + .align-items-md-stretch { + -webkit-box-align: stretch !important; + -ms-flex-align: stretch !important; + align-items: stretch !important; } + .align-content-md-start { + -ms-flex-line-pack: start !important; + align-content: flex-start !important; } + .align-content-md-end { + -ms-flex-line-pack: end !important; + align-content: flex-end !important; } + .align-content-md-center { + -ms-flex-line-pack: center !important; + align-content: center !important; } + .align-content-md-between { + -ms-flex-line-pack: justify !important; + align-content: space-between !important; } + .align-content-md-around { + -ms-flex-line-pack: distribute !important; + align-content: space-around !important; } + .align-content-md-stretch { + -ms-flex-line-pack: stretch !important; + align-content: stretch !important; } + .align-self-md-auto { + -ms-flex-item-align: auto !important; + align-self: auto !important; } + .align-self-md-start { + -ms-flex-item-align: start !important; + align-self: flex-start !important; } + .align-self-md-end { + -ms-flex-item-align: end !important; + align-self: flex-end !important; } + .align-self-md-center { + -ms-flex-item-align: center !important; + align-self: center !important; } + .align-self-md-baseline { + -ms-flex-item-align: baseline !important; + align-self: baseline !important; } + .align-self-md-stretch { + -ms-flex-item-align: stretch !important; + align-self: stretch !important; } + .order-md-first { + -webkit-box-ordinal-group: 0 !important; + -ms-flex-order: -1 !important; + order: -1 !important; } + .order-md-0 { + -webkit-box-ordinal-group: 1 !important; + -ms-flex-order: 0 !important; + order: 0 !important; } + .order-md-1 { + -webkit-box-ordinal-group: 2 !important; + -ms-flex-order: 1 !important; + order: 1 !important; } + .order-md-2 { + -webkit-box-ordinal-group: 3 !important; + -ms-flex-order: 2 !important; + order: 2 !important; } + .order-md-3 { + -webkit-box-ordinal-group: 4 !important; + -ms-flex-order: 3 !important; + order: 3 !important; } + .order-md-4 { + -webkit-box-ordinal-group: 5 !important; + -ms-flex-order: 4 !important; + order: 4 !important; } + .order-md-5 { + -webkit-box-ordinal-group: 6 !important; + -ms-flex-order: 5 !important; + order: 5 !important; } + .order-md-last { + -webkit-box-ordinal-group: 7 !important; + -ms-flex-order: 6 !important; + order: 6 !important; } + .m-md-0 { + margin: 0 !important; } + .m-md-1 { + margin: 0.25rem !important; } + .m-md-2 { + margin: 0.5rem !important; } + .m-md-3 { + margin: 1rem !important; } + .m-md-4 { + margin: 1.5rem !important; } + .m-md-5 { + margin: 3rem !important; } + .m-md-auto { + margin: auto !important; } + .mx-md-0 { + margin-right: 0 !important; + margin-left: 0 !important; } + .mx-md-1 { + margin-right: 0.25rem !important; + margin-left: 0.25rem !important; } + .mx-md-2 { + margin-right: 0.5rem !important; + margin-left: 0.5rem !important; } + .mx-md-3 { + margin-right: 1rem !important; + margin-left: 1rem !important; } + .mx-md-4 { + margin-right: 1.5rem !important; + margin-left: 1.5rem !important; } + .mx-md-5 { + margin-right: 3rem !important; + margin-left: 3rem !important; } + .mx-md-auto { + margin-right: auto !important; + margin-left: auto !important; } + .my-md-0 { + margin-top: 0 !important; + margin-bottom: 0 !important; } + .my-md-1 { + margin-top: 0.25rem !important; + margin-bottom: 0.25rem !important; } + .my-md-2 { + margin-top: 0.5rem !important; + margin-bottom: 0.5rem !important; } + .my-md-3 { + margin-top: 1rem !important; + margin-bottom: 1rem !important; } + .my-md-4 { + margin-top: 1.5rem !important; + margin-bottom: 1.5rem !important; } + .my-md-5 { + margin-top: 3rem !important; + margin-bottom: 3rem !important; } + .my-md-auto { + margin-top: auto !important; + margin-bottom: auto !important; } + .mt-md-0 { + margin-top: 0 !important; } + .mt-md-1 { + margin-top: 0.25rem !important; } + .mt-md-2 { + margin-top: 0.5rem !important; } + .mt-md-3 { + margin-top: 1rem !important; } + .mt-md-4 { + margin-top: 1.5rem !important; } + .mt-md-5 { + margin-top: 3rem !important; } + .mt-md-auto { + margin-top: auto !important; } + .me-md-0 { + margin-right: 0 !important; } + .me-md-1 { + margin-right: 0.25rem !important; } + .me-md-2 { + margin-right: 0.5rem !important; } + .me-md-3 { + margin-right: 1rem !important; } + .me-md-4 { + margin-right: 1.5rem !important; } + .me-md-5 { + margin-right: 3rem !important; } + .me-md-auto { + margin-right: auto !important; } + .mb-md-0 { + margin-bottom: 0 !important; } + .mb-md-1 { + margin-bottom: 0.25rem !important; } + .mb-md-2 { + margin-bottom: 0.5rem !important; } + .mb-md-3 { + margin-bottom: 1rem !important; } + .mb-md-4 { + margin-bottom: 1.5rem !important; } + .mb-md-5 { + margin-bottom: 3rem !important; } + .mb-md-auto { + margin-bottom: auto !important; } + .ms-md-0 { + margin-left: 0 !important; } + .ms-md-1 { + margin-left: 0.25rem !important; } + .ms-md-2 { + margin-left: 0.5rem !important; } + .ms-md-3 { + margin-left: 1rem !important; } + .ms-md-4 { + margin-left: 1.5rem !important; } + .ms-md-5 { + margin-left: 3rem !important; } + .ms-md-auto { + margin-left: auto !important; } + .m-md-n1 { + margin: -0.25rem !important; } + .m-md-n2 { + margin: -0.5rem !important; } + .m-md-n3 { + margin: -1rem !important; } + .m-md-n4 { + margin: -1.5rem !important; } + .m-md-n5 { + margin: -3rem !important; } + .mx-md-n1 { + margin-right: -0.25rem !important; + margin-left: -0.25rem !important; } + .mx-md-n2 { + margin-right: -0.5rem !important; + margin-left: -0.5rem !important; } + .mx-md-n3 { + margin-right: -1rem !important; + margin-left: -1rem !important; } + .mx-md-n4 { + margin-right: -1.5rem !important; + margin-left: -1.5rem !important; } + .mx-md-n5 { + margin-right: -3rem !important; + margin-left: -3rem !important; } + .my-md-n1 { + margin-top: -0.25rem !important; + margin-bottom: -0.25rem !important; } + .my-md-n2 { + margin-top: -0.5rem !important; + margin-bottom: -0.5rem !important; } + .my-md-n3 { + margin-top: -1rem !important; + margin-bottom: -1rem !important; } + .my-md-n4 { + margin-top: -1.5rem !important; + margin-bottom: -1.5rem !important; } + .my-md-n5 { + margin-top: -3rem !important; + margin-bottom: -3rem !important; } + .mt-md-n1 { + margin-top: -0.25rem !important; } + .mt-md-n2 { + margin-top: -0.5rem !important; } + .mt-md-n3 { + margin-top: -1rem !important; } + .mt-md-n4 { + margin-top: -1.5rem !important; } + .mt-md-n5 { + margin-top: -3rem !important; } + .me-md-n1 { + margin-right: -0.25rem !important; } + .me-md-n2 { + margin-right: -0.5rem !important; } + .me-md-n3 { + margin-right: -1rem !important; } + .me-md-n4 { + margin-right: -1.5rem !important; } + .me-md-n5 { + margin-right: -3rem !important; } + .mb-md-n1 { + margin-bottom: -0.25rem !important; } + .mb-md-n2 { + margin-bottom: -0.5rem !important; } + .mb-md-n3 { + margin-bottom: -1rem !important; } + .mb-md-n4 { + margin-bottom: -1.5rem !important; } + .mb-md-n5 { + margin-bottom: -3rem !important; } + .ms-md-n1 { + margin-left: -0.25rem !important; } + .ms-md-n2 { + margin-left: -0.5rem !important; } + .ms-md-n3 { + margin-left: -1rem !important; } + .ms-md-n4 { + margin-left: -1.5rem !important; } + .ms-md-n5 { + margin-left: -3rem !important; } + .p-md-0 { + padding: 0 !important; } + .p-md-1 { + padding: 0.25rem !important; } + .p-md-2 { + padding: 0.5rem !important; } + .p-md-3 { + padding: 1rem !important; } + .p-md-4 { + padding: 1.5rem !important; } + .p-md-5 { + padding: 3rem !important; } + .px-md-0 { + padding-right: 0 !important; + padding-left: 0 !important; } + .px-md-1 { + padding-right: 0.25rem !important; + padding-left: 0.25rem !important; } + .px-md-2 { + padding-right: 0.5rem !important; + padding-left: 0.5rem !important; } + .px-md-3 { + padding-right: 1rem !important; + padding-left: 1rem !important; } + .px-md-4 { + padding-right: 1.5rem !important; + padding-left: 1.5rem !important; } + .px-md-5 { + padding-right: 3rem !important; + padding-left: 3rem !important; } + .py-md-0 { + padding-top: 0 !important; + padding-bottom: 0 !important; } + .py-md-1 { + padding-top: 0.25rem !important; + padding-bottom: 0.25rem !important; } + .py-md-2 { + padding-top: 0.5rem !important; + padding-bottom: 0.5rem !important; } + .py-md-3 { + padding-top: 1rem !important; + padding-bottom: 1rem !important; } + .py-md-4 { + padding-top: 1.5rem !important; + padding-bottom: 1.5rem !important; } + .py-md-5 { + padding-top: 3rem !important; + padding-bottom: 3rem !important; } + .pt-md-0 { + padding-top: 0 !important; } + .pt-md-1 { + padding-top: 0.25rem !important; } + .pt-md-2 { + padding-top: 0.5rem !important; } + .pt-md-3 { + padding-top: 1rem !important; } + .pt-md-4 { + padding-top: 1.5rem !important; } + .pt-md-5 { + padding-top: 3rem !important; } + .pe-md-0 { + padding-right: 0 !important; } + .pe-md-1 { + padding-right: 0.25rem !important; } + .pe-md-2 { + padding-right: 0.5rem !important; } + .pe-md-3 { + padding-right: 1rem !important; } + .pe-md-4 { + padding-right: 1.5rem !important; } + .pe-md-5 { + padding-right: 3rem !important; } + .pb-md-0 { + padding-bottom: 0 !important; } + .pb-md-1 { + padding-bottom: 0.25rem !important; } + .pb-md-2 { + padding-bottom: 0.5rem !important; } + .pb-md-3 { + padding-bottom: 1rem !important; } + .pb-md-4 { + padding-bottom: 1.5rem !important; } + .pb-md-5 { + padding-bottom: 3rem !important; } + .ps-md-0 { + padding-left: 0 !important; } + .ps-md-1 { + padding-left: 0.25rem !important; } + .ps-md-2 { + padding-left: 0.5rem !important; } + .ps-md-3 { + padding-left: 1rem !important; } + .ps-md-4 { + padding-left: 1.5rem !important; } + .ps-md-5 { + padding-left: 3rem !important; } + .text-md-start { + text-align: left !important; } + .text-md-end { + text-align: right !important; } + .text-md-center { + text-align: center !important; } } + +@media (min-width: 992px) { + .float-lg-start { + float: left !important; } + .float-lg-end { + float: right !important; } + .float-lg-none { + float: none !important; } + .d-lg-inline { + display: inline !important; } + .d-lg-inline-block { + display: inline-block !important; } + .d-lg-block { + display: block !important; } + .d-lg-grid { + display: grid !important; } + .d-lg-table { + display: table !important; } + .d-lg-table-row { + display: table-row !important; } + .d-lg-table-cell { + display: table-cell !important; } + .d-lg-flex { + display: -webkit-box !important; + display: -ms-flexbox !important; + display: flex !important; } + .d-lg-inline-flex { + display: -webkit-inline-box !important; + display: -ms-inline-flexbox !important; + display: inline-flex !important; } + .d-lg-none { + display: none !important; } + .flex-lg-fill { + -webkit-box-flex: 1 !important; + -ms-flex: 1 1 auto !important; + flex: 1 1 auto !important; } + .flex-lg-row { + -webkit-box-orient: horizontal !important; + -webkit-box-direction: normal !important; + -ms-flex-direction: row !important; + flex-direction: row !important; } + .flex-lg-column { + -webkit-box-orient: vertical !important; + -webkit-box-direction: normal !important; + -ms-flex-direction: column !important; + flex-direction: column !important; } + .flex-lg-row-reverse { + -webkit-box-orient: horizontal !important; + -webkit-box-direction: reverse !important; + -ms-flex-direction: row-reverse !important; + flex-direction: row-reverse !important; } + .flex-lg-column-reverse { + -webkit-box-orient: vertical !important; + -webkit-box-direction: reverse !important; + -ms-flex-direction: column-reverse !important; + flex-direction: column-reverse !important; } + .flex-lg-grow-0 { + -webkit-box-flex: 0 !important; + -ms-flex-positive: 0 !important; + flex-grow: 0 !important; } + .flex-lg-grow-1 { + -webkit-box-flex: 1 !important; + -ms-flex-positive: 1 !important; + flex-grow: 1 !important; } + .flex-lg-shrink-0 { + -ms-flex-negative: 0 !important; + flex-shrink: 0 !important; } + .flex-lg-shrink-1 { + -ms-flex-negative: 1 !important; + flex-shrink: 1 !important; } + .flex-lg-wrap { + -ms-flex-wrap: wrap !important; + flex-wrap: wrap !important; } + .flex-lg-nowrap { + -ms-flex-wrap: nowrap !important; + flex-wrap: nowrap !important; } + .flex-lg-wrap-reverse { + -ms-flex-wrap: wrap-reverse !important; + flex-wrap: wrap-reverse !important; } + .gap-lg-0 { + gap: 0 !important; } + .gap-lg-1 { + gap: 0.25rem !important; } + .gap-lg-2 { + gap: 0.5rem !important; } + .gap-lg-3 { + gap: 1rem !important; } + .gap-lg-4 { + gap: 1.5rem !important; } + .gap-lg-5 { + gap: 3rem !important; } + .justify-content-lg-start { + -webkit-box-pack: start !important; + -ms-flex-pack: start !important; + justify-content: flex-start !important; } + .justify-content-lg-end { + -webkit-box-pack: end !important; + -ms-flex-pack: end !important; + justify-content: flex-end !important; } + .justify-content-lg-center { + -webkit-box-pack: center !important; + -ms-flex-pack: center !important; + justify-content: center !important; } + .justify-content-lg-between { + -webkit-box-pack: justify !important; + -ms-flex-pack: justify !important; + justify-content: space-between !important; } + .justify-content-lg-around { + -ms-flex-pack: distribute !important; + justify-content: space-around !important; } + .justify-content-lg-evenly { + -webkit-box-pack: space-evenly !important; + -ms-flex-pack: space-evenly !important; + justify-content: space-evenly !important; } + .align-items-lg-start { + -webkit-box-align: start !important; + -ms-flex-align: start !important; + align-items: flex-start !important; } + .align-items-lg-end { + -webkit-box-align: end !important; + -ms-flex-align: end !important; + align-items: flex-end !important; } + .align-items-lg-center { + -webkit-box-align: center !important; + -ms-flex-align: center !important; + align-items: center !important; } + .align-items-lg-baseline { + -webkit-box-align: baseline !important; + -ms-flex-align: baseline !important; + align-items: baseline !important; } + .align-items-lg-stretch { + -webkit-box-align: stretch !important; + -ms-flex-align: stretch !important; + align-items: stretch !important; } + .align-content-lg-start { + -ms-flex-line-pack: start !important; + align-content: flex-start !important; } + .align-content-lg-end { + -ms-flex-line-pack: end !important; + align-content: flex-end !important; } + .align-content-lg-center { + -ms-flex-line-pack: center !important; + align-content: center !important; } + .align-content-lg-between { + -ms-flex-line-pack: justify !important; + align-content: space-between !important; } + .align-content-lg-around { + -ms-flex-line-pack: distribute !important; + align-content: space-around !important; } + .align-content-lg-stretch { + -ms-flex-line-pack: stretch !important; + align-content: stretch !important; } + .align-self-lg-auto { + -ms-flex-item-align: auto !important; + align-self: auto !important; } + .align-self-lg-start { + -ms-flex-item-align: start !important; + align-self: flex-start !important; } + .align-self-lg-end { + -ms-flex-item-align: end !important; + align-self: flex-end !important; } + .align-self-lg-center { + -ms-flex-item-align: center !important; + align-self: center !important; } + .align-self-lg-baseline { + -ms-flex-item-align: baseline !important; + align-self: baseline !important; } + .align-self-lg-stretch { + -ms-flex-item-align: stretch !important; + align-self: stretch !important; } + .order-lg-first { + -webkit-box-ordinal-group: 0 !important; + -ms-flex-order: -1 !important; + order: -1 !important; } + .order-lg-0 { + -webkit-box-ordinal-group: 1 !important; + -ms-flex-order: 0 !important; + order: 0 !important; } + .order-lg-1 { + -webkit-box-ordinal-group: 2 !important; + -ms-flex-order: 1 !important; + order: 1 !important; } + .order-lg-2 { + -webkit-box-ordinal-group: 3 !important; + -ms-flex-order: 2 !important; + order: 2 !important; } + .order-lg-3 { + -webkit-box-ordinal-group: 4 !important; + -ms-flex-order: 3 !important; + order: 3 !important; } + .order-lg-4 { + -webkit-box-ordinal-group: 5 !important; + -ms-flex-order: 4 !important; + order: 4 !important; } + .order-lg-5 { + -webkit-box-ordinal-group: 6 !important; + -ms-flex-order: 5 !important; + order: 5 !important; } + .order-lg-last { + -webkit-box-ordinal-group: 7 !important; + -ms-flex-order: 6 !important; + order: 6 !important; } + .m-lg-0 { + margin: 0 !important; } + .m-lg-1 { + margin: 0.25rem !important; } + .m-lg-2 { + margin: 0.5rem !important; } + .m-lg-3 { + margin: 1rem !important; } + .m-lg-4 { + margin: 1.5rem !important; } + .m-lg-5 { + margin: 3rem !important; } + .m-lg-auto { + margin: auto !important; } + .mx-lg-0 { + margin-right: 0 !important; + margin-left: 0 !important; } + .mx-lg-1 { + margin-right: 0.25rem !important; + margin-left: 0.25rem !important; } + .mx-lg-2 { + margin-right: 0.5rem !important; + margin-left: 0.5rem !important; } + .mx-lg-3 { + margin-right: 1rem !important; + margin-left: 1rem !important; } + .mx-lg-4 { + margin-right: 1.5rem !important; + margin-left: 1.5rem !important; } + .mx-lg-5 { + margin-right: 3rem !important; + margin-left: 3rem !important; } + .mx-lg-auto { + margin-right: auto !important; + margin-left: auto !important; } + .my-lg-0 { + margin-top: 0 !important; + margin-bottom: 0 !important; } + .my-lg-1 { + margin-top: 0.25rem !important; + margin-bottom: 0.25rem !important; } + .my-lg-2 { + margin-top: 0.5rem !important; + margin-bottom: 0.5rem !important; } + .my-lg-3 { + margin-top: 1rem !important; + margin-bottom: 1rem !important; } + .my-lg-4 { + margin-top: 1.5rem !important; + margin-bottom: 1.5rem !important; } + .my-lg-5 { + margin-top: 3rem !important; + margin-bottom: 3rem !important; } + .my-lg-auto { + margin-top: auto !important; + margin-bottom: auto !important; } + .mt-lg-0 { + margin-top: 0 !important; } + .mt-lg-1 { + margin-top: 0.25rem !important; } + .mt-lg-2 { + margin-top: 0.5rem !important; } + .mt-lg-3 { + margin-top: 1rem !important; } + .mt-lg-4 { + margin-top: 1.5rem !important; } + .mt-lg-5 { + margin-top: 3rem !important; } + .mt-lg-auto { + margin-top: auto !important; } + .me-lg-0 { + margin-right: 0 !important; } + .me-lg-1 { + margin-right: 0.25rem !important; } + .me-lg-2 { + margin-right: 0.5rem !important; } + .me-lg-3 { + margin-right: 1rem !important; } + .me-lg-4 { + margin-right: 1.5rem !important; } + .me-lg-5 { + margin-right: 3rem !important; } + .me-lg-auto { + margin-right: auto !important; } + .mb-lg-0 { + margin-bottom: 0 !important; } + .mb-lg-1 { + margin-bottom: 0.25rem !important; } + .mb-lg-2 { + margin-bottom: 0.5rem !important; } + .mb-lg-3 { + margin-bottom: 1rem !important; } + .mb-lg-4 { + margin-bottom: 1.5rem !important; } + .mb-lg-5 { + margin-bottom: 3rem !important; } + .mb-lg-auto { + margin-bottom: auto !important; } + .ms-lg-0 { + margin-left: 0 !important; } + .ms-lg-1 { + margin-left: 0.25rem !important; } + .ms-lg-2 { + margin-left: 0.5rem !important; } + .ms-lg-3 { + margin-left: 1rem !important; } + .ms-lg-4 { + margin-left: 1.5rem !important; } + .ms-lg-5 { + margin-left: 3rem !important; } + .ms-lg-auto { + margin-left: auto !important; } + .m-lg-n1 { + margin: -0.25rem !important; } + .m-lg-n2 { + margin: -0.5rem !important; } + .m-lg-n3 { + margin: -1rem !important; } + .m-lg-n4 { + margin: -1.5rem !important; } + .m-lg-n5 { + margin: -3rem !important; } + .mx-lg-n1 { + margin-right: -0.25rem !important; + margin-left: -0.25rem !important; } + .mx-lg-n2 { + margin-right: -0.5rem !important; + margin-left: -0.5rem !important; } + .mx-lg-n3 { + margin-right: -1rem !important; + margin-left: -1rem !important; } + .mx-lg-n4 { + margin-right: -1.5rem !important; + margin-left: -1.5rem !important; } + .mx-lg-n5 { + margin-right: -3rem !important; + margin-left: -3rem !important; } + .my-lg-n1 { + margin-top: -0.25rem !important; + margin-bottom: -0.25rem !important; } + .my-lg-n2 { + margin-top: -0.5rem !important; + margin-bottom: -0.5rem !important; } + .my-lg-n3 { + margin-top: -1rem !important; + margin-bottom: -1rem !important; } + .my-lg-n4 { + margin-top: -1.5rem !important; + margin-bottom: -1.5rem !important; } + .my-lg-n5 { + margin-top: -3rem !important; + margin-bottom: -3rem !important; } + .mt-lg-n1 { + margin-top: -0.25rem !important; } + .mt-lg-n2 { + margin-top: -0.5rem !important; } + .mt-lg-n3 { + margin-top: -1rem !important; } + .mt-lg-n4 { + margin-top: -1.5rem !important; } + .mt-lg-n5 { + margin-top: -3rem !important; } + .me-lg-n1 { + margin-right: -0.25rem !important; } + .me-lg-n2 { + margin-right: -0.5rem !important; } + .me-lg-n3 { + margin-right: -1rem !important; } + .me-lg-n4 { + margin-right: -1.5rem !important; } + .me-lg-n5 { + margin-right: -3rem !important; } + .mb-lg-n1 { + margin-bottom: -0.25rem !important; } + .mb-lg-n2 { + margin-bottom: -0.5rem !important; } + .mb-lg-n3 { + margin-bottom: -1rem !important; } + .mb-lg-n4 { + margin-bottom: -1.5rem !important; } + .mb-lg-n5 { + margin-bottom: -3rem !important; } + .ms-lg-n1 { + margin-left: -0.25rem !important; } + .ms-lg-n2 { + margin-left: -0.5rem !important; } + .ms-lg-n3 { + margin-left: -1rem !important; } + .ms-lg-n4 { + margin-left: -1.5rem !important; } + .ms-lg-n5 { + margin-left: -3rem !important; } + .p-lg-0 { + padding: 0 !important; } + .p-lg-1 { + padding: 0.25rem !important; } + .p-lg-2 { + padding: 0.5rem !important; } + .p-lg-3 { + padding: 1rem !important; } + .p-lg-4 { + padding: 1.5rem !important; } + .p-lg-5 { + padding: 3rem !important; } + .px-lg-0 { + padding-right: 0 !important; + padding-left: 0 !important; } + .px-lg-1 { + padding-right: 0.25rem !important; + padding-left: 0.25rem !important; } + .px-lg-2 { + padding-right: 0.5rem !important; + padding-left: 0.5rem !important; } + .px-lg-3 { + padding-right: 1rem !important; + padding-left: 1rem !important; } + .px-lg-4 { + padding-right: 1.5rem !important; + padding-left: 1.5rem !important; } + .px-lg-5 { + padding-right: 3rem !important; + padding-left: 3rem !important; } + .py-lg-0 { + padding-top: 0 !important; + padding-bottom: 0 !important; } + .py-lg-1 { + padding-top: 0.25rem !important; + padding-bottom: 0.25rem !important; } + .py-lg-2 { + padding-top: 0.5rem !important; + padding-bottom: 0.5rem !important; } + .py-lg-3 { + padding-top: 1rem !important; + padding-bottom: 1rem !important; } + .py-lg-4 { + padding-top: 1.5rem !important; + padding-bottom: 1.5rem !important; } + .py-lg-5 { + padding-top: 3rem !important; + padding-bottom: 3rem !important; } + .pt-lg-0 { + padding-top: 0 !important; } + .pt-lg-1 { + padding-top: 0.25rem !important; } + .pt-lg-2 { + padding-top: 0.5rem !important; } + .pt-lg-3 { + padding-top: 1rem !important; } + .pt-lg-4 { + padding-top: 1.5rem !important; } + .pt-lg-5 { + padding-top: 3rem !important; } + .pe-lg-0 { + padding-right: 0 !important; } + .pe-lg-1 { + padding-right: 0.25rem !important; } + .pe-lg-2 { + padding-right: 0.5rem !important; } + .pe-lg-3 { + padding-right: 1rem !important; } + .pe-lg-4 { + padding-right: 1.5rem !important; } + .pe-lg-5 { + padding-right: 3rem !important; } + .pb-lg-0 { + padding-bottom: 0 !important; } + .pb-lg-1 { + padding-bottom: 0.25rem !important; } + .pb-lg-2 { + padding-bottom: 0.5rem !important; } + .pb-lg-3 { + padding-bottom: 1rem !important; } + .pb-lg-4 { + padding-bottom: 1.5rem !important; } + .pb-lg-5 { + padding-bottom: 3rem !important; } + .ps-lg-0 { + padding-left: 0 !important; } + .ps-lg-1 { + padding-left: 0.25rem !important; } + .ps-lg-2 { + padding-left: 0.5rem !important; } + .ps-lg-3 { + padding-left: 1rem !important; } + .ps-lg-4 { + padding-left: 1.5rem !important; } + .ps-lg-5 { + padding-left: 3rem !important; } + .text-lg-start { + text-align: left !important; } + .text-lg-end { + text-align: right !important; } + .text-lg-center { + text-align: center !important; } } + +@media (min-width: 1200px) { + .float-xl-start { + float: left !important; } + .float-xl-end { + float: right !important; } + .float-xl-none { + float: none !important; } + .d-xl-inline { + display: inline !important; } + .d-xl-inline-block { + display: inline-block !important; } + .d-xl-block { + display: block !important; } + .d-xl-grid { + display: grid !important; } + .d-xl-table { + display: table !important; } + .d-xl-table-row { + display: table-row !important; } + .d-xl-table-cell { + display: table-cell !important; } + .d-xl-flex { + display: -webkit-box !important; + display: -ms-flexbox !important; + display: flex !important; } + .d-xl-inline-flex { + display: -webkit-inline-box !important; + display: -ms-inline-flexbox !important; + display: inline-flex !important; } + .d-xl-none { + display: none !important; } + .flex-xl-fill { + -webkit-box-flex: 1 !important; + -ms-flex: 1 1 auto !important; + flex: 1 1 auto !important; } + .flex-xl-row { + -webkit-box-orient: horizontal !important; + -webkit-box-direction: normal !important; + -ms-flex-direction: row !important; + flex-direction: row !important; } + .flex-xl-column { + -webkit-box-orient: vertical !important; + -webkit-box-direction: normal !important; + -ms-flex-direction: column !important; + flex-direction: column !important; } + .flex-xl-row-reverse { + -webkit-box-orient: horizontal !important; + -webkit-box-direction: reverse !important; + -ms-flex-direction: row-reverse !important; + flex-direction: row-reverse !important; } + .flex-xl-column-reverse { + -webkit-box-orient: vertical !important; + -webkit-box-direction: reverse !important; + -ms-flex-direction: column-reverse !important; + flex-direction: column-reverse !important; } + .flex-xl-grow-0 { + -webkit-box-flex: 0 !important; + -ms-flex-positive: 0 !important; + flex-grow: 0 !important; } + .flex-xl-grow-1 { + -webkit-box-flex: 1 !important; + -ms-flex-positive: 1 !important; + flex-grow: 1 !important; } + .flex-xl-shrink-0 { + -ms-flex-negative: 0 !important; + flex-shrink: 0 !important; } + .flex-xl-shrink-1 { + -ms-flex-negative: 1 !important; + flex-shrink: 1 !important; } + .flex-xl-wrap { + -ms-flex-wrap: wrap !important; + flex-wrap: wrap !important; } + .flex-xl-nowrap { + -ms-flex-wrap: nowrap !important; + flex-wrap: nowrap !important; } + .flex-xl-wrap-reverse { + -ms-flex-wrap: wrap-reverse !important; + flex-wrap: wrap-reverse !important; } + .gap-xl-0 { + gap: 0 !important; } + .gap-xl-1 { + gap: 0.25rem !important; } + .gap-xl-2 { + gap: 0.5rem !important; } + .gap-xl-3 { + gap: 1rem !important; } + .gap-xl-4 { + gap: 1.5rem !important; } + .gap-xl-5 { + gap: 3rem !important; } + .justify-content-xl-start { + -webkit-box-pack: start !important; + -ms-flex-pack: start !important; + justify-content: flex-start !important; } + .justify-content-xl-end { + -webkit-box-pack: end !important; + -ms-flex-pack: end !important; + justify-content: flex-end !important; } + .justify-content-xl-center { + -webkit-box-pack: center !important; + -ms-flex-pack: center !important; + justify-content: center !important; } + .justify-content-xl-between { + -webkit-box-pack: justify !important; + -ms-flex-pack: justify !important; + justify-content: space-between !important; } + .justify-content-xl-around { + -ms-flex-pack: distribute !important; + justify-content: space-around !important; } + .justify-content-xl-evenly { + -webkit-box-pack: space-evenly !important; + -ms-flex-pack: space-evenly !important; + justify-content: space-evenly !important; } + .align-items-xl-start { + -webkit-box-align: start !important; + -ms-flex-align: start !important; + align-items: flex-start !important; } + .align-items-xl-end { + -webkit-box-align: end !important; + -ms-flex-align: end !important; + align-items: flex-end !important; } + .align-items-xl-center { + -webkit-box-align: center !important; + -ms-flex-align: center !important; + align-items: center !important; } + .align-items-xl-baseline { + -webkit-box-align: baseline !important; + -ms-flex-align: baseline !important; + align-items: baseline !important; } + .align-items-xl-stretch { + -webkit-box-align: stretch !important; + -ms-flex-align: stretch !important; + align-items: stretch !important; } + .align-content-xl-start { + -ms-flex-line-pack: start !important; + align-content: flex-start !important; } + .align-content-xl-end { + -ms-flex-line-pack: end !important; + align-content: flex-end !important; } + .align-content-xl-center { + -ms-flex-line-pack: center !important; + align-content: center !important; } + .align-content-xl-between { + -ms-flex-line-pack: justify !important; + align-content: space-between !important; } + .align-content-xl-around { + -ms-flex-line-pack: distribute !important; + align-content: space-around !important; } + .align-content-xl-stretch { + -ms-flex-line-pack: stretch !important; + align-content: stretch !important; } + .align-self-xl-auto { + -ms-flex-item-align: auto !important; + align-self: auto !important; } + .align-self-xl-start { + -ms-flex-item-align: start !important; + align-self: flex-start !important; } + .align-self-xl-end { + -ms-flex-item-align: end !important; + align-self: flex-end !important; } + .align-self-xl-center { + -ms-flex-item-align: center !important; + align-self: center !important; } + .align-self-xl-baseline { + -ms-flex-item-align: baseline !important; + align-self: baseline !important; } + .align-self-xl-stretch { + -ms-flex-item-align: stretch !important; + align-self: stretch !important; } + .order-xl-first { + -webkit-box-ordinal-group: 0 !important; + -ms-flex-order: -1 !important; + order: -1 !important; } + .order-xl-0 { + -webkit-box-ordinal-group: 1 !important; + -ms-flex-order: 0 !important; + order: 0 !important; } + .order-xl-1 { + -webkit-box-ordinal-group: 2 !important; + -ms-flex-order: 1 !important; + order: 1 !important; } + .order-xl-2 { + -webkit-box-ordinal-group: 3 !important; + -ms-flex-order: 2 !important; + order: 2 !important; } + .order-xl-3 { + -webkit-box-ordinal-group: 4 !important; + -ms-flex-order: 3 !important; + order: 3 !important; } + .order-xl-4 { + -webkit-box-ordinal-group: 5 !important; + -ms-flex-order: 4 !important; + order: 4 !important; } + .order-xl-5 { + -webkit-box-ordinal-group: 6 !important; + -ms-flex-order: 5 !important; + order: 5 !important; } + .order-xl-last { + -webkit-box-ordinal-group: 7 !important; + -ms-flex-order: 6 !important; + order: 6 !important; } + .m-xl-0 { + margin: 0 !important; } + .m-xl-1 { + margin: 0.25rem !important; } + .m-xl-2 { + margin: 0.5rem !important; } + .m-xl-3 { + margin: 1rem !important; } + .m-xl-4 { + margin: 1.5rem !important; } + .m-xl-5 { + margin: 3rem !important; } + .m-xl-auto { + margin: auto !important; } + .mx-xl-0 { + margin-right: 0 !important; + margin-left: 0 !important; } + .mx-xl-1 { + margin-right: 0.25rem !important; + margin-left: 0.25rem !important; } + .mx-xl-2 { + margin-right: 0.5rem !important; + margin-left: 0.5rem !important; } + .mx-xl-3 { + margin-right: 1rem !important; + margin-left: 1rem !important; } + .mx-xl-4 { + margin-right: 1.5rem !important; + margin-left: 1.5rem !important; } + .mx-xl-5 { + margin-right: 3rem !important; + margin-left: 3rem !important; } + .mx-xl-auto { + margin-right: auto !important; + margin-left: auto !important; } + .my-xl-0 { + margin-top: 0 !important; + margin-bottom: 0 !important; } + .my-xl-1 { + margin-top: 0.25rem !important; + margin-bottom: 0.25rem !important; } + .my-xl-2 { + margin-top: 0.5rem !important; + margin-bottom: 0.5rem !important; } + .my-xl-3 { + margin-top: 1rem !important; + margin-bottom: 1rem !important; } + .my-xl-4 { + margin-top: 1.5rem !important; + margin-bottom: 1.5rem !important; } + .my-xl-5 { + margin-top: 3rem !important; + margin-bottom: 3rem !important; } + .my-xl-auto { + margin-top: auto !important; + margin-bottom: auto !important; } + .mt-xl-0 { + margin-top: 0 !important; } + .mt-xl-1 { + margin-top: 0.25rem !important; } + .mt-xl-2 { + margin-top: 0.5rem !important; } + .mt-xl-3 { + margin-top: 1rem !important; } + .mt-xl-4 { + margin-top: 1.5rem !important; } + .mt-xl-5 { + margin-top: 3rem !important; } + .mt-xl-auto { + margin-top: auto !important; } + .me-xl-0 { + margin-right: 0 !important; } + .me-xl-1 { + margin-right: 0.25rem !important; } + .me-xl-2 { + margin-right: 0.5rem !important; } + .me-xl-3 { + margin-right: 1rem !important; } + .me-xl-4 { + margin-right: 1.5rem !important; } + .me-xl-5 { + margin-right: 3rem !important; } + .me-xl-auto { + margin-right: auto !important; } + .mb-xl-0 { + margin-bottom: 0 !important; } + .mb-xl-1 { + margin-bottom: 0.25rem !important; } + .mb-xl-2 { + margin-bottom: 0.5rem !important; } + .mb-xl-3 { + margin-bottom: 1rem !important; } + .mb-xl-4 { + margin-bottom: 1.5rem !important; } + .mb-xl-5 { + margin-bottom: 3rem !important; } + .mb-xl-auto { + margin-bottom: auto !important; } + .ms-xl-0 { + margin-left: 0 !important; } + .ms-xl-1 { + margin-left: 0.25rem !important; } + .ms-xl-2 { + margin-left: 0.5rem !important; } + .ms-xl-3 { + margin-left: 1rem !important; } + .ms-xl-4 { + margin-left: 1.5rem !important; } + .ms-xl-5 { + margin-left: 3rem !important; } + .ms-xl-auto { + margin-left: auto !important; } + .m-xl-n1 { + margin: -0.25rem !important; } + .m-xl-n2 { + margin: -0.5rem !important; } + .m-xl-n3 { + margin: -1rem !important; } + .m-xl-n4 { + margin: -1.5rem !important; } + .m-xl-n5 { + margin: -3rem !important; } + .mx-xl-n1 { + margin-right: -0.25rem !important; + margin-left: -0.25rem !important; } + .mx-xl-n2 { + margin-right: -0.5rem !important; + margin-left: -0.5rem !important; } + .mx-xl-n3 { + margin-right: -1rem !important; + margin-left: -1rem !important; } + .mx-xl-n4 { + margin-right: -1.5rem !important; + margin-left: -1.5rem !important; } + .mx-xl-n5 { + margin-right: -3rem !important; + margin-left: -3rem !important; } + .my-xl-n1 { + margin-top: -0.25rem !important; + margin-bottom: -0.25rem !important; } + .my-xl-n2 { + margin-top: -0.5rem !important; + margin-bottom: -0.5rem !important; } + .my-xl-n3 { + margin-top: -1rem !important; + margin-bottom: -1rem !important; } + .my-xl-n4 { + margin-top: -1.5rem !important; + margin-bottom: -1.5rem !important; } + .my-xl-n5 { + margin-top: -3rem !important; + margin-bottom: -3rem !important; } + .mt-xl-n1 { + margin-top: -0.25rem !important; } + .mt-xl-n2 { + margin-top: -0.5rem !important; } + .mt-xl-n3 { + margin-top: -1rem !important; } + .mt-xl-n4 { + margin-top: -1.5rem !important; } + .mt-xl-n5 { + margin-top: -3rem !important; } + .me-xl-n1 { + margin-right: -0.25rem !important; } + .me-xl-n2 { + margin-right: -0.5rem !important; } + .me-xl-n3 { + margin-right: -1rem !important; } + .me-xl-n4 { + margin-right: -1.5rem !important; } + .me-xl-n5 { + margin-right: -3rem !important; } + .mb-xl-n1 { + margin-bottom: -0.25rem !important; } + .mb-xl-n2 { + margin-bottom: -0.5rem !important; } + .mb-xl-n3 { + margin-bottom: -1rem !important; } + .mb-xl-n4 { + margin-bottom: -1.5rem !important; } + .mb-xl-n5 { + margin-bottom: -3rem !important; } + .ms-xl-n1 { + margin-left: -0.25rem !important; } + .ms-xl-n2 { + margin-left: -0.5rem !important; } + .ms-xl-n3 { + margin-left: -1rem !important; } + .ms-xl-n4 { + margin-left: -1.5rem !important; } + .ms-xl-n5 { + margin-left: -3rem !important; } + .p-xl-0 { + padding: 0 !important; } + .p-xl-1 { + padding: 0.25rem !important; } + .p-xl-2 { + padding: 0.5rem !important; } + .p-xl-3 { + padding: 1rem !important; } + .p-xl-4 { + padding: 1.5rem !important; } + .p-xl-5 { + padding: 3rem !important; } + .px-xl-0 { + padding-right: 0 !important; + padding-left: 0 !important; } + .px-xl-1 { + padding-right: 0.25rem !important; + padding-left: 0.25rem !important; } + .px-xl-2 { + padding-right: 0.5rem !important; + padding-left: 0.5rem !important; } + .px-xl-3 { + padding-right: 1rem !important; + padding-left: 1rem !important; } + .px-xl-4 { + padding-right: 1.5rem !important; + padding-left: 1.5rem !important; } + .px-xl-5 { + padding-right: 3rem !important; + padding-left: 3rem !important; } + .py-xl-0 { + padding-top: 0 !important; + padding-bottom: 0 !important; } + .py-xl-1 { + padding-top: 0.25rem !important; + padding-bottom: 0.25rem !important; } + .py-xl-2 { + padding-top: 0.5rem !important; + padding-bottom: 0.5rem !important; } + .py-xl-3 { + padding-top: 1rem !important; + padding-bottom: 1rem !important; } + .py-xl-4 { + padding-top: 1.5rem !important; + padding-bottom: 1.5rem !important; } + .py-xl-5 { + padding-top: 3rem !important; + padding-bottom: 3rem !important; } + .pt-xl-0 { + padding-top: 0 !important; } + .pt-xl-1 { + padding-top: 0.25rem !important; } + .pt-xl-2 { + padding-top: 0.5rem !important; } + .pt-xl-3 { + padding-top: 1rem !important; } + .pt-xl-4 { + padding-top: 1.5rem !important; } + .pt-xl-5 { + padding-top: 3rem !important; } + .pe-xl-0 { + padding-right: 0 !important; } + .pe-xl-1 { + padding-right: 0.25rem !important; } + .pe-xl-2 { + padding-right: 0.5rem !important; } + .pe-xl-3 { + padding-right: 1rem !important; } + .pe-xl-4 { + padding-right: 1.5rem !important; } + .pe-xl-5 { + padding-right: 3rem !important; } + .pb-xl-0 { + padding-bottom: 0 !important; } + .pb-xl-1 { + padding-bottom: 0.25rem !important; } + .pb-xl-2 { + padding-bottom: 0.5rem !important; } + .pb-xl-3 { + padding-bottom: 1rem !important; } + .pb-xl-4 { + padding-bottom: 1.5rem !important; } + .pb-xl-5 { + padding-bottom: 3rem !important; } + .ps-xl-0 { + padding-left: 0 !important; } + .ps-xl-1 { + padding-left: 0.25rem !important; } + .ps-xl-2 { + padding-left: 0.5rem !important; } + .ps-xl-3 { + padding-left: 1rem !important; } + .ps-xl-4 { + padding-left: 1.5rem !important; } + .ps-xl-5 { + padding-left: 3rem !important; } + .text-xl-start { + text-align: left !important; } + .text-xl-end { + text-align: right !important; } + .text-xl-center { + text-align: center !important; } } + +@media (min-width: 1400px) { + .float-xxl-start { + float: left !important; } + .float-xxl-end { + float: right !important; } + .float-xxl-none { + float: none !important; } + .d-xxl-inline { + display: inline !important; } + .d-xxl-inline-block { + display: inline-block !important; } + .d-xxl-block { + display: block !important; } + .d-xxl-grid { + display: grid !important; } + .d-xxl-table { + display: table !important; } + .d-xxl-table-row { + display: table-row !important; } + .d-xxl-table-cell { + display: table-cell !important; } + .d-xxl-flex { + display: -webkit-box !important; + display: -ms-flexbox !important; + display: flex !important; } + .d-xxl-inline-flex { + display: -webkit-inline-box !important; + display: -ms-inline-flexbox !important; + display: inline-flex !important; } + .d-xxl-none { + display: none !important; } + .flex-xxl-fill { + -webkit-box-flex: 1 !important; + -ms-flex: 1 1 auto !important; + flex: 1 1 auto !important; } + .flex-xxl-row { + -webkit-box-orient: horizontal !important; + -webkit-box-direction: normal !important; + -ms-flex-direction: row !important; + flex-direction: row !important; } + .flex-xxl-column { + -webkit-box-orient: vertical !important; + -webkit-box-direction: normal !important; + -ms-flex-direction: column !important; + flex-direction: column !important; } + .flex-xxl-row-reverse { + -webkit-box-orient: horizontal !important; + -webkit-box-direction: reverse !important; + -ms-flex-direction: row-reverse !important; + flex-direction: row-reverse !important; } + .flex-xxl-column-reverse { + -webkit-box-orient: vertical !important; + -webkit-box-direction: reverse !important; + -ms-flex-direction: column-reverse !important; + flex-direction: column-reverse !important; } + .flex-xxl-grow-0 { + -webkit-box-flex: 0 !important; + -ms-flex-positive: 0 !important; + flex-grow: 0 !important; } + .flex-xxl-grow-1 { + -webkit-box-flex: 1 !important; + -ms-flex-positive: 1 !important; + flex-grow: 1 !important; } + .flex-xxl-shrink-0 { + -ms-flex-negative: 0 !important; + flex-shrink: 0 !important; } + .flex-xxl-shrink-1 { + -ms-flex-negative: 1 !important; + flex-shrink: 1 !important; } + .flex-xxl-wrap { + -ms-flex-wrap: wrap !important; + flex-wrap: wrap !important; } + .flex-xxl-nowrap { + -ms-flex-wrap: nowrap !important; + flex-wrap: nowrap !important; } + .flex-xxl-wrap-reverse { + -ms-flex-wrap: wrap-reverse !important; + flex-wrap: wrap-reverse !important; } + .gap-xxl-0 { + gap: 0 !important; } + .gap-xxl-1 { + gap: 0.25rem !important; } + .gap-xxl-2 { + gap: 0.5rem !important; } + .gap-xxl-3 { + gap: 1rem !important; } + .gap-xxl-4 { + gap: 1.5rem !important; } + .gap-xxl-5 { + gap: 3rem !important; } + .justify-content-xxl-start { + -webkit-box-pack: start !important; + -ms-flex-pack: start !important; + justify-content: flex-start !important; } + .justify-content-xxl-end { + -webkit-box-pack: end !important; + -ms-flex-pack: end !important; + justify-content: flex-end !important; } + .justify-content-xxl-center { + -webkit-box-pack: center !important; + -ms-flex-pack: center !important; + justify-content: center !important; } + .justify-content-xxl-between { + -webkit-box-pack: justify !important; + -ms-flex-pack: justify !important; + justify-content: space-between !important; } + .justify-content-xxl-around { + -ms-flex-pack: distribute !important; + justify-content: space-around !important; } + .justify-content-xxl-evenly { + -webkit-box-pack: space-evenly !important; + -ms-flex-pack: space-evenly !important; + justify-content: space-evenly !important; } + .align-items-xxl-start { + -webkit-box-align: start !important; + -ms-flex-align: start !important; + align-items: flex-start !important; } + .align-items-xxl-end { + -webkit-box-align: end !important; + -ms-flex-align: end !important; + align-items: flex-end !important; } + .align-items-xxl-center { + -webkit-box-align: center !important; + -ms-flex-align: center !important; + align-items: center !important; } + .align-items-xxl-baseline { + -webkit-box-align: baseline !important; + -ms-flex-align: baseline !important; + align-items: baseline !important; } + .align-items-xxl-stretch { + -webkit-box-align: stretch !important; + -ms-flex-align: stretch !important; + align-items: stretch !important; } + .align-content-xxl-start { + -ms-flex-line-pack: start !important; + align-content: flex-start !important; } + .align-content-xxl-end { + -ms-flex-line-pack: end !important; + align-content: flex-end !important; } + .align-content-xxl-center { + -ms-flex-line-pack: center !important; + align-content: center !important; } + .align-content-xxl-between { + -ms-flex-line-pack: justify !important; + align-content: space-between !important; } + .align-content-xxl-around { + -ms-flex-line-pack: distribute !important; + align-content: space-around !important; } + .align-content-xxl-stretch { + -ms-flex-line-pack: stretch !important; + align-content: stretch !important; } + .align-self-xxl-auto { + -ms-flex-item-align: auto !important; + align-self: auto !important; } + .align-self-xxl-start { + -ms-flex-item-align: start !important; + align-self: flex-start !important; } + .align-self-xxl-end { + -ms-flex-item-align: end !important; + align-self: flex-end !important; } + .align-self-xxl-center { + -ms-flex-item-align: center !important; + align-self: center !important; } + .align-self-xxl-baseline { + -ms-flex-item-align: baseline !important; + align-self: baseline !important; } + .align-self-xxl-stretch { + -ms-flex-item-align: stretch !important; + align-self: stretch !important; } + .order-xxl-first { + -webkit-box-ordinal-group: 0 !important; + -ms-flex-order: -1 !important; + order: -1 !important; } + .order-xxl-0 { + -webkit-box-ordinal-group: 1 !important; + -ms-flex-order: 0 !important; + order: 0 !important; } + .order-xxl-1 { + -webkit-box-ordinal-group: 2 !important; + -ms-flex-order: 1 !important; + order: 1 !important; } + .order-xxl-2 { + -webkit-box-ordinal-group: 3 !important; + -ms-flex-order: 2 !important; + order: 2 !important; } + .order-xxl-3 { + -webkit-box-ordinal-group: 4 !important; + -ms-flex-order: 3 !important; + order: 3 !important; } + .order-xxl-4 { + -webkit-box-ordinal-group: 5 !important; + -ms-flex-order: 4 !important; + order: 4 !important; } + .order-xxl-5 { + -webkit-box-ordinal-group: 6 !important; + -ms-flex-order: 5 !important; + order: 5 !important; } + .order-xxl-last { + -webkit-box-ordinal-group: 7 !important; + -ms-flex-order: 6 !important; + order: 6 !important; } + .m-xxl-0 { + margin: 0 !important; } + .m-xxl-1 { + margin: 0.25rem !important; } + .m-xxl-2 { + margin: 0.5rem !important; } + .m-xxl-3 { + margin: 1rem !important; } + .m-xxl-4 { + margin: 1.5rem !important; } + .m-xxl-5 { + margin: 3rem !important; } + .m-xxl-auto { + margin: auto !important; } + .mx-xxl-0 { + margin-right: 0 !important; + margin-left: 0 !important; } + .mx-xxl-1 { + margin-right: 0.25rem !important; + margin-left: 0.25rem !important; } + .mx-xxl-2 { + margin-right: 0.5rem !important; + margin-left: 0.5rem !important; } + .mx-xxl-3 { + margin-right: 1rem !important; + margin-left: 1rem !important; } + .mx-xxl-4 { + margin-right: 1.5rem !important; + margin-left: 1.5rem !important; } + .mx-xxl-5 { + margin-right: 3rem !important; + margin-left: 3rem !important; } + .mx-xxl-auto { + margin-right: auto !important; + margin-left: auto !important; } + .my-xxl-0 { + margin-top: 0 !important; + margin-bottom: 0 !important; } + .my-xxl-1 { + margin-top: 0.25rem !important; + margin-bottom: 0.25rem !important; } + .my-xxl-2 { + margin-top: 0.5rem !important; + margin-bottom: 0.5rem !important; } + .my-xxl-3 { + margin-top: 1rem !important; + margin-bottom: 1rem !important; } + .my-xxl-4 { + margin-top: 1.5rem !important; + margin-bottom: 1.5rem !important; } + .my-xxl-5 { + margin-top: 3rem !important; + margin-bottom: 3rem !important; } + .my-xxl-auto { + margin-top: auto !important; + margin-bottom: auto !important; } + .mt-xxl-0 { + margin-top: 0 !important; } + .mt-xxl-1 { + margin-top: 0.25rem !important; } + .mt-xxl-2 { + margin-top: 0.5rem !important; } + .mt-xxl-3 { + margin-top: 1rem !important; } + .mt-xxl-4 { + margin-top: 1.5rem !important; } + .mt-xxl-5 { + margin-top: 3rem !important; } + .mt-xxl-auto { + margin-top: auto !important; } + .me-xxl-0 { + margin-right: 0 !important; } + .me-xxl-1 { + margin-right: 0.25rem !important; } + .me-xxl-2 { + margin-right: 0.5rem !important; } + .me-xxl-3 { + margin-right: 1rem !important; } + .me-xxl-4 { + margin-right: 1.5rem !important; } + .me-xxl-5 { + margin-right: 3rem !important; } + .me-xxl-auto { + margin-right: auto !important; } + .mb-xxl-0 { + margin-bottom: 0 !important; } + .mb-xxl-1 { + margin-bottom: 0.25rem !important; } + .mb-xxl-2 { + margin-bottom: 0.5rem !important; } + .mb-xxl-3 { + margin-bottom: 1rem !important; } + .mb-xxl-4 { + margin-bottom: 1.5rem !important; } + .mb-xxl-5 { + margin-bottom: 3rem !important; } + .mb-xxl-auto { + margin-bottom: auto !important; } + .ms-xxl-0 { + margin-left: 0 !important; } + .ms-xxl-1 { + margin-left: 0.25rem !important; } + .ms-xxl-2 { + margin-left: 0.5rem !important; } + .ms-xxl-3 { + margin-left: 1rem !important; } + .ms-xxl-4 { + margin-left: 1.5rem !important; } + .ms-xxl-5 { + margin-left: 3rem !important; } + .ms-xxl-auto { + margin-left: auto !important; } + .m-xxl-n1 { + margin: -0.25rem !important; } + .m-xxl-n2 { + margin: -0.5rem !important; } + .m-xxl-n3 { + margin: -1rem !important; } + .m-xxl-n4 { + margin: -1.5rem !important; } + .m-xxl-n5 { + margin: -3rem !important; } + .mx-xxl-n1 { + margin-right: -0.25rem !important; + margin-left: -0.25rem !important; } + .mx-xxl-n2 { + margin-right: -0.5rem !important; + margin-left: -0.5rem !important; } + .mx-xxl-n3 { + margin-right: -1rem !important; + margin-left: -1rem !important; } + .mx-xxl-n4 { + margin-right: -1.5rem !important; + margin-left: -1.5rem !important; } + .mx-xxl-n5 { + margin-right: -3rem !important; + margin-left: -3rem !important; } + .my-xxl-n1 { + margin-top: -0.25rem !important; + margin-bottom: -0.25rem !important; } + .my-xxl-n2 { + margin-top: -0.5rem !important; + margin-bottom: -0.5rem !important; } + .my-xxl-n3 { + margin-top: -1rem !important; + margin-bottom: -1rem !important; } + .my-xxl-n4 { + margin-top: -1.5rem !important; + margin-bottom: -1.5rem !important; } + .my-xxl-n5 { + margin-top: -3rem !important; + margin-bottom: -3rem !important; } + .mt-xxl-n1 { + margin-top: -0.25rem !important; } + .mt-xxl-n2 { + margin-top: -0.5rem !important; } + .mt-xxl-n3 { + margin-top: -1rem !important; } + .mt-xxl-n4 { + margin-top: -1.5rem !important; } + .mt-xxl-n5 { + margin-top: -3rem !important; } + .me-xxl-n1 { + margin-right: -0.25rem !important; } + .me-xxl-n2 { + margin-right: -0.5rem !important; } + .me-xxl-n3 { + margin-right: -1rem !important; } + .me-xxl-n4 { + margin-right: -1.5rem !important; } + .me-xxl-n5 { + margin-right: -3rem !important; } + .mb-xxl-n1 { + margin-bottom: -0.25rem !important; } + .mb-xxl-n2 { + margin-bottom: -0.5rem !important; } + .mb-xxl-n3 { + margin-bottom: -1rem !important; } + .mb-xxl-n4 { + margin-bottom: -1.5rem !important; } + .mb-xxl-n5 { + margin-bottom: -3rem !important; } + .ms-xxl-n1 { + margin-left: -0.25rem !important; } + .ms-xxl-n2 { + margin-left: -0.5rem !important; } + .ms-xxl-n3 { + margin-left: -1rem !important; } + .ms-xxl-n4 { + margin-left: -1.5rem !important; } + .ms-xxl-n5 { + margin-left: -3rem !important; } + .p-xxl-0 { + padding: 0 !important; } + .p-xxl-1 { + padding: 0.25rem !important; } + .p-xxl-2 { + padding: 0.5rem !important; } + .p-xxl-3 { + padding: 1rem !important; } + .p-xxl-4 { + padding: 1.5rem !important; } + .p-xxl-5 { + padding: 3rem !important; } + .px-xxl-0 { + padding-right: 0 !important; + padding-left: 0 !important; } + .px-xxl-1 { + padding-right: 0.25rem !important; + padding-left: 0.25rem !important; } + .px-xxl-2 { + padding-right: 0.5rem !important; + padding-left: 0.5rem !important; } + .px-xxl-3 { + padding-right: 1rem !important; + padding-left: 1rem !important; } + .px-xxl-4 { + padding-right: 1.5rem !important; + padding-left: 1.5rem !important; } + .px-xxl-5 { + padding-right: 3rem !important; + padding-left: 3rem !important; } + .py-xxl-0 { + padding-top: 0 !important; + padding-bottom: 0 !important; } + .py-xxl-1 { + padding-top: 0.25rem !important; + padding-bottom: 0.25rem !important; } + .py-xxl-2 { + padding-top: 0.5rem !important; + padding-bottom: 0.5rem !important; } + .py-xxl-3 { + padding-top: 1rem !important; + padding-bottom: 1rem !important; } + .py-xxl-4 { + padding-top: 1.5rem !important; + padding-bottom: 1.5rem !important; } + .py-xxl-5 { + padding-top: 3rem !important; + padding-bottom: 3rem !important; } + .pt-xxl-0 { + padding-top: 0 !important; } + .pt-xxl-1 { + padding-top: 0.25rem !important; } + .pt-xxl-2 { + padding-top: 0.5rem !important; } + .pt-xxl-3 { + padding-top: 1rem !important; } + .pt-xxl-4 { + padding-top: 1.5rem !important; } + .pt-xxl-5 { + padding-top: 3rem !important; } + .pe-xxl-0 { + padding-right: 0 !important; } + .pe-xxl-1 { + padding-right: 0.25rem !important; } + .pe-xxl-2 { + padding-right: 0.5rem !important; } + .pe-xxl-3 { + padding-right: 1rem !important; } + .pe-xxl-4 { + padding-right: 1.5rem !important; } + .pe-xxl-5 { + padding-right: 3rem !important; } + .pb-xxl-0 { + padding-bottom: 0 !important; } + .pb-xxl-1 { + padding-bottom: 0.25rem !important; } + .pb-xxl-2 { + padding-bottom: 0.5rem !important; } + .pb-xxl-3 { + padding-bottom: 1rem !important; } + .pb-xxl-4 { + padding-bottom: 1.5rem !important; } + .pb-xxl-5 { + padding-bottom: 3rem !important; } + .ps-xxl-0 { + padding-left: 0 !important; } + .ps-xxl-1 { + padding-left: 0.25rem !important; } + .ps-xxl-2 { + padding-left: 0.5rem !important; } + .ps-xxl-3 { + padding-left: 1rem !important; } + .ps-xxl-4 { + padding-left: 1.5rem !important; } + .ps-xxl-5 { + padding-left: 3rem !important; } + .text-xxl-start { + text-align: left !important; } + .text-xxl-end { + text-align: right !important; } + .text-xxl-center { + text-align: center !important; } } + +@media (min-width: 1200px) { + .fs-1 { + font-size: 2.25rem !important; } + .fs-2 { + font-size: 1.8rem !important; } + .fs-3 { + font-size: 1.575rem !important; } + .fs-4 { + font-size: 1.35rem !important; } } + +@media print { + .d-print-inline { + display: inline !important; } + .d-print-inline-block { + display: inline-block !important; } + .d-print-block { + display: block !important; } + .d-print-grid { + display: grid !important; } + .d-print-table { + display: table !important; } + .d-print-table-row { + display: table-row !important; } + .d-print-table-cell { + display: table-cell !important; } + .d-print-flex { + display: -webkit-box !important; + display: -ms-flexbox !important; + display: flex !important; } + .d-print-inline-flex { + display: -webkit-inline-box !important; + display: -ms-inline-flexbox !important; + display: inline-flex !important; } + .d-print-none { + display: none !important; } } + +/* ============= + General +============= */ +html { + position: relative; + min-height: 100%; } + +.h1, +.h2, +.h3, +.h4, +.h5, +.h6, h1, .h1, h2, .h2, h3, .h3, h4, .h4, h5, .h5, h6, .h6 { + color: #495057; + font-weight: 500; + font-family: "Roboto", sans-serif; } + +a { + text-decoration: none !important; } + +label { + font-weight: 500; + margin-bottom: 0.5rem; } + +.ff-primary { + font-family: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", "Liberation Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; } + +.ff-secondary { + font-family: "Roboto", sans-serif; } + +.small, small, .small { + font-weight: 400; } + +.blockquote { + padding: 10px 20px; + border-left: 4px solid #eff0f2; } + +.blockquote-reverse { + border-left: 0; + border-right: 4px solid #eff0f2; + text-align: right; } + +@media (min-width: 1200px) { + .container, + .container-lg, + .container-md, + .container-sm, + .container-xl, + .container-xxl { + max-width: 1140px; } } + +.row > * { + position: relative; } + +.icon { + position: relative; + top: -2px; } + +.icon-xxs { + height: 14px; + width: 14px; } + +.icon-xs { + height: 16px; + width: 16px; } + +.icon-sm { + height: 20px; + width: 20px; } + +.icon-lg { + height: 32px; + width: 32px; } + +.icon-xl { + height: 46px; + width: 46px; } + +.icon-xxl { + height: 62px; + width: 62px; } + +.sw-3 { + stroke-width: 3px; } + +.sw-1_5 { + stroke-width: 1.5px; } + +.sw-1 { + stroke-width: 1px; } + +.icon-dual-primary { + color: #2e86de; + fill: rgba(3, 142, 220, 0.16); } + +.icon-fill-primary { + fill: #2e86de !important; } + +.icon-dual-secondary { + color: #74788d; + fill: rgba(116, 120, 141, 0.16); } + +.icon-fill-secondary { + fill: #74788d !important; } + +.icon-dual-success { + color: #51d28c; + fill: rgba(81, 210, 140, 0.16); } + +.icon-fill-success { + fill: #51d28c !important; } + +.icon-dual-info { + color: #5fd0f3; + fill: rgba(95, 208, 243, 0.16); } + +.icon-fill-info { + fill: #5fd0f3 !important; } + +.icon-dual-warning { + color: #f7cc53; + fill: rgba(247, 204, 83, 0.16); } + +.icon-fill-warning { + fill: #f7cc53 !important; } + +.icon-dual-danger { + color: #f34e4e; + fill: rgba(243, 78, 78, 0.16); } + +.icon-fill-danger { + fill: #f34e4e !important; } + +.icon-dual-pink { + color: #e83e8c; + fill: rgba(232, 62, 140, 0.16); } + +.icon-fill-pink { + fill: #e83e8c !important; } + +.icon-dual-light { + color: #f5f6f8; + fill: rgba(245, 246, 248, 0.16); } + +.icon-fill-light { + fill: #f5f6f8 !important; } + +.icon-dual-dark { + color: #343a40; + fill: rgba(52, 58, 64, 0.16); } + +.icon-fill-dark { + fill: #343a40 !important; } + +.icon-dual-purple { + color: #564ab1; + fill: rgba(86, 74, 177, 0.16); } + +.icon-fill-purple { + fill: #564ab1 !important; } + +.custom-blockpuote.blockquote { + padding: 20px 20px 20px 30px; + border-left: 3px solid; + font-size: 16px; } + +.custom-blockpuote.blockquote.blockpuote-primary { + color: #2e86de; + border-color: #2e86de; + background-color: rgba(3, 142, 220, 0.15); } + .custom-blockpuote.blockquote.blockpuote-primary .blockquote-footer { + color: #2e86de; } + +.custom-blockpuote.blockquote.blockpuote-outline-primary { + border: 1px solid #2e86de; + border-left: 4px solid #2e86de; } + .custom-blockpuote.blockquote.blockpuote-outline-primary .blockquote-footer { + color: #2e86de; } + +.custom-blockpuote.blockquote.blockpuote-secondary { + color: #74788d; + border-color: #74788d; + background-color: rgba(116, 120, 141, 0.15); } + .custom-blockpuote.blockquote.blockpuote-secondary .blockquote-footer { + color: #74788d; } + +.custom-blockpuote.blockquote.blockpuote-outline-secondary { + border: 1px solid #74788d; + border-left: 4px solid #74788d; } + .custom-blockpuote.blockquote.blockpuote-outline-secondary .blockquote-footer { + color: #74788d; } + +.custom-blockpuote.blockquote.blockpuote-success { + color: #51d28c; + border-color: #51d28c; + background-color: rgba(81, 210, 140, 0.15); } + .custom-blockpuote.blockquote.blockpuote-success .blockquote-footer { + color: #51d28c; } + +.custom-blockpuote.blockquote.blockpuote-outline-success { + border: 1px solid #51d28c; + border-left: 4px solid #51d28c; } + .custom-blockpuote.blockquote.blockpuote-outline-success .blockquote-footer { + color: #51d28c; } + +.custom-blockpuote.blockquote.blockpuote-info { + color: #5fd0f3; + border-color: #5fd0f3; + background-color: rgba(95, 208, 243, 0.15); } + .custom-blockpuote.blockquote.blockpuote-info .blockquote-footer { + color: #5fd0f3; } + +.custom-blockpuote.blockquote.blockpuote-outline-info { + border: 1px solid #5fd0f3; + border-left: 4px solid #5fd0f3; } + .custom-blockpuote.blockquote.blockpuote-outline-info .blockquote-footer { + color: #5fd0f3; } + +.custom-blockpuote.blockquote.blockpuote-warning { + color: #f7cc53; + border-color: #f7cc53; + background-color: rgba(247, 204, 83, 0.15); } + .custom-blockpuote.blockquote.blockpuote-warning .blockquote-footer { + color: #f7cc53; } + +.custom-blockpuote.blockquote.blockpuote-outline-warning { + border: 1px solid #f7cc53; + border-left: 4px solid #f7cc53; } + .custom-blockpuote.blockquote.blockpuote-outline-warning .blockquote-footer { + color: #f7cc53; } + +.custom-blockpuote.blockquote.blockpuote-danger { + color: #f34e4e; + border-color: #f34e4e; + background-color: rgba(243, 78, 78, 0.15); } + .custom-blockpuote.blockquote.blockpuote-danger .blockquote-footer { + color: #f34e4e; } + +.custom-blockpuote.blockquote.blockpuote-outline-danger { + border: 1px solid #f34e4e; + border-left: 4px solid #f34e4e; } + .custom-blockpuote.blockquote.blockpuote-outline-danger .blockquote-footer { + color: #f34e4e; } + +.custom-blockpuote.blockquote.blockpuote-pink { + color: #e83e8c; + border-color: #e83e8c; + background-color: rgba(232, 62, 140, 0.15); } + .custom-blockpuote.blockquote.blockpuote-pink .blockquote-footer { + color: #e83e8c; } + +.custom-blockpuote.blockquote.blockpuote-outline-pink { + border: 1px solid #e83e8c; + border-left: 4px solid #e83e8c; } + .custom-blockpuote.blockquote.blockpuote-outline-pink .blockquote-footer { + color: #e83e8c; } + +.custom-blockpuote.blockquote.blockpuote-light { + color: #f5f6f8; + border-color: #f5f6f8; + background-color: rgba(245, 246, 248, 0.15); } + .custom-blockpuote.blockquote.blockpuote-light .blockquote-footer { + color: #f5f6f8; } + +.custom-blockpuote.blockquote.blockpuote-outline-light { + border: 1px solid #f5f6f8; + border-left: 4px solid #f5f6f8; } + .custom-blockpuote.blockquote.blockpuote-outline-light .blockquote-footer { + color: #f5f6f8; } + +.custom-blockpuote.blockquote.blockpuote-dark { + color: #343a40; + border-color: #343a40; + background-color: rgba(52, 58, 64, 0.15); } + .custom-blockpuote.blockquote.blockpuote-dark .blockquote-footer { + color: #343a40; } + +.custom-blockpuote.blockquote.blockpuote-outline-dark { + border: 1px solid #343a40; + border-left: 4px solid #343a40; } + .custom-blockpuote.blockquote.blockpuote-outline-dark .blockquote-footer { + color: #343a40; } + +.custom-blockpuote.blockquote.blockpuote-purple { + color: #564ab1; + border-color: #564ab1; + background-color: rgba(86, 74, 177, 0.15); } + .custom-blockpuote.blockquote.blockpuote-purple .blockquote-footer { + color: #564ab1; } + +.custom-blockpuote.blockquote.blockpuote-outline-purple { + border: 1px solid #564ab1; + border-left: 4px solid #564ab1; } + .custom-blockpuote.blockquote.blockpuote-outline-purple .blockquote-footer { + color: #564ab1; } + +.bg-soft-primary { + background-color: rgba(3, 142, 220, 0.25) !important; } + +.bg-soft-secondary { + background-color: rgba(116, 120, 141, 0.25) !important; } + +.bg-soft-success { + background-color: rgba(81, 210, 140, 0.25) !important; } + +.bg-soft-info { + background-color: rgba(95, 208, 243, 0.25) !important; } + +.bg-soft-warning { + background-color: rgba(247, 204, 83, 0.25) !important; } + +.bg-soft-danger { + background-color: rgba(243, 78, 78, 0.25) !important; } + +.bg-soft-pink { + background-color: rgba(232, 62, 140, 0.25) !important; } + +.bg-soft-light { + background-color: rgba(245, 246, 248, 0.25) !important; } + +.bg-soft-dark { + background-color: rgba(52, 58, 64, 0.25) !important; } + +.bg-soft-purple { + background-color: rgba(86, 74, 177, 0.25) !important; } + +body[data-layout-mode="dark"] .bg-body { + background-color: #03273c !important; } + +body[data-layout-mode="dark"] .bg-light { + background-color: #043a5a !important; } + +body[data-layout-mode="dark"] .bg-dark { + background-color: #033350 !important; } + +body[data-layout-mode="dark"] .bg-soft-light { + background-color: rgba(4, 58, 90, 0.25) !important; } + +.badge-soft-primary { + color: #2e86de; + background-color: rgba(3, 142, 220, 0.1); } + +.badge-soft-secondary { + color: #74788d; + background-color: rgba(116, 120, 141, 0.1); } + +.badge-soft-success { + color: #51d28c; + background-color: rgba(81, 210, 140, 0.1); } + +.badge-soft-info { + color: #5fd0f3; + background-color: rgba(95, 208, 243, 0.1); } + +.badge-soft-warning { + color: #f7cc53; + background-color: rgba(247, 204, 83, 0.1); } + +.badge-soft-danger { + color: #f34e4e; + background-color: rgba(243, 78, 78, 0.1); } + +.badge-soft-pink { + color: #e83e8c; + background-color: rgba(232, 62, 140, 0.1); } + +.badge-soft-light { + color: #f5f6f8; + background-color: rgba(245, 246, 248, 0.1); } + +.badge-soft-dark { + color: #343a40; + background-color: rgba(52, 58, 64, 0.1); } + +.badge-soft-purple { + color: #564ab1; + background-color: rgba(86, 74, 177, 0.1); } + +.badge-outline-primary { + color: #2e86de; + border: 1px solid #2e86de; + background-color: transparent; } + +.badge-outline-secondary { + color: #74788d; + border: 1px solid #74788d; + background-color: transparent; } + +.badge-outline-success { + color: #51d28c; + border: 1px solid #51d28c; + background-color: transparent; } + +.badge-outline-info { + color: #5fd0f3; + border: 1px solid #5fd0f3; + background-color: transparent; } + +.badge-outline-warning { + color: #f7cc53; + border: 1px solid #f7cc53; + background-color: transparent; } + +.badge-outline-danger { + color: #f34e4e; + border: 1px solid #f34e4e; + background-color: transparent; } + +.badge-outline-pink { + color: #e83e8c; + border: 1px solid #e83e8c; + background-color: transparent; } + +.badge-outline-light { + color: #f5f6f8; + border: 1px solid #f5f6f8; + background-color: transparent; } + +.badge-outline-dark { + color: #343a40; + border: 1px solid #343a40; + background-color: transparent; } + +.badge-outline-purple { + color: #564ab1; + border: 1px solid #564ab1; + background-color: transparent; } + +.badge.bg-light, .badge.badge-soft-light, .badge.badge-outline-light { + color: #343a40; } + +body[data-layout-mode=dark] .badge.bg-light, body[data-layout-mode=dark] .badge.badge-soft-light, body[data-layout-mode=dark] .badge.badge-outline-light { + color: #adb5bd; } + +body[data-layout-mode=dark] .badge-soft-dark { + color: #adb5bd; } + +button, +a { + outline: none !important; } + +.btn-warning { + color: #fff !important; } + +.btn-soft-primary { + color: #2e86de; + background-color: rgba(3, 142, 220, 0.1); + border-color: transparent; } + .btn-soft-primary:hover, .btn-soft-primary:focus, .btn-soft-primary:active { + color: #fff; + background-color: #2e86de; } + .btn-soft-primary:focus { + -webkit-box-shadow: 0 0 0 0.15rem rgba(3, 142, 220, 0.5); + box-shadow: 0 0 0 0.15rem rgba(3, 142, 220, 0.5); } + +.btn-soft-secondary { + color: #74788d; + background-color: rgba(116, 120, 141, 0.1); + border-color: transparent; } + .btn-soft-secondary:hover, .btn-soft-secondary:focus, .btn-soft-secondary:active { + color: #fff; + background-color: #74788d; } + .btn-soft-secondary:focus { + -webkit-box-shadow: 0 0 0 0.15rem rgba(116, 120, 141, 0.5); + box-shadow: 0 0 0 0.15rem rgba(116, 120, 141, 0.5); } + +.btn-soft-success { + color: #51d28c; + background-color: rgba(81, 210, 140, 0.1); + border-color: transparent; } + .btn-soft-success:hover, .btn-soft-success:focus, .btn-soft-success:active { + color: #fff; + background-color: #51d28c; } + .btn-soft-success:focus { + -webkit-box-shadow: 0 0 0 0.15rem rgba(81, 210, 140, 0.5); + box-shadow: 0 0 0 0.15rem rgba(81, 210, 140, 0.5); } + +.btn-soft-info { + color: #5fd0f3; + background-color: rgba(95, 208, 243, 0.1); + border-color: transparent; } + .btn-soft-info:hover, .btn-soft-info:focus, .btn-soft-info:active { + color: #fff; + background-color: #5fd0f3; } + .btn-soft-info:focus { + -webkit-box-shadow: 0 0 0 0.15rem rgba(95, 208, 243, 0.5); + box-shadow: 0 0 0 0.15rem rgba(95, 208, 243, 0.5); } + +.btn-soft-warning { + color: #f7cc53; + background-color: rgba(247, 204, 83, 0.1); + border-color: transparent; } + .btn-soft-warning:hover, .btn-soft-warning:focus, .btn-soft-warning:active { + color: #fff; + background-color: #f7cc53; } + .btn-soft-warning:focus { + -webkit-box-shadow: 0 0 0 0.15rem rgba(247, 204, 83, 0.5); + box-shadow: 0 0 0 0.15rem rgba(247, 204, 83, 0.5); } + +.btn-soft-danger { + color: #f34e4e; + background-color: rgba(243, 78, 78, 0.1); + border-color: transparent; } + .btn-soft-danger:hover, .btn-soft-danger:focus, .btn-soft-danger:active { + color: #fff; + background-color: #f34e4e; } + .btn-soft-danger:focus { + -webkit-box-shadow: 0 0 0 0.15rem rgba(243, 78, 78, 0.5); + box-shadow: 0 0 0 0.15rem rgba(243, 78, 78, 0.5); } + +.btn-soft-pink { + color: #e83e8c; + background-color: rgba(232, 62, 140, 0.1); + border-color: transparent; } + .btn-soft-pink:hover, .btn-soft-pink:focus, .btn-soft-pink:active { + color: #fff; + background-color: #e83e8c; } + .btn-soft-pink:focus { + -webkit-box-shadow: 0 0 0 0.15rem rgba(232, 62, 140, 0.5); + box-shadow: 0 0 0 0.15rem rgba(232, 62, 140, 0.5); } + +.btn-soft-light { + color: #f5f6f8; + background-color: rgba(245, 246, 248, 0.1); + border-color: transparent; } + .btn-soft-light:hover, .btn-soft-light:focus, .btn-soft-light:active { + color: #fff; + background-color: #f5f6f8; } + .btn-soft-light:focus { + -webkit-box-shadow: 0 0 0 0.15rem rgba(245, 246, 248, 0.5); + box-shadow: 0 0 0 0.15rem rgba(245, 246, 248, 0.5); } + +.btn-soft-dark { + color: #343a40; + background-color: rgba(52, 58, 64, 0.1); + border-color: transparent; } + .btn-soft-dark:hover, .btn-soft-dark:focus, .btn-soft-dark:active { + color: #fff; + background-color: #343a40; } + .btn-soft-dark:focus { + -webkit-box-shadow: 0 0 0 0.15rem rgba(52, 58, 64, 0.5); + box-shadow: 0 0 0 0.15rem rgba(52, 58, 64, 0.5); } + +.btn-soft-purple { + color: #564ab1; + background-color: rgba(86, 74, 177, 0.1); + border-color: transparent; } + .btn-soft-purple:hover, .btn-soft-purple:focus, .btn-soft-purple:active { + color: #fff; + background-color: #564ab1; } + .btn-soft-purple:focus { + -webkit-box-shadow: 0 0 0 0.15rem rgba(86, 74, 177, 0.5); + box-shadow: 0 0 0 0.15rem rgba(86, 74, 177, 0.5); } + +.btn-soft-light { + color: #74788d; } + .btn-soft-light:hover, .btn-soft-light:focus, .btn-soft-light:active { + color: #343a40; } + +.btn-outline-light { + color: #343a40; } + .btn-outline-light:hover, .btn-outline-light:focus, .btn-outline-light:active { + color: #343a40; } + +.btn-primary.dropdown-toggle-split { + background-color: #039ef5; + border: none; } + +.btn-secondary.dropdown-toggle-split { + background-color: #828698; + border: none; } + +.btn-success.dropdown-toggle-split { + background-color: #65d799; + border: none; } + +.btn-info.dropdown-toggle-split { + background-color: #77d7f5; + border: none; } + +.btn-warning.dropdown-toggle-split { + background-color: #f8d36b; + border: none; } + +.btn-danger.dropdown-toggle-split { + background-color: #f56666; + border: none; } + +.btn-pink.dropdown-toggle-split { + background-color: #eb559a; + border: none; } + +.btn-light.dropdown-toggle-split { + background-color: white; + border: none; } + +.btn-dark.dropdown-toggle-split { + background-color: #3f474e; + border: none; } + +.btn-purple.dropdown-toggle-split { + background-color: #665aba; + border: none; } + +.btn-light.dropdown-toggle-split { + background-color: #eceef2; } + +.btn-rounded { + border-radius: 30px; } + +body[data-layout-mode="dark"] .btn-light { + color: #eff0f2; + background-color: #043a5a; + border-color: #043a5a !important; } + +body[data-layout-mode="dark"] .btn-outline-light { + color: #adb5bd; + border-color: #043a5a; } + body[data-layout-mode="dark"] .btn-outline-light:hover { + color: #adb5bd; + background-color: #043a5a; + border-color: #043a5a; } + +body[data-layout-mode="dark"] .btn-check:focus + .btn-light, +body[data-layout-mode="dark"] .btn-light:focus, +body[data-layout-mode="dark"] .btn-check:focus + .btn-dark, +body[data-layout-mode="dark"] .btn-dark:focus, +body[data-layout-mode="dark"] .btn-check:focus + .btn-outline-light, +body[data-layout-mode="dark"] .btn-outline-light:focus { + -webkit-box-shadow: 0 0 0 0.15rem rgba(4, 58, 90, 0.5); + box-shadow: 0 0 0 0.15rem rgba(4, 58, 90, 0.5); } + +body[data-layout-mode="dark"] .btn-soft-dark, body[data-layout-mode="dark"] .btn-outline-dark { + color: #eff0f2; } + +body[data-layout-mode="dark"] .btn-dark { + background-color: #033350; + border-color: #033350; } + +body[data-layout-mode="dark"] .btn-outline-dark { + border-color: #033350; } + body[data-layout-mode="dark"] .btn-outline-dark:hover { + background-color: #033350; + border-color: #033350; } + +body[data-layout-mode="dark"] .btn-soft-dark { + background-color: rgba(3, 45, 70, 0.25); + border-color: rgba(3, 45, 70, 0.25); } + body[data-layout-mode="dark"] .btn-soft-dark:hover { + background-color: #033350; + border-color: #033350; } + +.breadcrumb-item > a { + color: #495057; } + +.breadcrumb-item + .breadcrumb-item::before { + font-family: "Material Design Icons"; } + +body[data-layout-mode="dark"] .breadcrumb-item > a { + color: #adb5bd; } + +body[data-layout-mode="dark"] .breadcrumb-item.active { + color: #7e93a0; } + +.card { + margin-bottom: 20px; + -webkit-box-shadow: 0 2px 3px #eaedf2; + box-shadow: 0 2px 3px #eaedf2; } + +.card-drop { + color: #495057; } + +.card-title { + font-size: 16px; + margin-bottom: 0; } + +.card-title-desc { + color: #74788d; + margin-bottom: 24px; } + +.card-h-100 { + height: calc(100% - 20px); } + +.card-header.bg-primary { + background-color: #039ef5 !important; + border-bottom: none; } + +.card-header.bg-secondary { + background-color: #828698 !important; + border-bottom: none; } + +.card-header.bg-success { + background-color: #65d799 !important; + border-bottom: none; } + +.card-header.bg-info { + background-color: #77d7f5 !important; + border-bottom: none; } + +.card-header.bg-warning { + background-color: #f8d36b !important; + border-bottom: none; } + +.card-header.bg-danger { + background-color: #f56666 !important; + border-bottom: none; } + +.card-header.bg-pink { + background-color: #eb559a !important; + border-bottom: none; } + +.card-header.bg-light { + background-color: white !important; + border-bottom: none; } + +.card-header.bg-dark { + background-color: #3f474e !important; + border-bottom: none; } + +.card-header.bg-purple { + background-color: #665aba !important; + border-bottom: none; } + +body[data-layout-mode="dark"] .card { + -webkit-box-shadow: 0 2px 3px #022032; + box-shadow: 0 2px 3px #022032; } + +body[data-layout-mode="dark"] .card, body[data-layout-mode="dark"] .card-header, body[data-layout-mode="dark"] .card-footer, +body[data-layout-mode="dark"] .modal-content, body[data-layout-mode="dark"] .offcanvas { + background-color: #032d46; + border-color: #043a5a; } + +body[data-layout-mode="dark"] .card-title-desc { + color: #7e93a0; } + +.carousel-control-prev, +.carousel-control-next { + height: 30px; + width: 30px; + margin: auto 0; + background-color: #2e86de; } + +.carousel-dark .carousel-caption { + color: rgba(0, 0, 0, 0.8); } + .carousel-dark .carousel-caption .h1, .carousel-dark .carousel-caption .h2, .carousel-dark .carousel-caption .h3, .carousel-dark .carousel-caption .h4, .carousel-dark .carousel-caption .h5, .carousel-dark .carousel-caption .h6, + .carousel-dark .carousel-caption h1, + .carousel-dark .carousel-caption .h1, .carousel-dark .carousel-caption h2, .carousel-dark .carousel-caption .h2, .carousel-dark .carousel-caption h3, .carousel-dark .carousel-caption .h3, .carousel-dark .carousel-caption h4, .carousel-dark .carousel-caption .h4, .carousel-dark .carousel-caption h5, .carousel-dark .carousel-caption .h5, .carousel-dark .carousel-caption h6, .carousel-dark .carousel-caption .h6 { + color: rgba(0, 0, 0, 0.8); } + +.dropdown-menu { + -webkit-box-shadow: 0 5px 6px rgba(52, 58, 64, 0.1); + box-shadow: 0 5px 6px rgba(52, 58, 64, 0.1); + -webkit-animation-name: DropDownSlide; + animation-name: DropDownSlide; + -webkit-animation-duration: .3s; + animation-duration: .3s; + -webkit-animation-fill-mode: both; + animation-fill-mode: both; + position: absolute; + z-index: 1000; } + .dropdown-menu.dropdown-megamenu { + padding: 20px; + left: 0 !important; + right: 0 !important; } + .dropdown-menu[data-popper-placement="top-start"] { + -webkit-animation-name: DropDownSlideDown; + animation-name: DropDownSlideDown; } + +@-webkit-keyframes DropDownSlide { + 100% { + margin-top: -1px; } + 0% { + margin-top: 8px; } } + +@keyframes DropDownSlide { + 100% { + margin-top: -1px; } + 0% { + margin-top: 8px; } } + +@-webkit-keyframes DropDownSlideDown { + 100% { + margin-bottom: 0; } + 0% { + margin-bottom: 8px; } } + +@keyframes DropDownSlideDown { + 100% { + margin-bottom: 0; } + 0% { + margin-bottom: 8px; } } + +@media (min-width: 600px) { + .dropdown-menu-xl { + width: 420px; } + .dropdown-menu-lg { + width: 320px; } + .dropdown-menu-md { + width: 240px; } } + +.dropdown-divider { + border-top-color: #eff0f2; } + +.dropdown-mega { + position: static !important; } + +.dropdown-mega-menu-xl { + width: 38rem; } + +.dropdown-mega-menu-lg { + width: 26rem; } + +[dir="ltr"] .dropdown-menu-start { + --bs-position: end; } + +[dir="ltr"] .dropdown-menu-end { + --bs-position: start; } + +body[data-layout-mode="dark"] .dropdown-menu { + background-color: #043552; + border-color: #043a5a; + color: #adb5bd; + -webkit-box-shadow: 0 5px 6px rgba(3, 45, 70, 0.1); + box-shadow: 0 5px 6px rgba(3, 45, 70, 0.1); } + +body[data-layout-mode="dark"] .dropdown-item { + color: #adb5bd; } + body[data-layout-mode="dark"] .dropdown-item:hover, body[data-layout-mode="dark"] .dropdown-item:active, body[data-layout-mode="dark"] .dropdown-item:focus { + background-color: #043d5e; } + body[data-layout-mode="dark"] .dropdown-item.active, body[data-layout-mode="dark"] .dropdown-item:active { + background-color: #043d5e; } + +body[data-layout-mode="dark"] .dropdown-divider { + border-top-color: #043a5a; } + +.nav-tabs > li > a, .nav-pills > li > a { + color: #495057; + font-weight: 500; } + +.nav-pills > a { + color: #495057; + font-weight: 500; } + +.nav-tabs-custom { + border-bottom: 2px solid #eff0f2; } + .nav-tabs-custom .nav-item { + position: relative; + color: #343a40; } + .nav-tabs-custom .nav-item .nav-link { + border: none; } + .nav-tabs-custom .nav-item .nav-link::after { + content: ""; + background: #2e86de; + height: 2px; + position: absolute; + width: 100%; + left: 0; + bottom: -2px; + -webkit-transition: all 250ms ease 0s; + transition: all 250ms ease 0s; + -webkit-transform: scale(0); + transform: scale(0); } + .nav-tabs-custom .nav-item .nav-link.active { + color: #2e86de; } + .nav-tabs-custom .nav-item .nav-link.active:after { + -webkit-transform: scale(1); + transform: scale(1); } + +body[data-layout-mode="dark"] .nav-link { + color: #adb5bd; } + +body[data-layout-mode="dark"] .nav-tabs { + border-color: #043a5a; } + body[data-layout-mode="dark"] .nav-tabs .nav-link { + color: #eff0f2; } + body[data-layout-mode="dark"] .nav-tabs .nav-link:focus, body[data-layout-mode="dark"] .nav-tabs .nav-link:hover { + border-color: #043a5a #043a5a #043a5a; } + body[data-layout-mode="dark"] .nav-tabs .nav-link.active { + background-color: #032d46; + border-color: #043a5a #043a5a #032d46; } + +body[data-layout-mode="dark"] .nav-pills .nav-link { + color: #eff0f2; } + body[data-layout-mode="dark"] .nav-pills .nav-link.active { + color: #fff; } + +.table th { + font-weight: 500; } + +.table .table-light { + color: #495057; + border-color: #eff0f2; + background-color: #f8f9fa; } + +.table-nowrap th, +.table-nowrap td { + white-space: nowrap; } + +.table-responsive::-webkit-scrollbar { + -webkit-appearance: none; } + +.table-responsive::-webkit-scrollbar:vertical { + width: 12px; } + +.table-responsive::-webkit-scrollbar:horizontal { + height: 9px; } + +.table-responsive::-webkit-scrollbar-thumb { + background-color: rgba(52, 58, 64, 0.2); + border-radius: 10px; + border: 2px solid #fff; } + +.table-responsive::-webkit-scrollbar-track { + border-radius: 10px; + background-color: #fff; } + +body[data-layout-mode="dark"] .table { + border-color: #043d5e; + color: #7e93a0; } + +body[data-layout-mode="dark"] .table-bordered { + border-color: #043a5a; } + body[data-layout-mode="dark"] .table-bordered th, + body[data-layout-mode="dark"] .table-bordered td { + border-color: #043a5a; } + +body[data-layout-mode="dark"] .table > :not(:last-child) > :last-child > * { + border-bottom-color: #043a5a; } + +body[data-layout-mode="dark"] .table-striped > tbody > tr:nth-of-type(odd), +body[data-layout-mode="dark"] .table-hover > tbody > tr:hover, +body[data-layout-mode="dark"] .table .table-light { + --bs-table-accent-bg: #03304b; + color: #7e93a0; } + +body[data-layout-mode="dark"] .table-dark { + background-color: #043a5a; } + body[data-layout-mode="dark"] .table-dark > :not(caption) > * > * { + background-color: #043a5a; } + +body[data-layout-mode="dark"] .table-active { + background-color: #033350 !important; + color: #7e93a0; } + body[data-layout-mode="dark"] .table-active th, body[data-layout-mode="dark"] .table-active td { + background-color: #033350; } + +body[data-layout-mode="dark"] .table-responsive::-webkit-scrollbar-thumb { + background-color: rgba(255, 255, 255, 0.075); + border: 2px solid #032d46; } + +body[data-layout-mode="dark"] .table-responsive::-webkit-scrollbar-track { + background-color: #032d46; } + +.pagination-rounded .page-link { + border-radius: 30px !important; + margin: 0 5px; + border: none; + width: 32px; + height: 32px; + padding: 0; + text-align: center; + line-height: 32px; } + +body[data-layout-mode="dark"] .page-link { + background-color: #032d46; + border-color: #043a5a; + color: #adb5bd; } + body[data-layout-mode="dark"] .page-link:hover { + background-color: #033350; + color: #2e86de; } + +body[data-layout-mode="dark"] .page-item.disabled .page-link { + color: #7e93a0; + background-color: #032d46; + border-color: #043a5a; } + +body[data-layout-mode="dark"] .page-item.active .page-link { + color: #fff; + background-color: #2e86de; + border-color: #2e86de; } + +.progress-sm { + height: 5px; } + +.progress-md { + height: 8px; } + +.progress-lg { + height: 12px; } + +.progress-xl { + height: 16px; } + +.animated-progess { + position: relative; } + .animated-progess .progress-bar { + position: relative; + border-radius: 30px; + -webkit-animation: animate-positive 2s; + animation: animate-positive 2s; } + +@-webkit-keyframes animate-positive { + 0% { + width: 0; } } + +@keyframes animate-positive { + 0% { + width: 0; } } + +.custom-progress { + height: 15px; + padding: 4px; + border-radius: 30px; } + .custom-progress .progress-bar { + position: relative; + border-radius: 30px; } + .custom-progress .progress-bar::before { + content: ""; + position: absolute; + width: 4px; + height: 4px; + background-color: #fff; + border-radius: 7px; + right: 2px; + top: 50%; + -webkit-transform: translateY(-50%); + transform: translateY(-50%); } + +body[data-layout-mode="dark"] .progress { + background-color: #043a5a; } + +.popover { + -webkit-box-shadow: 0 5px 6px rgba(52, 58, 64, 0.1); + box-shadow: 0 5px 6px rgba(52, 58, 64, 0.1); } diff --git a/api/install/assets/custom.js b/api/install/assets/custom.js new file mode 100644 index 0000000..3d80531 --- /dev/null +++ b/api/install/assets/custom.js @@ -0,0 +1,43 @@ + +$("#checkDB").click(function() { + var dbloca = $('#dblocal').val(); + var dbuser = $('#dbuser').val(); + var dbpass = $('#dbpass').val(); + var dbname = $('#dbname').val(); + +// $(this).parents('.AccessoryItem').fadeOut('fast'); + + $.ajax({ + cache: false, + type: 'POST', + url: '/install/index/checkdb', + data: { + "dbloca" : dbloca, + "dbuser" : dbuser, + "dbpass" : dbpass, + "dbname" : dbname, + + }, + success: function(data) + { + + if(data == 1) { + $("#dbstatus").slideDown("fast", function () { + $('#dbstatus').html('Database Connection Successful.'); + $('#dbstatus').removeClass('alert-danger').addClass('alert-success'); + }); + + } else { + $("#dbstatus").slideDown("fast", function () { + $('#dbstatus').html('Failed to Connect to Database. Check your settings and try again.'); + $('#dbstatus').removeClass('alert-success').addClass('alert-danger'); + }); + + } + } + }); + return false; +}); + + + diff --git a/api/install/assets/jquery.min.js b/api/install/assets/jquery.min.js new file mode 100644 index 0000000..409c3f4 --- /dev/null +++ b/api/install/assets/jquery.min.js @@ -0,0 +1,2 @@ +/*! jQuery v3.4.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(C,e){"use strict";var t=[],E=C.document,r=Object.getPrototypeOf,s=t.slice,g=t.concat,u=t.push,i=t.indexOf,n={},o=n.toString,v=n.hasOwnProperty,a=v.toString,l=a.call(Object),y={},m=function(e){return"function"==typeof e&&"number"!=typeof e.nodeType},x=function(e){return null!=e&&e===e.window},c={type:!0,src:!0,nonce:!0,noModule:!0};function b(e,t,n){var r,i,o=(n=n||E).createElement("script");if(o.text=e,t)for(r in c)(i=t[r]||t.getAttribute&&t.getAttribute(r))&&o.setAttribute(r,i);n.head.appendChild(o).parentNode.removeChild(o)}function w(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?n[o.call(e)]||"object":typeof e}var f="3.4.1",k=function(e,t){return new k.fn.init(e,t)},p=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g;function d(e){var t=!!e&&"length"in e&&e.length,n=w(e);return!m(e)&&!x(e)&&("array"===n||0===t||"number"==typeof t&&0+~]|"+M+")"+M+"*"),U=new RegExp(M+"|>"),X=new RegExp($),V=new RegExp("^"+I+"$"),G={ID:new RegExp("^#("+I+")"),CLASS:new RegExp("^\\.("+I+")"),TAG:new RegExp("^("+I+"|[*])"),ATTR:new RegExp("^"+W),PSEUDO:new RegExp("^"+$),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("^(?:"+R+")$","i"),needsContext:new RegExp("^"+M+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+M+"*((?:-\\d)?\\d*)"+M+"*\\)|)(?=[^-]|$)","i")},Y=/HTML$/i,Q=/^(?:input|select|textarea|button)$/i,J=/^h\d$/i,K=/^[^{]+\{\s*\[native \w/,Z=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,ee=/[+~]/,te=new RegExp("\\\\([\\da-f]{1,6}"+M+"?|("+M+")|.)","ig"),ne=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)},re=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,ie=function(e,t){return t?"\0"===e?"\ufffd":e.slice(0,-1)+"\\"+e.charCodeAt(e.length-1).toString(16)+" ":"\\"+e},oe=function(){T()},ae=be(function(e){return!0===e.disabled&&"fieldset"===e.nodeName.toLowerCase()},{dir:"parentNode",next:"legend"});try{H.apply(t=O.call(m.childNodes),m.childNodes),t[m.childNodes.length].nodeType}catch(e){H={apply:t.length?function(e,t){L.apply(e,O.call(t))}:function(e,t){var n=e.length,r=0;while(e[n++]=t[r++]);e.length=n-1}}}function se(t,e,n,r){var i,o,a,s,u,l,c,f=e&&e.ownerDocument,p=e?e.nodeType:9;if(n=n||[],"string"!=typeof t||!t||1!==p&&9!==p&&11!==p)return n;if(!r&&((e?e.ownerDocument||e:m)!==C&&T(e),e=e||C,E)){if(11!==p&&(u=Z.exec(t)))if(i=u[1]){if(9===p){if(!(a=e.getElementById(i)))return n;if(a.id===i)return n.push(a),n}else if(f&&(a=f.getElementById(i))&&y(e,a)&&a.id===i)return n.push(a),n}else{if(u[2])return H.apply(n,e.getElementsByTagName(t)),n;if((i=u[3])&&d.getElementsByClassName&&e.getElementsByClassName)return H.apply(n,e.getElementsByClassName(i)),n}if(d.qsa&&!A[t+" "]&&(!v||!v.test(t))&&(1!==p||"object"!==e.nodeName.toLowerCase())){if(c=t,f=e,1===p&&U.test(t)){(s=e.getAttribute("id"))?s=s.replace(re,ie):e.setAttribute("id",s=k),o=(l=h(t)).length;while(o--)l[o]="#"+s+" "+xe(l[o]);c=l.join(","),f=ee.test(t)&&ye(e.parentNode)||e}try{return H.apply(n,f.querySelectorAll(c)),n}catch(e){A(t,!0)}finally{s===k&&e.removeAttribute("id")}}}return g(t.replace(B,"$1"),e,n,r)}function ue(){var r=[];return function e(t,n){return r.push(t+" ")>b.cacheLength&&delete e[r.shift()],e[t+" "]=n}}function le(e){return e[k]=!0,e}function ce(e){var t=C.createElement("fieldset");try{return!!e(t)}catch(e){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function fe(e,t){var n=e.split("|"),r=n.length;while(r--)b.attrHandle[n[r]]=t}function pe(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 de(t){return function(e){return"input"===e.nodeName.toLowerCase()&&e.type===t}}function he(n){return function(e){var t=e.nodeName.toLowerCase();return("input"===t||"button"===t)&&e.type===n}}function ge(t){return function(e){return"form"in e?e.parentNode&&!1===e.disabled?"label"in e?"label"in e.parentNode?e.parentNode.disabled===t:e.disabled===t:e.isDisabled===t||e.isDisabled!==!t&&ae(e)===t:e.disabled===t:"label"in e&&e.disabled===t}}function ve(a){return le(function(o){return o=+o,le(function(e,t){var n,r=a([],e.length,o),i=r.length;while(i--)e[n=r[i]]&&(e[n]=!(t[n]=e[n]))})})}function ye(e){return e&&"undefined"!=typeof e.getElementsByTagName&&e}for(e in d=se.support={},i=se.isXML=function(e){var t=e.namespaceURI,n=(e.ownerDocument||e).documentElement;return!Y.test(t||n&&n.nodeName||"HTML")},T=se.setDocument=function(e){var t,n,r=e?e.ownerDocument||e:m;return r!==C&&9===r.nodeType&&r.documentElement&&(a=(C=r).documentElement,E=!i(C),m!==C&&(n=C.defaultView)&&n.top!==n&&(n.addEventListener?n.addEventListener("unload",oe,!1):n.attachEvent&&n.attachEvent("onunload",oe)),d.attributes=ce(function(e){return e.className="i",!e.getAttribute("className")}),d.getElementsByTagName=ce(function(e){return e.appendChild(C.createComment("")),!e.getElementsByTagName("*").length}),d.getElementsByClassName=K.test(C.getElementsByClassName),d.getById=ce(function(e){return a.appendChild(e).id=k,!C.getElementsByName||!C.getElementsByName(k).length}),d.getById?(b.filter.ID=function(e){var t=e.replace(te,ne);return function(e){return e.getAttribute("id")===t}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n=t.getElementById(e);return n?[n]:[]}}):(b.filter.ID=function(e){var n=e.replace(te,ne);return function(e){var t="undefined"!=typeof e.getAttributeNode&&e.getAttributeNode("id");return t&&t.value===n}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){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[]}}),b.find.TAG=d.getElementsByTagName?function(e,t){return"undefined"!=typeof t.getElementsByTagName?t.getElementsByTagName(e):d.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},b.find.CLASS=d.getElementsByClassName&&function(e,t){if("undefined"!=typeof t.getElementsByClassName&&E)return t.getElementsByClassName(e)},s=[],v=[],(d.qsa=K.test(C.querySelectorAll))&&(ce(function(e){a.appendChild(e).innerHTML="",e.querySelectorAll("[msallowcapture^='']").length&&v.push("[*^$]="+M+"*(?:''|\"\")"),e.querySelectorAll("[selected]").length||v.push("\\["+M+"*(?:value|"+R+")"),e.querySelectorAll("[id~="+k+"-]").length||v.push("~="),e.querySelectorAll(":checked").length||v.push(":checked"),e.querySelectorAll("a#"+k+"+*").length||v.push(".#.+[+~]")}),ce(function(e){e.innerHTML="";var t=C.createElement("input");t.setAttribute("type","hidden"),e.appendChild(t).setAttribute("name","D"),e.querySelectorAll("[name=d]").length&&v.push("name"+M+"*[*^$|!~]?="),2!==e.querySelectorAll(":enabled").length&&v.push(":enabled",":disabled"),a.appendChild(e).disabled=!0,2!==e.querySelectorAll(":disabled").length&&v.push(":enabled",":disabled"),e.querySelectorAll("*,:x"),v.push(",.*:")})),(d.matchesSelector=K.test(c=a.matches||a.webkitMatchesSelector||a.mozMatchesSelector||a.oMatchesSelector||a.msMatchesSelector))&&ce(function(e){d.disconnectedMatch=c.call(e,"*"),c.call(e,"[s!='']:x"),s.push("!=",$)}),v=v.length&&new RegExp(v.join("|")),s=s.length&&new RegExp(s.join("|")),t=K.test(a.compareDocumentPosition),y=t||K.test(a.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 l=!0,0;var n=!e.compareDocumentPosition-!t.compareDocumentPosition;return n||(1&(n=(e.ownerDocument||e)===(t.ownerDocument||t)?e.compareDocumentPosition(t):1)||!d.sortDetached&&t.compareDocumentPosition(e)===n?e===C||e.ownerDocument===m&&y(m,e)?-1:t===C||t.ownerDocument===m&&y(m,t)?1:u?P(u,e)-P(u,t):0:4&n?-1:1)}:function(e,t){if(e===t)return l=!0,0;var n,r=0,i=e.parentNode,o=t.parentNode,a=[e],s=[t];if(!i||!o)return e===C?-1:t===C?1:i?-1:o?1:u?P(u,e)-P(u,t):0;if(i===o)return pe(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?pe(a[r],s[r]):a[r]===m?-1:s[r]===m?1:0}),C},se.matches=function(e,t){return se(e,null,null,t)},se.matchesSelector=function(e,t){if((e.ownerDocument||e)!==C&&T(e),d.matchesSelector&&E&&!A[t+" "]&&(!s||!s.test(t))&&(!v||!v.test(t)))try{var n=c.call(e,t);if(n||d.disconnectedMatch||e.document&&11!==e.document.nodeType)return n}catch(e){A(t,!0)}return 0":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(te,ne),e[3]=(e[3]||e[4]||e[5]||"").replace(te,ne),"~="===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]||se.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]&&se.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return G.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"":n&&X.test(n)&&(t=h(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(te,ne).toLowerCase();return"*"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=p[e+" "];return t||(t=new RegExp("(^|"+M+")"+e+"("+M+"|$)"))&&p(e,function(e){return t.test("string"==typeof e.className&&e.className||"undefined"!=typeof e.getAttribute&&e.getAttribute("class")||"")})},ATTR:function(n,r,i){return function(e){var t=se.attr(e,n);return null==t?"!="===r:!r||(t+="","="===r?t===i:"!="===r?t!==i:"^="===r?i&&0===t.indexOf(i):"*="===r?i&&-1:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i;function j(e,n,r){return m(n)?k.grep(e,function(e,t){return!!n.call(e,t,e)!==r}):n.nodeType?k.grep(e,function(e){return e===n!==r}):"string"!=typeof n?k.grep(e,function(e){return-1)[^>]*|#([\w-]+))$/;(k.fn.init=function(e,t,n){var r,i;if(!e)return this;if(n=n||q,"string"==typeof e){if(!(r="<"===e[0]&&">"===e[e.length-1]&&3<=e.length?[null,e,null]:L.exec(e))||!r[1]&&t)return!t||t.jquery?(t||n).find(e):this.constructor(t).find(e);if(r[1]){if(t=t instanceof k?t[0]:t,k.merge(this,k.parseHTML(r[1],t&&t.nodeType?t.ownerDocument||t:E,!0)),D.test(r[1])&&k.isPlainObject(t))for(r in t)m(this[r])?this[r](t[r]):this.attr(r,t[r]);return this}return(i=E.getElementById(r[2]))&&(this[0]=i,this.length=1),this}return e.nodeType?(this[0]=e,this.length=1,this):m(e)?void 0!==n.ready?n.ready(e):e(k):k.makeArray(e,this)}).prototype=k.fn,q=k(E);var H=/^(?:parents|prev(?:Until|All))/,O={children:!0,contents:!0,next:!0,prev:!0};function P(e,t){while((e=e[t])&&1!==e.nodeType);return e}k.fn.extend({has:function(e){var t=k(e,this),n=t.length;return this.filter(function(){for(var e=0;e\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,"",""]};function ve(e,t){var n;return n="undefined"!=typeof e.getElementsByTagName?e.getElementsByTagName(t||"*"):"undefined"!=typeof e.querySelectorAll?e.querySelectorAll(t||"*"):[],void 0===t||t&&A(e,t)?k.merge([e],n):n}function ye(e,t){for(var n=0,r=e.length;nx",y.noCloneChecked=!!me.cloneNode(!0).lastChild.defaultValue;var Te=/^key/,Ce=/^(?:mouse|pointer|contextmenu|drag|drop)|click/,Ee=/^([^.]*)(?:\.(.+)|)/;function ke(){return!0}function Se(){return!1}function Ne(e,t){return e===function(){try{return E.activeElement}catch(e){}}()==("focus"===t)}function Ae(e,t,n,r,i,o){var a,s;if("object"==typeof t){for(s in"string"!=typeof n&&(r=r||n,n=void 0),t)Ae(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=Se;else if(!i)return e;return 1===o&&(a=i,(i=function(e){return k().off(e),a.apply(this,arguments)}).guid=a.guid||(a.guid=k.guid++)),e.each(function(){k.event.add(this,t,i,r,n)})}function De(e,i,o){o?(Q.set(e,i,!1),k.event.add(e,i,{namespace:!1,handler:function(e){var t,n,r=Q.get(this,i);if(1&e.isTrigger&&this[i]){if(r.length)(k.event.special[i]||{}).delegateType&&e.stopPropagation();else if(r=s.call(arguments),Q.set(this,i,r),t=o(this,i),this[i](),r!==(n=Q.get(this,i))||t?Q.set(this,i,!1):n={},r!==n)return e.stopImmediatePropagation(),e.preventDefault(),n.value}else r.length&&(Q.set(this,i,{value:k.event.trigger(k.extend(r[0],k.Event.prototype),r.slice(1),this)}),e.stopImmediatePropagation())}})):void 0===Q.get(e,i)&&k.event.add(e,i,ke)}k.event={global:{},add:function(t,e,n,r,i){var o,a,s,u,l,c,f,p,d,h,g,v=Q.get(t);if(v){n.handler&&(n=(o=n).handler,i=o.selector),i&&k.find.matchesSelector(ie,i),n.guid||(n.guid=k.guid++),(u=v.events)||(u=v.events={}),(a=v.handle)||(a=v.handle=function(e){return"undefined"!=typeof k&&k.event.triggered!==e.type?k.event.dispatch.apply(t,arguments):void 0}),l=(e=(e||"").match(R)||[""]).length;while(l--)d=g=(s=Ee.exec(e[l])||[])[1],h=(s[2]||"").split(".").sort(),d&&(f=k.event.special[d]||{},d=(i?f.delegateType:f.bindType)||d,f=k.event.special[d]||{},c=k.extend({type:d,origType:g,data:r,handler:n,guid:n.guid,selector:i,needsContext:i&&k.expr.match.needsContext.test(i),namespace:h.join(".")},o),(p=u[d])||((p=u[d]=[]).delegateCount=0,f.setup&&!1!==f.setup.call(t,r,h,a)||t.addEventListener&&t.addEventListener(d,a)),f.add&&(f.add.call(t,c),c.handler.guid||(c.handler.guid=n.guid)),i?p.splice(p.delegateCount++,0,c):p.push(c),k.event.global[d]=!0)}},remove:function(e,t,n,r,i){var o,a,s,u,l,c,f,p,d,h,g,v=Q.hasData(e)&&Q.get(e);if(v&&(u=v.events)){l=(t=(t||"").match(R)||[""]).length;while(l--)if(d=g=(s=Ee.exec(t[l])||[])[1],h=(s[2]||"").split(".").sort(),d){f=k.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,v.handle)||k.removeEvent(e,d,v.handle),delete u[d])}else for(d in u)k.event.remove(e,d+t[l],n,r,!0);k.isEmptyObject(u)&&Q.remove(e,"handle events")}},dispatch:function(e){var t,n,r,i,o,a,s=k.event.fix(e),u=new Array(arguments.length),l=(Q.get(this,"events")||{})[s.type]||[],c=k.event.special[s.type]||{};for(u[0]=s,t=1;t\x20\t\r\n\f]*)[^>]*)\/>/gi,qe=/\s*$/g;function Oe(e,t){return A(e,"table")&&A(11!==t.nodeType?t:t.firstChild,"tr")&&k(e).children("tbody")[0]||e}function Pe(e){return e.type=(null!==e.getAttribute("type"))+"/"+e.type,e}function Re(e){return"true/"===(e.type||"").slice(0,5)?e.type=e.type.slice(5):e.removeAttribute("type"),e}function Me(e,t){var n,r,i,o,a,s,u,l;if(1===t.nodeType){if(Q.hasData(e)&&(o=Q.access(e),a=Q.set(t,o),l=o.events))for(i in delete a.handle,a.events={},l)for(n=0,r=l[i].length;n")},clone:function(e,t,n){var r,i,o,a,s,u,l,c=e.cloneNode(!0),f=oe(e);if(!(y.noCloneChecked||1!==e.nodeType&&11!==e.nodeType||k.isXMLDoc(e)))for(a=ve(c),r=0,i=(o=ve(e)).length;r").attr(n.scriptAttrs||{}).prop({charset:n.scriptCharset,src:n.url}).on("load error",i=function(e){r.remove(),i=null,e&&t("error"===e.type?404:200,e.type)}),E.head.appendChild(r[0])},abort:function(){i&&i()}}});var Vt,Gt=[],Yt=/(=)\?(?=&|$)|\?\?/;k.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var e=Gt.pop()||k.expando+"_"+kt++;return this[e]=!0,e}}),k.ajaxPrefilter("json jsonp",function(e,t,n){var r,i,o,a=!1!==e.jsonp&&(Yt.test(e.url)?"url":"string"==typeof e.data&&0===(e.contentType||"").indexOf("application/x-www-form-urlencoded")&&Yt.test(e.data)&&"data");if(a||"jsonp"===e.dataTypes[0])return r=e.jsonpCallback=m(e.jsonpCallback)?e.jsonpCallback():e.jsonpCallback,a?e[a]=e[a].replace(Yt,"$1"+r):!1!==e.jsonp&&(e.url+=(St.test(e.url)?"&":"?")+e.jsonp+"="+r),e.converters["script json"]=function(){return o||k.error(r+" was not called"),o[0]},e.dataTypes[0]="json",i=C[r],C[r]=function(){o=arguments},n.always(function(){void 0===i?k(C).removeProp(r):C[r]=i,e[r]&&(e.jsonpCallback=t.jsonpCallback,Gt.push(r)),o&&m(i)&&i(o[0]),o=i=void 0}),"script"}),y.createHTMLDocument=((Vt=E.implementation.createHTMLDocument("").body).innerHTML="
",2===Vt.childNodes.length),k.parseHTML=function(e,t,n){return"string"!=typeof e?[]:("boolean"==typeof t&&(n=t,t=!1),t||(y.createHTMLDocument?((r=(t=E.implementation.createHTMLDocument("")).createElement("base")).href=E.location.href,t.head.appendChild(r)):t=E),o=!n&&[],(i=D.exec(e))?[t.createElement(i[1])]:(i=we([e],t,o),o&&o.length&&k(o).remove(),k.merge([],i.childNodes)));var r,i,o},k.fn.load=function(e,t,n){var r,i,o,a=this,s=e.indexOf(" ");return-1").append(k.parseHTML(e)).find(r):e)}).always(n&&function(e,t){a.each(function(){n.apply(this,o||[e.responseText,t,e])})}),this},k.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(e,t){k.fn[t]=function(e){return this.on(t,e)}}),k.expr.pseudos.animated=function(t){return k.grep(k.timers,function(e){return t===e.elem}).length},k.offset={setOffset:function(e,t,n){var r,i,o,a,s,u,l=k.css(e,"position"),c=k(e),f={};"static"===l&&(e.style.position="relative"),s=c.offset(),o=k.css(e,"top"),u=k.css(e,"left"),("absolute"===l||"fixed"===l)&&-1<(o+u).indexOf("auto")?(a=(r=c.position()).top,i=r.left):(a=parseFloat(o)||0,i=parseFloat(u)||0),m(t)&&(t=t.call(e,n,k.extend({},s))),null!=t.top&&(f.top=t.top-s.top+a),null!=t.left&&(f.left=t.left-s.left+i),"using"in t?t.using.call(e,f):c.css(f)}},k.fn.extend({offset:function(t){if(arguments.length)return void 0===t?this:this.each(function(e){k.offset.setOffset(this,t,e)});var e,n,r=this[0];return r?r.getClientRects().length?(e=r.getBoundingClientRect(),n=r.ownerDocument.defaultView,{top:e.top+n.pageYOffset,left:e.left+n.pageXOffset}):{top:0,left:0}:void 0},position:function(){if(this[0]){var e,t,n,r=this[0],i={top:0,left:0};if("fixed"===k.css(r,"position"))t=r.getBoundingClientRect();else{t=this.offset(),n=r.ownerDocument,e=r.offsetParent||n.documentElement;while(e&&(e===n.body||e===n.documentElement)&&"static"===k.css(e,"position"))e=e.parentNode;e&&e!==r&&1===e.nodeType&&((i=k(e).offset()).top+=k.css(e,"borderTopWidth",!0),i.left+=k.css(e,"borderLeftWidth",!0))}return{top:t.top-i.top-k.css(r,"marginTop",!0),left:t.left-i.left-k.css(r,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){var e=this.offsetParent;while(e&&"static"===k.css(e,"position"))e=e.offsetParent;return e||ie})}}),k.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(t,i){var o="pageYOffset"===i;k.fn[t]=function(e){return _(this,function(e,t,n){var r;if(x(e)?r=e:9===e.nodeType&&(r=e.defaultView),void 0===n)return r?r[i]:e[t];r?r.scrollTo(o?r.pageXOffset:n,o?n:r.pageYOffset):e[t]=n},t,e,arguments.length)}}),k.each(["top","left"],function(e,n){k.cssHooks[n]=ze(y.pixelPosition,function(e,t){if(t)return t=_e(e,n),$e.test(t)?k(e).position()[n]+"px":t})}),k.each({Height:"height",Width:"width"},function(a,s){k.each({padding:"inner"+a,content:s,"":"outer"+a},function(r,o){k.fn[o]=function(e,t){var n=arguments.length&&(r||"boolean"!=typeof e),i=r||(!0===e||!0===t?"margin":"border");return _(this,function(e,t,n){var r;return x(e)?0===o.indexOf("outer")?e["inner"+a]:e.document.documentElement["client"+a]:9===e.nodeType?(r=e.documentElement,Math.max(e.body["scroll"+a],r["scroll"+a],e.body["offset"+a],r["offset"+a],r["client"+a])):void 0===n?k.css(e,t,i):k.style(e,t,n,i)},s,n?e:void 0,n)}})}),k.each("blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu".split(" "),function(e,n){k.fn[n]=function(e,t){return 0Styles['commenta'] = ''; + $this->view->Styles = $this->Styles; + + $this->JavaScript[] = ASSETS . "/js/pages/crypto-dashboard.init.js"; + $this->view->JavaScript = $this->JavaScript; + + + $this->view->title = COMPANY; + $this->view->render(__CLASS__ .'/'. __FUNCTION__); + } + + +} // end class \ No newline at end of file diff --git a/api/install/controllers/i.php b/api/install/controllers/i.php new file mode 100644 index 0000000..41184f1 --- /dev/null +++ b/api/install/controllers/i.php @@ -0,0 +1,35 @@ +Styles['commenta'] = ''; + $this->view->Styles = $this->Styles; + + $this->JavaScript[] = "/install/assets/jquery.min.js"; + $this->JavaScript[] = "/install/assets/custom.js"; + $this->view->JavaScript = $this->JavaScript; + + } + + function index() { + die(); + } + + function requirements(){ + $this->view->render('index/requirements'); + } + + function setup(){ + $this->view->render('index/setup'); + } + + function complete(){ + $this->view->render('index/complete'); + } + + + +} \ No newline at end of file diff --git a/api/install/controllers/index.php b/api/install/controllers/index.php new file mode 100644 index 0000000..d3898bb --- /dev/null +++ b/api/install/controllers/index.php @@ -0,0 +1,139 @@ +view->render(__CLASS__ .'/'. __FUNCTION__); + } + + function checkDB(){ + // \Helper::print_array($_POST); + + $host = $_POST['dbloca']; + $user = $_POST['dbuser']; + $pass = $_POST['dbpass']; + $db = $_POST['dbname']; + $charset = 'utf8'; + + + $dsn = "mysql:host=$host;dbname=$db;charset=$charset"; + $options = [ + PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, + PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_LAZY, + PDO::ATTR_EMULATE_PREPARES => false, + ]; + + try { + $pdo = new PDO($dsn, $user, $pass, $options); + echo 1; + } catch (\PDOException $e) { + echo 0; + } + } + + function installation() { + // \Helper::print_array($_POST); + if(!$_POST) { + header("Location: /install/"); + die(); + }; + + $dbLoca = $_POST['dbloca']; + $dbName = $_POST['dbname']; + $dbUser = $_POST['dbuser']; + $dbPass = $_POST['dbpass']; + + + # Step 1: building out database structure + $SQLFile = $_SERVER['DOCUMENT_ROOT'] . '/install/dump.sql'; + $ConfigFile = $_SERVER['DOCUMENT_ROOT'] . '/config.php'; + + + $HashKey = $this->getName(50); + $HashAPIKey = $this->getName(50); + + + + if(!file_exists($SQLFile) ){ + die('Error: Failed to Load SQL Dump File. '); + } + + $sql = file_get_contents($SQLFile); + $mysqli = new mysqli($dbLoca, $dbUser, $dbPass, $dbName); + + /* check connection */ + if ($mysqli->connect_errno) { + printf("Connect failed: %s\n", $mysqli->connect_error); + exit(); + } + + if (!$mysqli->multi_query($sql)) { + printf("Error message: %s\n", $mysqli->error); + }; + + /* close connection */ + $mysqli->close(); + + # Step 2: Create the Config file. + // if(file_exists($ConfigFile)) { die ('File Currently Exist. Please Delete config.php; if you are trying to do a new install.'); } + + $myfile = fopen($ConfigFile, "w") or die("Unable to Write or Open file, Please check your permissions!"); + fwrite($myfile, ''); + fclose($myfile); + + + $config_content = + <<setDefaultPath(__DIR__); +$bootstrap->init(); + diff --git a/api/install/views/_error/index.php b/api/install/views/_error/index.php new file mode 100644 index 0000000..8a5fbab --- /dev/null +++ b/api/install/views/_error/index.php @@ -0,0 +1,15 @@ + +
+

404

+

Page Not Found

+ +
+ Sorry, but the page you are looking for has note been found. Try checking the URL for error, then hit the refresh button on your browser or try found something else in our app. +
+
+ +
+ +
+
+
\ No newline at end of file diff --git a/api/install/views/index/complete.php b/api/install/views/index/complete.php new file mode 100644 index 0000000..8e12cc2 --- /dev/null +++ b/api/install/views/index/complete.php @@ -0,0 +1,32 @@ +
+ +
+
+

Installation Complete!

+ +

Congratulations! The installation has been successful!

+ +

You can now login with the following information:

+ + + + + + + + + + + + + + + + +
URL
Usernameadmin
Passwordadmin
+ +
+
+
+ + diff --git a/api/install/views/index/index.php b/api/install/views/index/index.php new file mode 100644 index 0000000..9eacbce --- /dev/null +++ b/api/install/views/index/index.php @@ -0,0 +1,19 @@ + +
+ +
+
+
+

Installation

+

Something short and leading about the collection below—its contents, the creator, etc. Make it short and sweet, but not too short so folks don’t simply skip over it entirely.

+

+ Start Installation +

+
+
+
+ + +
+ + diff --git a/api/install/views/index/requirements.php b/api/install/views/index/requirements.php new file mode 100644 index 0000000..2f4926a --- /dev/null +++ b/api/install/views/index/requirements.php @@ -0,0 +1,150 @@ +
+ +
+
+

Requirements

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PrerequisitesRequiredCurrent
PHP Version7.4+ + = 0): ?> + + + + + + +
cURLEnabled + + + + + + + +
OpenSSLEnabled + + + + + + + +
mbstringEnabled + + + + + + + +
PDOEnabled + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + +
Path / FileStatus
/config.php + + + + + + + +
/uploads/ + + + + + + +
+ +
+ +
+
+ Everything looks good. Proceed to the next step. + Next Step +
+ + +

+ +
+ +
+
+
+ + diff --git a/api/install/views/index/setup.php b/api/install/views/index/setup.php new file mode 100644 index 0000000..5a9cb28 --- /dev/null +++ b/api/install/views/index/setup.php @@ -0,0 +1,55 @@ +
+ +
+
+

Setup

+ +
+
+ + +
+
+ + +
Make sure to specify the full url of the installation path of the website. https://www.yourproject.com/
+
+
+

Database Info

+
+ + +
Make sure to specify the full url of the installation path of the website. https://www.yourproject.com/
+
+ +
+ + +
Make sure to specify the full url of the installation path of the website. https://www.yourproject.com/
+
+ +
+ + +
Make sure to specify the full url of the installation path of the website. https://www.yourproject.com/
+
+ +
+ + +
Make sure to specify the full url of the installation path of the website. https://www.yourproject.com/
+
+ +
+ + + +
+ +
+ +
+
+
+ + diff --git a/api/install/views/wrapper/site/footer.php b/api/install/views/wrapper/site/footer.php new file mode 100644 index 0000000..49e03a3 --- /dev/null +++ b/api/install/views/wrapper/site/footer.php @@ -0,0 +1,20 @@ + +JavaScript) { + foreach ($this->JavaScript as $kj => $JavaScript) : + if(!is_numeric($kj)) { + echo PHP_EOL; + echo $JavaScript . PHP_EOL; + continue; + } + echo '' . PHP_EOL; + endforeach; +} +?> + + + + + + + \ No newline at end of file diff --git a/api/install/views/wrapper/site/header.php b/api/install/views/wrapper/site/header.php new file mode 100644 index 0000000..c23ecbb --- /dev/null +++ b/api/install/views/wrapper/site/header.php @@ -0,0 +1,61 @@ + + + + + + + + + SeedProject - Framework Installation + + + Styles) { + foreach ($this->Styles as $ks => $Styles) : + if(!is_numeric($ks)) { + echo PHP_EOL; + echo $Styles . PHP_EOL; + continue; + } + echo '' .PHP_EOL; + + endforeach; + } + ?> + + + +
+ + +
+ + diff --git a/api/manifest.json b/api/manifest.json new file mode 100644 index 0000000..e494284 --- /dev/null +++ b/api/manifest.json @@ -0,0 +1,21 @@ +{ + "name": "SeedProject Framework", + "short_name" : "PWA", + "start_url": "/", + "scope" : "./", + "icons": [ + { + "src": "/assets/icons/ComiidaIcon-192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "/assets/icons/ComiidaIcon.png", + "sizes": "512x512", + "type": "image/png" + } + ], + "theme_color": "#ffee00", + "background_color": "#ffee00", + "display": "standalone" +} \ No newline at end of file diff --git a/api/sw.js b/api/sw.js new file mode 100644 index 0000000..fa7f2cf --- /dev/null +++ b/api/sw.js @@ -0,0 +1,19 @@ +/** An empty service worker! */ +self.addEventListener ('install', e => { + console.log("Installed!"); + e.waitUntil( + caches.open("static").then(cache => { + return cache.addAll(["/", "/assets/css/pwa.css", "/assets/icons/ComiidaIcon-192.png"]); + }) + ); +}); + + +self.addEventListener("fetch", e => { + e.respondWith( + caches.match(e.request).then(response => { + return response || fetch(e.request); + }) + ); +}); + \ No newline at end of file diff --git a/api/system/ErrorHandler.php b/api/system/ErrorHandler.php new file mode 100644 index 0000000..278f898 --- /dev/null +++ b/api/system/ErrorHandler.php @@ -0,0 +1,170 @@ + 'E_ERROR', + E_WARNING => 'E_WARNING', + E_PARSE => 'E_PARSE', + E_NOTICE => 'E_NOTICE', + E_CORE_ERROR => 'E_CORE_ERROR', + E_CORE_WARNING => 'E_CORE_WARNING', + E_COMPILE_ERROR => 'E_COMPILE_ERROR', + E_COMPILE_WARNING => 'E_COMPILE_WARNING', + E_USER_ERROR => 'E_USER_ERROR', + E_USER_WARNING => 'E_USER_WARNING', + E_USER_NOTICE => 'E_USER_NOTICE', + E_STRICT => 'E_STRICT', + E_RECOVERABLE_ERROR => 'E_RECOVERABLE_ERROR', + E_DEPRECATED => 'E_DEPRECATED', + E_USER_DEPRECATED => 'E_USER_DEPRECATED', + ]; + + private static array $errorTypeLabels = [ + E_ERROR => 'Fatal Error', + E_WARNING => 'Warning', + E_PARSE => 'Parse Error', + E_NOTICE => 'Notice', + E_CORE_ERROR => 'Core Error', + E_CORE_WARNING => 'Core Warning', + E_COMPILE_ERROR => 'Compile Error', + E_COMPILE_WARNING => 'Compile Warning', + E_USER_ERROR => 'User Error', + E_USER_WARNING => 'User Warning', + E_USER_NOTICE => 'User Notice', + E_STRICT => 'Strict Standards', + E_RECOVERABLE_ERROR => 'Recoverable Error', + E_DEPRECATED => 'Deprecated', + E_USER_DEPRECATED => 'User Deprecated', + ]; + + private static array $fatalTypes = [ + E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR, + E_USER_ERROR, E_RECOVERABLE_ERROR, + ]; + + public static function handleError($errno, $errstr, $errfile, $errline) { + if (!(error_reporting() & $errno)) { + return; + } + + // Only persist fatal/critical errors — skip warnings, notices, deprecated, strict + if (in_array($errno, self::$fatalTypes)) { + self::persistError($errno, $errstr, $errfile, $errline); + self::logError($errno, $errstr, $errfile, $errline); + } + + self::displayError($errno, $errstr, $errfile, $errline); + + return true; + } + + public static function handleException($exception) { + $trace = $exception->getTraceAsString(); + self::persistError(E_ERROR, $exception->getMessage(), $exception->getFile(), $exception->getLine(), $trace, 'Exception'); + self::logError(E_ERROR, $exception->getMessage(), $exception->getFile(), $exception->getLine()); + self::displayError(E_ERROR, $exception->getMessage(), $exception->getFile(), $exception->getLine(), $trace); + } + + public static function handleShutdown() { + $error = error_get_last(); + if ($error !== null && in_array($error['type'], [E_ERROR, E_CORE_ERROR, E_COMPILE_ERROR, E_PARSE])) { + self::persistError($error['type'], $error['message'], $error['file'], $error['line']); + self::logError($error['type'], $error['message'], $error['file'], $error['line']); + self::displayError($error['type'], $error['message'], $error['file'], $error['line']); + } + } + + // ─── JSON flat-file persistence ─────────────────────────────────────────── + + private static function persistError($errno, $errstr, $errfile, $errline, $trace = null, $forcedType = null) { + try { + $errorType = $forcedType ?? (self::$errorTypeMap[$errno] ?? 'UNKNOWN'); + + $userId = null; + $orgId = null; + if (isset($_SESSION['login'])) { + $userId = $_SESSION['login']['userid'] ?? null; + $orgId = $_SESSION['login']['org_id'] ?? null; + } + + $url = ((!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'http') + . '://' . ($_SERVER['HTTP_HOST'] ?? '') . ($_SERVER['REQUEST_URI'] ?? ''); + $method = $_SERVER['REQUEST_METHOD'] ?? null; + $ipAddress = $_SERVER['REMOTE_ADDR'] ?? null; + $userAgent = $_SERVER['HTTP_USER_AGENT'] ?? null; + $referer = $_SERVER['HTTP_REFERER'] ?? null; + + $entry = [ + 'id' => uniqid('err_', true), + 'userid' => $userId, + 'org_id' => $orgId, + 'errortype' => $errorType, + 'errorcode' => (string)$errno, + 'url' => substr($url, 0, 512), + 'file' => substr($errfile, 0, 512), + 'line' => (int)$errline, + 'fullerror' => $errstr, + 'trace' => $trace, + 'method' => $method ? substr($method, 0, 10) : null, + 'ip_address' => $ipAddress ? substr($ipAddress, 0, 45) : null, + 'user_agent' => $userAgent ? substr($userAgent, 0, 512) : null, + 'referer' => $referer ? substr($referer, 0, 512) : null, + 'status' => 'unresolved', + 'createdate' => date('Y-m-d H:i:s'), + ]; + + $logPath = __DIR__ . '/errors.json'; + $entries = []; + if (file_exists($logPath)) { + $raw = file_get_contents($logPath); + $decoded = json_decode($raw, true); + if (is_array($decoded)) { + $entries = $decoded; + } + } + + array_unshift($entries, $entry); + if (count($entries) > 1000) { + $entries = array_slice($entries, 0, 1000); + } + + file_put_contents($logPath, json_encode($entries, JSON_PRETTY_PRINT), LOCK_EX); + + } catch (\Throwable $e) { + // Silently fail — never let error logging crash the app + error_log('ErrorHandler::persistError failed: ' . $e->getMessage()); + } + } + + // ─── File logging ───────────────────────────────────────────────────────── + + private static function logError($errno, $errstr, $errfile, $errline) { + $message = date('[Y-m-d H:i:s]') . " Error: [$errno] $errstr in $errfile on line $errline\n"; + error_log($message, 3, __DIR__ . '/app_errors.log'); + } + + // ─── Display (debug mode only) ──────────────────────────────────────────── + + private static function displayError($errno, $errstr, $errfile, $errline, $trace = null) { + $errorType = self::$errorTypeLabels[$errno] ?? 'Unknown Error'; + + if (defined('DEBUG') && DEBUG) { + echo "
"; + echo "

" . htmlspecialchars($errorType) . " Occurred

"; + echo "

Message: " . htmlspecialchars($errstr) . "

"; + echo "

File: " . htmlspecialchars($errfile) . "

"; + echo "

Line: " . htmlspecialchars((string)$errline) . "

"; + if ($trace) { + echo "

Stack Trace:

"; + echo "
" . htmlspecialchars($trace) . "
"; + } + echo "

Request Details:

";
+            echo "URL: "    . htmlspecialchars($_SERVER['REQUEST_URI']    ?? '') . "\n";
+            echo "Method: " . htmlspecialchars($_SERVER['REQUEST_METHOD'] ?? '') . "\n";
+            echo "Time: "   . date('Y-m-d H:i:s') . "\n";
+            echo "IP: "     . htmlspecialchars($_SERVER['REMOTE_ADDR']    ?? '') . "\n";
+            echo "
"; + } + } +} diff --git a/api/system/errors.json b/api/system/errors.json new file mode 100644 index 0000000..73ad76c --- /dev/null +++ b/api/system/errors.json @@ -0,0 +1,362 @@ +[ + { + "id": "err_69ee699b38a214.70118470", + "userid": 2565, + "org_id": null, + "errortype": "Exception", + "errorcode": "1", + "url": "https:\/\/secure.creditpullengine.com\/appi\/pulls\/pullReport", + "file": "\/www\/wwwroot\/cpeSecure\/app\/Services\/Credit\/EquifaxCodeMap.php", + "line": 317, + "fullerror": "Call to undefined method App\\Services\\Credit\\EquifaxCodeMap::getScoreFactors()", + "trace": "#0 \/www\/wwwroot\/cpeSecure\/app\/Services\/Credit\/EquifaxAdapter.php(159): App\\Services\\Credit\\EquifaxCodeMap::getScoreFactor()\n#1 \/www\/wwwroot\/cpeSecure\/app\/Services\/Credit\/EquifaxAdapter.php(43): App\\Services\\Credit\\EquifaxAdapter->mapScoreFactors()\n#2 \/www\/wwwroot\/cpeSecure\/appi\/controllers\/pulls.php(134): App\\Services\\Credit\\EquifaxAdapter->transform()\n#3 \/www\/wwwroot\/cpeSecure\/core\/Bootstrap.php(227): pulls->pullReport()\n#4 \/www\/wwwroot\/cpeSecure\/core\/Bootstrap.php(64): Bootstrap->_callControllerMethod()\n#5 \/www\/wwwroot\/cpeSecure\/appi\/index.php(11): Bootstrap->init()\n#6 {main}", + "method": "POST", + "ip_address": "193.36.238.59", + "user_agent": "Mozilla\/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit\/537.36 (KHTML, like Gecko) Chrome\/147.0.0.0 Safari\/537.36", + "referer": "https:\/\/secure.creditpullengine.com\/pulls", + "status": "unresolved", + "createdate": "2026-04-26 15:38:03" + }, + { + "id": "err_69ee698c669d29.22210069", + "userid": 2565, + "org_id": null, + "errortype": "Exception", + "errorcode": "1", + "url": "https:\/\/secure.creditpullengine.com\/appi\/pulls\/pullReport", + "file": "\/www\/wwwroot\/cpeSecure\/app\/Services\/Credit\/EquifaxCodeMap.php", + "line": 317, + "fullerror": "Call to undefined method App\\Services\\Credit\\EquifaxCodeMap::getScoreFactors()", + "trace": "#0 \/www\/wwwroot\/cpeSecure\/app\/Services\/Credit\/EquifaxAdapter.php(159): App\\Services\\Credit\\EquifaxCodeMap::getScoreFactor()\n#1 \/www\/wwwroot\/cpeSecure\/app\/Services\/Credit\/EquifaxAdapter.php(43): App\\Services\\Credit\\EquifaxAdapter->mapScoreFactors()\n#2 \/www\/wwwroot\/cpeSecure\/appi\/controllers\/pulls.php(134): App\\Services\\Credit\\EquifaxAdapter->transform()\n#3 \/www\/wwwroot\/cpeSecure\/core\/Bootstrap.php(227): pulls->pullReport()\n#4 \/www\/wwwroot\/cpeSecure\/core\/Bootstrap.php(64): Bootstrap->_callControllerMethod()\n#5 \/www\/wwwroot\/cpeSecure\/appi\/index.php(11): Bootstrap->init()\n#6 {main}", + "method": "POST", + "ip_address": "193.36.238.59", + "user_agent": "Mozilla\/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit\/537.36 (KHTML, like Gecko) Chrome\/147.0.0.0 Safari\/537.36", + "referer": "https:\/\/secure.creditpullengine.com\/pulls", + "status": "unresolved", + "createdate": "2026-04-26 15:37:48" + }, + { + "id": "err_69ee6950756db9.31923142", + "userid": 2565, + "org_id": null, + "errortype": "Exception", + "errorcode": "1", + "url": "https:\/\/secure.creditpullengine.com\/appi\/pulls\/pullReport", + "file": "\/www\/wwwroot\/cpeSecure\/app\/Services\/Credit\/EquifaxCodeMap.php", + "line": 317, + "fullerror": "Call to undefined method App\\Services\\Credit\\EquifaxCodeMap::getScoreFactors()", + "trace": "#0 \/www\/wwwroot\/cpeSecure\/app\/Services\/Credit\/EquifaxAdapter.php(159): App\\Services\\Credit\\EquifaxCodeMap::getScoreFactor()\n#1 \/www\/wwwroot\/cpeSecure\/app\/Services\/Credit\/EquifaxAdapter.php(43): App\\Services\\Credit\\EquifaxAdapter->mapScoreFactors()\n#2 \/www\/wwwroot\/cpeSecure\/appi\/controllers\/pulls.php(134): App\\Services\\Credit\\EquifaxAdapter->transform()\n#3 \/www\/wwwroot\/cpeSecure\/core\/Bootstrap.php(227): pulls->pullReport()\n#4 \/www\/wwwroot\/cpeSecure\/core\/Bootstrap.php(64): Bootstrap->_callControllerMethod()\n#5 \/www\/wwwroot\/cpeSecure\/appi\/index.php(11): Bootstrap->init()\n#6 {main}", + "method": "POST", + "ip_address": "193.36.238.59", + "user_agent": "Mozilla\/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit\/537.36 (KHTML, like Gecko) Chrome\/147.0.0.0 Safari\/537.36", + "referer": "https:\/\/secure.creditpullengine.com\/pulls", + "status": "unresolved", + "createdate": "2026-04-26 15:36:48" + }, + { + "id": "err_69ee6934c9c873.23488128", + "userid": 2565, + "org_id": null, + "errortype": "Exception", + "errorcode": "1", + "url": "https:\/\/secure.creditpullengine.com\/appi\/pulls\/pullReport", + "file": "\/www\/wwwroot\/cpeSecure\/app\/Services\/Credit\/EquifaxCodeMap.php", + "line": 317, + "fullerror": "Call to undefined method App\\Services\\Credit\\EquifaxCodeMap::getScoreFactors()", + "trace": "#0 \/www\/wwwroot\/cpeSecure\/app\/Services\/Credit\/EquifaxAdapter.php(159): App\\Services\\Credit\\EquifaxCodeMap::getScoreFactor()\n#1 \/www\/wwwroot\/cpeSecure\/app\/Services\/Credit\/EquifaxAdapter.php(43): App\\Services\\Credit\\EquifaxAdapter->mapScoreFactors()\n#2 \/www\/wwwroot\/cpeSecure\/appi\/controllers\/pulls.php(134): App\\Services\\Credit\\EquifaxAdapter->transform()\n#3 \/www\/wwwroot\/cpeSecure\/core\/Bootstrap.php(227): pulls->pullReport()\n#4 \/www\/wwwroot\/cpeSecure\/core\/Bootstrap.php(64): Bootstrap->_callControllerMethod()\n#5 \/www\/wwwroot\/cpeSecure\/appi\/index.php(11): Bootstrap->init()\n#6 {main}", + "method": "POST", + "ip_address": "193.36.238.59", + "user_agent": "Mozilla\/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit\/537.36 (KHTML, like Gecko) Chrome\/147.0.0.0 Safari\/537.36", + "referer": "https:\/\/secure.creditpullengine.com\/pulls", + "status": "unresolved", + "createdate": "2026-04-26 15:36:20" + }, + { + "id": "err_69ee69273633b8.88201084", + "userid": 2565, + "org_id": null, + "errortype": "Exception", + "errorcode": "1", + "url": "https:\/\/secure.creditpullengine.com\/appi\/pulls\/pullReport", + "file": "\/www\/wwwroot\/cpeSecure\/app\/Services\/Credit\/EquifaxCodeMap.php", + "line": 317, + "fullerror": "Call to undefined method App\\Services\\Credit\\EquifaxCodeMap::getScoreFactors()", + "trace": "#0 \/www\/wwwroot\/cpeSecure\/app\/Services\/Credit\/EquifaxAdapter.php(159): App\\Services\\Credit\\EquifaxCodeMap::getScoreFactor()\n#1 \/www\/wwwroot\/cpeSecure\/app\/Services\/Credit\/EquifaxAdapter.php(43): App\\Services\\Credit\\EquifaxAdapter->mapScoreFactors()\n#2 \/www\/wwwroot\/cpeSecure\/appi\/controllers\/pulls.php(134): App\\Services\\Credit\\EquifaxAdapter->transform()\n#3 \/www\/wwwroot\/cpeSecure\/core\/Bootstrap.php(227): pulls->pullReport()\n#4 \/www\/wwwroot\/cpeSecure\/core\/Bootstrap.php(64): Bootstrap->_callControllerMethod()\n#5 \/www\/wwwroot\/cpeSecure\/appi\/index.php(11): Bootstrap->init()\n#6 {main}", + "method": "POST", + "ip_address": "193.36.238.59", + "user_agent": "Mozilla\/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit\/537.36 (KHTML, like Gecko) Chrome\/147.0.0.0 Safari\/537.36", + "referer": "https:\/\/secure.creditpullengine.com\/pulls", + "status": "unresolved", + "createdate": "2026-04-26 15:36:07" + }, + { + "id": "err_69ed670a033b66.19048308", + "userid": null, + "org_id": null, + "errortype": "Exception", + "errorcode": "1", + "url": "https:\/\/secure.creditpullengine.com\/public\/", + "file": "\/www\/wwwroot\/cpeSecure\/core\/Bootstrap.php", + "line": 131, + "fullerror": "Failed opening required 'public\/controllers\/index.php' (include_path='.:')", + "trace": "#0 \/www\/wwwroot\/cpeSecure\/core\/Bootstrap.php(40): Bootstrap->_loadDefaultController()\n#1 \/www\/wwwroot\/cpeSecure\/public\/index.php(11): Bootstrap->init()\n#2 {main}", + "method": "GET", + "ip_address": "89.244.95.100", + "user_agent": "Mozilla\/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit\/537.36 (KHTML, like Gecko) Chrome\/143.0.0.0 Safari\/537.36", + "referer": "https:\/\/secure.creditpullengine.com\/login", + "status": "unresolved", + "createdate": "2026-04-25 21:14:50" + }, + { + "id": "err_69ed5d01f08370.23851037", + "userid": null, + "org_id": null, + "errortype": "Exception", + "errorcode": "1", + "url": "https:\/\/secure.creditpullengine.com\/cron\/jsonReports", + "file": "\/www\/wwwroot\/cpeSecure\/public\/controllers\/cron.php", + "line": 14, + "fullerror": "Class \"Carbon\\Carbon\" not found", + "trace": "#0 \/www\/wwwroot\/cpeSecure\/core\/Bootstrap.php(148): Cron->__construct()\n#1 \/www\/wwwroot\/cpeSecure\/core\/Bootstrap.php(63): Bootstrap->_loadExistingController()\n#2 \/www\/wwwroot\/cpeSecure\/public\/index.php(11): Bootstrap->init()\n#3 \/www\/wwwroot\/cpeSecure\/index.php(2): require_once('...')\n#4 {main}", + "method": "GET", + "ip_address": "140.82.26.192", + "user_agent": "curl\/7.76.1", + "referer": null, + "status": "unresolved", + "createdate": "2026-04-25 20:32:01" + }, + { + "id": "err_69ed5d01e8bb28.09924309", + "userid": null, + "org_id": null, + "errortype": "Exception", + "errorcode": "1", + "url": "https:\/\/secure.creditpullengine.com\/cron\/jsonReports", + "file": "\/www\/wwwroot\/cpeSecure\/public\/controllers\/cron.php", + "line": 14, + "fullerror": "Class \"Carbon\\Carbon\" not found", + "trace": "#0 \/www\/wwwroot\/cpeSecure\/core\/Bootstrap.php(148): Cron->__construct()\n#1 \/www\/wwwroot\/cpeSecure\/core\/Bootstrap.php(63): Bootstrap->_loadExistingController()\n#2 \/www\/wwwroot\/cpeSecure\/public\/index.php(11): Bootstrap->init()\n#3 \/www\/wwwroot\/cpeSecure\/index.php(2): require_once('...')\n#4 {main}", + "method": "GET", + "ip_address": "207.246.75.27", + "user_agent": null, + "referer": null, + "status": "unresolved", + "createdate": "2026-04-25 20:32:01" + }, + { + "id": "err_69ed5cf5670d39.78662554", + "userid": null, + "org_id": null, + "errortype": "Exception", + "errorcode": "1", + "url": "https:\/\/secure.creditpullengine.com\/cron\/jsonReports", + "file": "\/www\/wwwroot\/cpeSecure\/public\/controllers\/cron.php", + "line": 14, + "fullerror": "Class \"Carbon\\Carbon\" not found", + "trace": "#0 \/www\/wwwroot\/cpeSecure\/core\/Bootstrap.php(148): Cron->__construct()\n#1 \/www\/wwwroot\/cpeSecure\/core\/Bootstrap.php(63): Bootstrap->_loadExistingController()\n#2 \/www\/wwwroot\/cpeSecure\/public\/index.php(11): Bootstrap->init()\n#3 \/www\/wwwroot\/cpeSecure\/index.php(2): require_once('...')\n#4 {main}", + "method": "GET", + "ip_address": "206.62.143.66", + "user_agent": "Mozilla\/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit\/537.36 (KHTML, like Gecko) Chrome\/147.0.0.0 Safari\/537.36", + "referer": null, + "status": "unresolved", + "createdate": "2026-04-25 20:31:49" + }, + { + "id": "err_69ed5cdaeef019.29989883", + "userid": null, + "org_id": null, + "errortype": "Exception", + "errorcode": "1", + "url": "https:\/\/secure.creditpullengine.com\/cron\/jsonReports", + "file": "\/www\/wwwroot\/cpeSecure\/public\/controllers\/cron.php", + "line": 14, + "fullerror": "Class \"Carbon\\Carbon\" not found", + "trace": "#0 \/www\/wwwroot\/cpeSecure\/core\/Bootstrap.php(148): Cron->__construct()\n#1 \/www\/wwwroot\/cpeSecure\/core\/Bootstrap.php(63): Bootstrap->_loadExistingController()\n#2 \/www\/wwwroot\/cpeSecure\/public\/index.php(11): Bootstrap->init()\n#3 \/www\/wwwroot\/cpeSecure\/index.php(2): require_once('...')\n#4 {main}", + "method": "GET", + "ip_address": "206.62.143.66", + "user_agent": "Mozilla\/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit\/537.36 (KHTML, like Gecko) Chrome\/147.0.0.0 Safari\/537.36", + "referer": null, + "status": "unresolved", + "createdate": "2026-04-25 20:31:22" + }, + { + "id": "err_69ed5cc5a99338.48739755", + "userid": null, + "org_id": null, + "errortype": "Exception", + "errorcode": "1", + "url": "https:\/\/secure.creditpullengine.com\/cron\/jsonReports", + "file": "\/www\/wwwroot\/cpeSecure\/public\/controllers\/cron.php", + "line": 14, + "fullerror": "Class \"Carbon\\Carbon\" not found", + "trace": "#0 \/www\/wwwroot\/cpeSecure\/core\/Bootstrap.php(148): Cron->__construct()\n#1 \/www\/wwwroot\/cpeSecure\/core\/Bootstrap.php(63): Bootstrap->_loadExistingController()\n#2 \/www\/wwwroot\/cpeSecure\/public\/index.php(11): Bootstrap->init()\n#3 \/www\/wwwroot\/cpeSecure\/index.php(2): require_once('...')\n#4 {main}", + "method": "GET", + "ip_address": "207.246.75.27", + "user_agent": null, + "referer": null, + "status": "unresolved", + "createdate": "2026-04-25 20:31:01" + }, + { + "id": "err_69ed5cc5a5d377.51767422", + "userid": null, + "org_id": null, + "errortype": "Exception", + "errorcode": "1", + "url": "https:\/\/secure.creditpullengine.com\/cron\/jsonReports", + "file": "\/www\/wwwroot\/cpeSecure\/public\/controllers\/cron.php", + "line": 14, + "fullerror": "Class \"Carbon\\Carbon\" not found", + "trace": "#0 \/www\/wwwroot\/cpeSecure\/core\/Bootstrap.php(148): Cron->__construct()\n#1 \/www\/wwwroot\/cpeSecure\/core\/Bootstrap.php(63): Bootstrap->_loadExistingController()\n#2 \/www\/wwwroot\/cpeSecure\/public\/index.php(11): Bootstrap->init()\n#3 \/www\/wwwroot\/cpeSecure\/index.php(2): require_once('...')\n#4 {main}", + "method": "GET", + "ip_address": "140.82.26.192", + "user_agent": "curl\/7.76.1", + "referer": null, + "status": "unresolved", + "createdate": "2026-04-25 20:31:01" + }, + { + "id": "err_69ed5c896b0de3.37548309", + "userid": null, + "org_id": null, + "errortype": "Exception", + "errorcode": "1", + "url": "https:\/\/secure.creditpullengine.com\/cron\/jsonReports", + "file": "\/www\/wwwroot\/cpeSecure\/public\/controllers\/cron.php", + "line": 14, + "fullerror": "Class \"App\\Core\\StripeService\" not found", + "trace": "#0 \/www\/wwwroot\/cpeSecure\/core\/Bootstrap.php(148): Cron->__construct()\n#1 \/www\/wwwroot\/cpeSecure\/core\/Bootstrap.php(63): Bootstrap->_loadExistingController()\n#2 \/www\/wwwroot\/cpeSecure\/public\/index.php(11): Bootstrap->init()\n#3 \/www\/wwwroot\/cpeSecure\/index.php(2): require_once('...')\n#4 {main}", + "method": "GET", + "ip_address": "207.246.75.27", + "user_agent": null, + "referer": null, + "status": "unresolved", + "createdate": "2026-04-25 20:30:01" + }, + { + "id": "err_69ed5c895c99d9.73562260", + "userid": null, + "org_id": null, + "errortype": "Exception", + "errorcode": "1", + "url": "https:\/\/secure.creditpullengine.com\/cron\/jsonReports", + "file": "\/www\/wwwroot\/cpeSecure\/public\/controllers\/cron.php", + "line": 14, + "fullerror": "Class \"App\\Core\\StripeService\" not found", + "trace": "#0 \/www\/wwwroot\/cpeSecure\/core\/Bootstrap.php(148): Cron->__construct()\n#1 \/www\/wwwroot\/cpeSecure\/core\/Bootstrap.php(63): Bootstrap->_loadExistingController()\n#2 \/www\/wwwroot\/cpeSecure\/public\/index.php(11): Bootstrap->init()\n#3 \/www\/wwwroot\/cpeSecure\/index.php(2): require_once('...')\n#4 {main}", + "method": "GET", + "ip_address": "140.82.26.192", + "user_agent": "curl\/7.76.1", + "referer": null, + "status": "unresolved", + "createdate": "2026-04-25 20:30:01" + }, + { + "id": "err_69e773f2990218.94993625", + "userid": 2548, + "org_id": null, + "errortype": "Exception", + "errorcode": "1", + "url": "http:\/\/nsecure.creditpullengine.com\/", + "file": "\/www\/wwwroot\/nSecure\/core\/View.php", + "line": 19, + "fullerror": "Failed opening required '\/www\/wwwroot\/nSecure\/public\/views\/index\/orgadmin.php' (include_path='.:')", + "trace": "#0 \/www\/wwwroot\/nSecure\/public\/controllers\/index.php(25): View->render()\n#1 \/www\/wwwroot\/nSecure\/core\/Bootstrap.php(133): Index->index()\n#2 \/www\/wwwroot\/nSecure\/core\/Bootstrap.php(40): Bootstrap->_loadDefaultController()\n#3 \/www\/wwwroot\/nSecure\/public\/index.php(11): Bootstrap->init()\n#4 \/www\/wwwroot\/nSecure\/index.php(2): require_once('...')\n#5 {main}", + "method": "GET", + "ip_address": "172.70.55.166", + "user_agent": "Mozilla\/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit\/537.36 (KHTML, like Gecko) Chrome\/147.0.0.0 Safari\/537.36", + "referer": "https:\/\/nsecure.creditpullengine.com\/company\/consumer-shield-llc.ZhYbs5uFyhX2G7JfdVIGsmxCGaDRq2A91vcUedk5_yU", + "status": "unresolved", + "createdate": "2026-04-21 08:56:18" + }, + { + "id": "err_69e77133a9dbc2.26211292", + "userid": 3, + "org_id": null, + "errortype": "Exception", + "errorcode": "1", + "url": "http:\/\/nsecure.creditpullengine.com\/pulls\/view\/1427.f5CHVieZZZGJISi3HECOj9z-oRbIbCuYysAbz22Y9oI", + "file": "\/www\/wwwroot\/nSecure\/public\/controllers\/pulls.php", + "line": 104, + "fullerror": "Call to undefined function redirect()", + "trace": "#0 \/www\/wwwroot\/nSecure\/core\/Bootstrap.php(222): pulls->view()\n#1 \/www\/wwwroot\/nSecure\/core\/Bootstrap.php(64): Bootstrap->_callControllerMethod()\n#2 \/www\/wwwroot\/nSecure\/public\/index.php(11): Bootstrap->init()\n#3 \/www\/wwwroot\/nSecure\/index.php(2): require_once('...')\n#4 {main}", + "method": "GET", + "ip_address": "172.70.55.166", + "user_agent": "Mozilla\/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit\/537.36 (KHTML, like Gecko) Chrome\/147.0.0.0 Safari\/537.36", + "referer": null, + "status": "unresolved", + "createdate": "2026-04-21 08:44:35" + }, + { + "id": "err_69e77014a27279.45734769", + "userid": 2566, + "org_id": null, + "errortype": "Exception", + "errorcode": "1", + "url": "http:\/\/nsecure.creditpullengine.com\/", + "file": "\/www\/wwwroot\/nSecure\/core\/View.php", + "line": 19, + "fullerror": "Failed opening required '\/www\/wwwroot\/nSecure\/public\/views\/index\/orgadmin.php' (include_path='.:')", + "trace": "#0 \/www\/wwwroot\/nSecure\/public\/controllers\/index.php(25): View->render()\n#1 \/www\/wwwroot\/nSecure\/core\/Bootstrap.php(133): Index->index()\n#2 \/www\/wwwroot\/nSecure\/core\/Bootstrap.php(40): Bootstrap->_loadDefaultController()\n#3 \/www\/wwwroot\/nSecure\/public\/index.php(11): Bootstrap->init()\n#4 \/www\/wwwroot\/nSecure\/index.php(2): require_once('...')\n#5 {main}", + "method": "GET", + "ip_address": "104.23.237.20", + "user_agent": "Mozilla\/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit\/537.36 (KHTML, like Gecko) Chrome\/147.0.0.0 Safari\/537.36", + "referer": "https:\/\/nsecure.creditpullengine.com\/admin\/users", + "status": "unresolved", + "createdate": "2026-04-21 08:39:48" + }, + { + "id": "err_69e76e93d1cc43.29318732", + "userid": 3, + "org_id": null, + "errortype": "Exception", + "errorcode": "1", + "url": "http:\/\/nsecure.creditpullengine.com\/pulls\/view\/1427.f5CHVieZZZGJISi3HECOj9z-oRbIbCuYysAbz22Y9oI", + "file": "\/www\/wwwroot\/nSecure\/public\/controllers\/pulls.php", + "line": 104, + "fullerror": "Call to undefined function redirect()", + "trace": "#0 \/www\/wwwroot\/nSecure\/core\/Bootstrap.php(222): pulls->view()\n#1 \/www\/wwwroot\/nSecure\/core\/Bootstrap.php(64): Bootstrap->_callControllerMethod()\n#2 \/www\/wwwroot\/nSecure\/public\/index.php(11): Bootstrap->init()\n#3 \/www\/wwwroot\/nSecure\/index.php(2): require_once('...')\n#4 {main}", + "method": "GET", + "ip_address": "104.23.237.20", + "user_agent": "Mozilla\/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit\/537.36 (KHTML, like Gecko) Chrome\/147.0.0.0 Safari\/537.36", + "referer": "https:\/\/nsecure.creditpullengine.com\/reports\/pulls\/consumer-shield-llc.YAj8lpRtFakNZbbopBFBpJGCDUF7bXC5B3HzJg0I3Bw", + "status": "unresolved", + "createdate": "2026-04-21 08:33:23" + }, + { + "id": "err_69e76bb0efcf58.05946519", + "userid": 3, + "org_id": null, + "errortype": "Exception", + "errorcode": "1", + "url": "http:\/\/nsecure.creditpullengine.com\/pulls\/view\/1459.jDV7q5dcrBikg_9FMFi4k6ZMtZokNkCl9Uk6JMYOIZg", + "file": "\/www\/wwwroot\/nSecure\/public\/controllers\/pulls.php", + "line": 104, + "fullerror": "Call to undefined function redirect()", + "trace": "#0 \/www\/wwwroot\/nSecure\/core\/Bootstrap.php(222): pulls->view()\n#1 \/www\/wwwroot\/nSecure\/core\/Bootstrap.php(64): Bootstrap->_callControllerMethod()\n#2 \/www\/wwwroot\/nSecure\/public\/index.php(11): Bootstrap->init()\n#3 \/www\/wwwroot\/nSecure\/index.php(2): require_once('...')\n#4 {main}", + "method": "GET", + "ip_address": "198.41.231.17", + "user_agent": "Mozilla\/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit\/537.36 (KHTML, like Gecko) Chrome\/147.0.0.0 Safari\/537.36", + "referer": "https:\/\/nsecure.creditpullengine.com\/reports\/pulls\/limpia-deudas-llc._i0s98IqNv38bmtnqrE7eluqQNSKLtkOdaJu2qOk3-I", + "status": "unresolved", + "createdate": "2026-04-21 08:21:04" + }, + { + "id": "err_69e76b1f397f19.04102361", + "userid": 2565, + "org_id": null, + "errortype": "Exception", + "errorcode": "1", + "url": "http:\/\/nsecure.creditpullengine.com\/", + "file": "\/www\/wwwroot\/nSecure\/core\/View.php", + "line": 19, + "fullerror": "Failed opening required '\/www\/wwwroot\/nSecure\/public\/views\/index\/orgadmin.php' (include_path='.:')", + "trace": "#0 \/www\/wwwroot\/nSecure\/public\/controllers\/index.php(25): View->render()\n#1 \/www\/wwwroot\/nSecure\/core\/Bootstrap.php(133): Index->index()\n#2 \/www\/wwwroot\/nSecure\/core\/Bootstrap.php(40): Bootstrap->_loadDefaultController()\n#3 \/www\/wwwroot\/nSecure\/public\/index.php(11): Bootstrap->init()\n#4 \/www\/wwwroot\/nSecure\/index.php(2): require_once('...')\n#5 {main}", + "method": "GET", + "ip_address": "172.68.7.18", + "user_agent": "Mozilla\/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit\/537.36 (KHTML, like Gecko) Chrome\/147.0.0.0 Safari\/537.36", + "referer": "https:\/\/nsecure.creditpullengine.com\/admin\/users", + "status": "unresolved", + "createdate": "2026-04-21 08:18:39" + } +] \ No newline at end of file diff --git a/app/.astroagent/skills/ui-ux/SKILL.md b/app/.astroagent/skills/ui-ux/SKILL.md new file mode 100644 index 0000000..343d5d2 --- /dev/null +++ b/app/.astroagent/skills/ui-ux/SKILL.md @@ -0,0 +1,711 @@ +--- +name: ui-ux-pro-max +description: "UI/UX design intelligence for web and mobile. Includes 50+ styles, 161 color palettes, 57 font pairings, 161 product types, 99 UX guidelines, and 25 chart types across 10 stacks (React, Next.js, Vue, Svelte, SwiftUI, React Native, Flutter, Tailwind, shadcn/ui, and HTML/CSS). Actions: plan, build, create, design, implement, review, fix, improve, optimize, enhance, refactor, and check UI/UX code. Projects: website, landing page, dashboard, admin panel, e-commerce, SaaS, portfolio, blog, and mobile app. Elements: button, modal, navbar, sidebar, card, table, form, and chart. Styles: glassmorphism, claymorphism, minimalism, brutalism, neumorphism, bento grid, dark mode, responsive, skeuomorphism, and flat design. Topics: color systems, accessibility, animation, layout, typography, font pairing, spacing, interaction states, shadow, and gradient. Integrations: shadcn/ui MCP for component search and examples." +--- + +> **astroagent environment note:** You run headless in a sandbox with **no shell/scripts**. Ignore any instruction here to run search scripts or `--domain` / `--design-system` / CLI queries — apply the written guidance directly. This project is an **Astro + Tailwind** website: prefer the Web/CSS/Tailwind rules and treat native iOS/Android-only items (haptics, VoiceOver, safe-area) as optional. Always match the project's existing design system first (read AGENTS.md / DESIGN.md and nearby components). + +# UI/UX Pro Max - Design Intelligence + +Comprehensive design guide for web and mobile applications. Contains 50+ styles, 161 color palettes, 57 font pairings, 161 product types with reasoning rules, 99 UX guidelines, and 25 chart types across 10 technology stacks. Searchable database with priority-based recommendations. + +## When to Apply + +This Skill should be used when the task involves **UI structure, visual design decisions, interaction patterns, or user experience quality control**. + +### Must Use + +This Skill must be invoked in the following situations: + +- Designing new pages (Landing Page, Dashboard, Admin, SaaS, Mobile App) +- Creating or refactoring UI components (buttons, modals, forms, tables, charts, etc.) +- Choosing color schemes, typography systems, spacing standards, or layout systems +- Reviewing UI code for user experience, accessibility, or visual consistency +- Implementing navigation structures, animations, or responsive behavior +- Making product-level design decisions (style, information hierarchy, brand expression) +- Improving perceived quality, clarity, or usability of interfaces + +### Recommended + +This Skill is recommended in the following situations: + +- UI looks "not professional enough" but the reason is unclear +- Receiving feedback on usability or experience +- Pre-launch UI quality optimization +- Aligning cross-platform design (Web / iOS / Android) +- Building design systems or reusable component libraries + +### Skip + +This Skill is not needed in the following situations: + +- Pure backend logic development +- Only involving API or database design +- Performance optimization unrelated to the interface +- Infrastructure or DevOps work +- Non-visual scripts or automation tasks + +**Decision criteria**: If the task will change how a feature **looks, feels, moves, or is interacted with**, this Skill should be used. + +## Rule Categories by Priority + +*For human/AI reference: follow priority 1→10 to decide which rule category to focus on first; use `--domain ` to query details when needed. Scripts do not read this table.* + +| Priority | Category | Impact | Domain | Key Checks (Must Have) | Anti-Patterns (Avoid) | +|----------|----------|--------|--------|------------------------|------------------------| +| 1 | Accessibility | CRITICAL | `ux` | Contrast 4.5:1, Alt text, Keyboard nav, Aria-labels | Removing focus rings, Icon-only buttons without labels | +| 2 | Touch & Interaction | CRITICAL | `ux` | Min size 44×44px, 8px+ spacing, Loading feedback | Reliance on hover only, Instant state changes (0ms) | +| 3 | Performance | HIGH | `ux` | WebP/AVIF, Lazy loading, Reserve space (CLS < 0.1) | Layout thrashing, Cumulative Layout Shift | +| 4 | Style Selection | HIGH | `style`, `product` | Match product type, Consistency, SVG icons (no emoji) | Mixing flat & skeuomorphic randomly, Emoji as icons | +| 5 | Layout & Responsive | HIGH | `ux` | Mobile-first breakpoints, Viewport meta, No horizontal scroll | Horizontal scroll, Fixed px container widths, Disable zoom | +| 6 | Typography & Color | MEDIUM | `typography`, `color` | Base 16px, Line-height 1.5, Semantic color tokens | Text < 12px body, Gray-on-gray, Raw hex in components | +| 7 | Animation | MEDIUM | `ux` | Duration 150–300ms, Motion conveys meaning, Spatial continuity | Decorative-only animation, Animating width/height, No reduced-motion | +| 8 | Forms & Feedback | MEDIUM | `ux` | Visible labels, Error near field, Helper text, Progressive disclosure | Placeholder-only label, Errors only at top, Overwhelm upfront | +| 9 | Navigation Patterns | HIGH | `ux` | Predictable back, Bottom nav ≤5, Deep linking | Overloaded nav, Broken back behavior, No deep links | +| 10 | Charts & Data | LOW | `chart` | Legends, Tooltips, Accessible colors | Relying on color alone to convey meaning | + +## Quick Reference + +### 1. Accessibility (CRITICAL) + +- `color-contrast` - Minimum 4.5:1 ratio for normal text (large text 3:1); Material Design +- `focus-states` - Visible focus rings on interactive elements (2–4px; Apple HIG, MD) +- `alt-text` - Descriptive alt text for meaningful images +- `aria-labels` - aria-label for icon-only buttons; accessibilityLabel in native (Apple HIG) +- `keyboard-nav` - Tab order matches visual order; full keyboard support (Apple HIG) +- `form-labels` - Use label with for attribute +- `skip-links` - Skip to main content for keyboard users +- `heading-hierarchy` - Sequential h1→h6, no level skip +- `color-not-only` - Don't convey info by color alone (add icon/text) +- `dynamic-type` - Support system text scaling; avoid truncation as text grows (Apple Dynamic Type, MD) +- `reduced-motion` - Respect prefers-reduced-motion; reduce/disable animations when requested (Apple Reduced Motion API, MD) +- `voiceover-sr` - Meaningful accessibilityLabel/accessibilityHint; logical reading order for VoiceOver/screen readers (Apple HIG, MD) +- `escape-routes` - Provide cancel/back in modals and multi-step flows (Apple HIG) +- `keyboard-shortcuts` - Preserve system and a11y shortcuts; offer keyboard alternatives for drag-and-drop (Apple HIG) + +### 2. Touch & Interaction (CRITICAL) + +- `touch-target-size` - Min 44×44pt (Apple) / 48×48dp (Material); extend hit area beyond visual bounds if needed +- `touch-spacing` - Minimum 8px/8dp gap between touch targets (Apple HIG, MD) +- `hover-vs-tap` - Use click/tap for primary interactions; don't rely on hover alone +- `loading-buttons` - Disable button during async operations; show spinner or progress +- `error-feedback` - Clear error messages near problem +- `cursor-pointer` - Add cursor-pointer to clickable elements (Web) +- `gesture-conflicts` - Avoid horizontal swipe on main content; prefer vertical scroll +- `tap-delay` - Use touch-action: manipulation to reduce 300ms delay (Web) +- `standard-gestures` - Use platform standard gestures consistently; don't redefine (e.g. swipe-back, pinch-zoom) (Apple HIG) +- `system-gestures` - Don't block system gestures (Control Center, back swipe, etc.) (Apple HIG) +- `press-feedback` - Visual feedback on press (ripple/highlight; MD state layers) +- `haptic-feedback` - Use haptic for confirmations and important actions; avoid overuse (Apple HIG) +- `gesture-alternative` - Don't rely on gesture-only interactions; always provide visible controls for critical actions +- `safe-area-awareness` - Keep primary touch targets away from notch, Dynamic Island, gesture bar and screen edges +- `no-precision-required` - Avoid requiring pixel-perfect taps on small icons or thin edges +- `swipe-clarity` - Swipe actions must show clear affordance or hint (chevron, label, tutorial) +- `drag-threshold` - Use a movement threshold before starting drag to avoid accidental drags + +### 3. Performance (HIGH) + +- `image-optimization` - Use WebP/AVIF, responsive images (srcset/sizes), lazy load non-critical assets +- `image-dimension` - Declare width/height or use aspect-ratio to prevent layout shift (Core Web Vitals: CLS) +- `font-loading` - Use font-display: swap/optional to avoid invisible text (FOIT); reserve space to reduce layout shift (MD) +- `font-preload` - Preload only critical fonts; avoid overusing preload on every variant +- `critical-css` - Prioritize above-the-fold CSS (inline critical CSS or early-loaded stylesheet) +- `lazy-loading` - Lazy load non-hero components via dynamic import / route-level splitting +- `bundle-splitting` - Split code by route/feature (React Suspense / Next.js dynamic) to reduce initial load and TTI +- `third-party-scripts` - Load third-party scripts async/defer; audit and remove unnecessary ones (MD) +- `reduce-reflows` - Avoid frequent layout reads/writes; batch DOM reads then writes +- `content-jumping` - Reserve space for async content to avoid layout jumps (Core Web Vitals: CLS) +- `lazy-load-below-fold` - Use loading="lazy" for below-the-fold images and heavy media +- `virtualize-lists` - Virtualize lists with 50+ items to improve memory efficiency and scroll performance +- `main-thread-budget` - Keep per-frame work under ~16ms for 60fps; move heavy tasks off main thread (HIG, MD) +- `progressive-loading` - Use skeleton screens / shimmer instead of long blocking spinners for >1s operations (Apple HIG) +- `input-latency` - Keep input latency under ~100ms for taps/scrolls (Material responsiveness standard) +- `tap-feedback-speed` - Provide visual feedback within 100ms of tap (Apple HIG) +- `debounce-throttle` - Use debounce/throttle for high-frequency events (scroll, resize, input) +- `offline-support` - Provide offline state messaging and basic fallback (PWA / mobile) +- `network-fallback` - Offer degraded modes for slow networks (lower-res images, fewer animations) + +### 4. Style Selection (HIGH) + +- `style-match` - Match style to product type (use `--design-system` for recommendations) +- `consistency` - Use same style across all pages +- `no-emoji-icons` - Use SVG icons (Heroicons, Lucide), not emojis +- `color-palette-from-product` - Choose palette from product/industry (search `--domain color`) +- `effects-match-style` - Shadows, blur, radius aligned with chosen style (glass / flat / clay etc.) +- `platform-adaptive` - Respect platform idioms (iOS HIG vs Material): navigation, controls, typography, motion +- `state-clarity` - Make hover/pressed/disabled states visually distinct while staying on-style (Material state layers) +- `elevation-consistent` - Use a consistent elevation/shadow scale for cards, sheets, modals; avoid random shadow values +- `dark-mode-pairing` - Design light/dark variants together to keep brand, contrast, and style consistent +- `icon-style-consistent` - Use one icon set/visual language (stroke width, corner radius) across the product +- `system-controls` - Prefer native/system controls over fully custom ones; only customize when branding requires it (Apple HIG) +- `blur-purpose` - Use blur to indicate background dismissal (modals, sheets), not as decoration (Apple HIG) +- `primary-action` - Each screen should have only one primary CTA; secondary actions visually subordinate (Apple HIG) + +### 5. Layout & Responsive (HIGH) + +- `viewport-meta` - width=device-width initial-scale=1 (never disable zoom) +- `mobile-first` - Design mobile-first, then scale up to tablet and desktop +- `breakpoint-consistency` - Use systematic breakpoints (e.g. 375 / 768 / 1024 / 1440) +- `readable-font-size` - Minimum 16px body text on mobile (avoids iOS auto-zoom) +- `line-length-control` - Mobile 35–60 chars per line; desktop 60–75 chars +- `horizontal-scroll` - No horizontal scroll on mobile; ensure content fits viewport width +- `spacing-scale` - Use 4pt/8dp incremental spacing system (Material Design) +- `touch-density` - Keep component spacing comfortable for touch: not cramped, not causing mis-taps +- `container-width` - Consistent max-width on desktop (max-w-6xl / 7xl) +- `z-index-management` - Define layered z-index scale (e.g. 0 / 10 / 20 / 40 / 100 / 1000) +- `fixed-element-offset` - Fixed navbar/bottom bar must reserve safe padding for underlying content +- `scroll-behavior` - Avoid nested scroll regions that interfere with the main scroll experience +- `viewport-units` - Prefer min-h-dvh over 100vh on mobile +- `orientation-support` - Keep layout readable and operable in landscape mode +- `content-priority` - Show core content first on mobile; fold or hide secondary content +- `visual-hierarchy` - Establish hierarchy via size, spacing, contrast — not color alone + +### 6. Typography & Color (MEDIUM) + +- `line-height` - Use 1.5-1.75 for body text +- `line-length` - Limit to 65-75 characters per line +- `font-pairing` - Match heading/body font personalities +- `font-scale` - Consistent type scale (e.g. 12 14 16 18 24 32) +- `contrast-readability` - Darker text on light backgrounds (e.g. slate-900 on white) +- `text-styles-system` - Use platform type system: iOS 11 Dynamic Type styles / Material 5 type roles (display, headline, title, body, label) (HIG, MD) +- `weight-hierarchy` - Use font-weight to reinforce hierarchy: Bold headings (600–700), Regular body (400), Medium labels (500) (MD) +- `color-semantic` - Define semantic color tokens (primary, secondary, error, surface, on-surface) not raw hex in components (Material color system) +- `color-dark-mode` - Dark mode uses desaturated / lighter tonal variants, not inverted colors; test contrast separately (HIG, MD) +- `color-accessible-pairs` - Foreground/background pairs must meet 4.5:1 (AA) or 7:1 (AAA); use tools to verify (WCAG, MD) +- `color-not-decorative-only` - Functional color (error red, success green) must include icon/text; avoid color-only meaning (HIG, MD) +- `truncation-strategy` - Prefer wrapping over truncation; when truncating use ellipsis and provide full text via tooltip/expand (Apple HIG) +- `letter-spacing` - Respect default letter-spacing per platform; avoid tight tracking on body text (HIG, MD) +- `number-tabular` - Use tabular/monospaced figures for data columns, prices, and timers to prevent layout shift +- `whitespace-balance` - Use whitespace intentionally to group related items and separate sections; avoid visual clutter (Apple HIG) + +### 7. Animation (MEDIUM) + +- `duration-timing` - Use 150–300ms for micro-interactions; complex transitions ≤400ms; avoid >500ms (MD) +- `transform-performance` - Use transform/opacity only; avoid animating width/height/top/left +- `loading-states` - Show skeleton or progress indicator when loading exceeds 300ms +- `excessive-motion` - Animate 1-2 key elements per view max +- `easing` - Use ease-out for entering, ease-in for exiting; avoid linear for UI transitions +- `motion-meaning` - Every animation must express a cause-effect relationship, not just be decorative (Apple HIG) +- `state-transition` - State changes (hover / active / expanded / collapsed / modal) should animate smoothly, not snap +- `continuity` - Page/screen transitions should maintain spatial continuity (shared element, directional slide) (Apple HIG) +- `parallax-subtle` - Use parallax sparingly; must respect reduced-motion and not cause disorientation (Apple HIG) +- `spring-physics` - Prefer spring/physics-based curves over linear or cubic-bezier for natural feel (Apple HIG fluid animations) +- `exit-faster-than-enter` - Exit animations shorter than enter (~60–70% of enter duration) to feel responsive (MD motion) +- `stagger-sequence` - Stagger list/grid item entrance by 30–50ms per item; avoid all-at-once or too-slow reveals (MD) +- `shared-element-transition` - Use shared element / hero transitions for visual continuity between screens (MD, HIG) +- `interruptible` - Animations must be interruptible; user tap/gesture cancels in-progress animation immediately (Apple HIG) +- `no-blocking-animation` - Never block user input during an animation; UI must stay interactive (Apple HIG) +- `fade-crossfade` - Use crossfade for content replacement within the same container (MD) +- `scale-feedback` - Subtle scale (0.95–1.05) on press for tappable cards/buttons; restore on release (HIG, MD) +- `gesture-feedback` - Drag, swipe, and pinch must provide real-time visual response tracking the finger (MD Motion) +- `hierarchy-motion` - Use translate/scale direction to express hierarchy: enter from below = deeper, exit upward = back (MD) +- `motion-consistency` - Unify duration/easing tokens globally; all animations share the same rhythm and feel +- `opacity-threshold` - Fading elements should not linger below opacity 0.2; either fade fully or remain visible +- `modal-motion` - Modals/sheets should animate from their trigger source (scale+fade or slide-in) for spatial context (HIG, MD) +- `navigation-direction` - Forward navigation animates left/up; backward animates right/down — keep direction logically consistent (HIG) +- `layout-shift-avoid` - Animations must not cause layout reflow or CLS; use transform for position changes + +### 8. Forms & Feedback (MEDIUM) + +- `input-labels` - Visible label per input (not placeholder-only) +- `error-placement` - Show error below the related field +- `submit-feedback` - Loading then success/error state on submit +- `required-indicators` - Mark required fields (e.g. asterisk) +- `empty-states` - Helpful message and action when no content +- `toast-dismiss` - Auto-dismiss toasts in 3-5s +- `confirmation-dialogs` - Confirm before destructive actions +- `input-helper-text` - Provide persistent helper text below complex inputs, not just placeholder (Material Design) +- `disabled-states` - Disabled elements use reduced opacity (0.38–0.5) + cursor change + semantic attribute (MD) +- `progressive-disclosure` - Reveal complex options progressively; don't overwhelm users upfront (Apple HIG) +- `inline-validation` - Validate on blur (not keystroke); show error only after user finishes input (MD) +- `input-type-keyboard` - Use semantic input types (email, tel, number) to trigger the correct mobile keyboard (HIG, MD) +- `password-toggle` - Provide show/hide toggle for password fields (MD) +- `autofill-support` - Use autocomplete / textContentType attributes so the system can autofill (HIG, MD) +- `undo-support` - Allow undo for destructive or bulk actions (e.g. "Undo delete" toast) (Apple HIG) +- `success-feedback` - Confirm completed actions with brief visual feedback (checkmark, toast, color flash) (MD) +- `error-recovery` - Error messages must include a clear recovery path (retry, edit, help link) (HIG, MD) +- `multi-step-progress` - Multi-step flows show step indicator or progress bar; allow back navigation (MD) +- `form-autosave` - Long forms should auto-save drafts to prevent data loss on accidental dismissal (Apple HIG) +- `sheet-dismiss-confirm` - Confirm before dismissing a sheet/modal with unsaved changes (Apple HIG) +- `error-clarity` - Error messages must state cause + how to fix (not just "Invalid input") (HIG, MD) +- `field-grouping` - Group related fields logically (fieldset/legend or visual grouping) (MD) +- `read-only-distinction` - Read-only state should be visually and semantically different from disabled (MD) +- `focus-management` - After submit error, auto-focus the first invalid field (WCAG, MD) +- `error-summary` - For multiple errors, show summary at top with anchor links to each field (WCAG) +- `touch-friendly-input` - Mobile input height ≥44px to meet touch target requirements (Apple HIG) +- `destructive-emphasis` - Destructive actions use semantic danger color (red) and are visually separated from primary actions (HIG, MD) +- `toast-accessibility` - Toasts must not steal focus; use aria-live="polite" for screen reader announcement (WCAG) +- `aria-live-errors` - Form errors use aria-live region or role="alert" to notify screen readers (WCAG) +- `contrast-feedback` - Error and success state colors must meet 4.5:1 contrast ratio (WCAG, MD) +- `timeout-feedback` - Request timeout must show clear feedback with retry option (MD) + +### 9. Navigation Patterns (HIGH) + +- `bottom-nav-limit` - Bottom navigation max 5 items; use labels with icons (Material Design) +- `drawer-usage` - Use drawer/sidebar for secondary navigation, not primary actions (Material Design) +- `back-behavior` - Back navigation must be predictable and consistent; preserve scroll/state (Apple HIG, MD) +- `deep-linking` - All key screens must be reachable via deep link / URL for sharing and notifications (Apple HIG, MD) +- `tab-bar-ios` - iOS: use bottom Tab Bar for top-level navigation (Apple HIG) +- `top-app-bar-android` - Android: use Top App Bar with navigation icon for primary structure (Material Design) +- `nav-label-icon` - Navigation items must have both icon and text label; icon-only nav harms discoverability (MD) +- `nav-state-active` - Current location must be visually highlighted (color, weight, indicator) in navigation (HIG, MD) +- `nav-hierarchy` - Primary nav (tabs/bottom bar) vs secondary nav (drawer/settings) must be clearly separated (MD) +- `modal-escape` - Modals and sheets must offer a clear close/dismiss affordance; swipe-down to dismiss on mobile (Apple HIG) +- `search-accessible` - Search must be easily reachable (top bar or tab); provide recent/suggested queries (MD) +- `breadcrumb-web` - Web: use breadcrumbs for 3+ level deep hierarchies to aid orientation (MD) +- `state-preservation` - Navigating back must restore previous scroll position, filter state, and input (HIG, MD) +- `gesture-nav-support` - Support system gesture navigation (iOS swipe-back, Android predictive back) without conflict (HIG, MD) +- `tab-badge` - Use badges on nav items sparingly to indicate unread/pending; clear after user visits (HIG, MD) +- `overflow-menu` - When actions exceed available space, use overflow/more menu instead of cramming (MD) +- `bottom-nav-top-level` - Bottom nav is for top-level screens only; never nest sub-navigation inside it (MD) +- `adaptive-navigation` - Large screens (≥1024px) prefer sidebar; small screens use bottom/top nav (Material Adaptive) +- `back-stack-integrity` - Never silently reset the navigation stack or unexpectedly jump to home (HIG, MD) +- `navigation-consistency` - Navigation placement must stay the same across all pages; don't change by page type +- `avoid-mixed-patterns` - Don't mix Tab + Sidebar + Bottom Nav at the same hierarchy level +- `modal-vs-navigation` - Modals must not be used for primary navigation flows; they break the user's path (HIG) +- `focus-on-route-change` - After page transition, move focus to main content region for screen reader users (WCAG) +- `persistent-nav` - Core navigation must remain reachable from deep pages; don't hide it entirely in sub-flows (HIG, MD) +- `destructive-nav-separation` - Dangerous actions (delete account, logout) must be visually and spatially separated from normal nav items (HIG, MD) +- `empty-nav-state` - When a nav destination is unavailable, explain why instead of silently hiding it (MD) + +### 10. Charts & Data (LOW) + +- `chart-type` - Match chart type to data type (trend → line, comparison → bar, proportion → pie/donut) +- `color-guidance` - Use accessible color palettes; avoid red/green only pairs for colorblind users (WCAG, MD) +- `data-table` - Provide table alternative for accessibility; charts alone are not screen-reader friendly (WCAG) +- `pattern-texture` - Supplement color with patterns, textures, or shapes so data is distinguishable without color (WCAG, MD) +- `legend-visible` - Always show legend; position near the chart, not detached below a scroll fold (MD) +- `tooltip-on-interact` - Provide tooltips/data labels on hover (Web) or tap (mobile) showing exact values (HIG, MD) +- `axis-labels` - Label axes with units and readable scale; avoid truncated or rotated labels on mobile +- `responsive-chart` - Charts must reflow or simplify on small screens (e.g. horizontal bar instead of vertical, fewer ticks) +- `empty-data-state` - Show meaningful empty state when no data exists ("No data yet" + guidance), not a blank chart (MD) +- `loading-chart` - Use skeleton or shimmer placeholder while chart data loads; don't show an empty axis frame +- `animation-optional` - Chart entrance animations must respect prefers-reduced-motion; data should be readable immediately (HIG) +- `large-dataset` - For 1000+ data points, aggregate or sample; provide drill-down for detail instead of rendering all (MD) +- `number-formatting` - Use locale-aware formatting for numbers, dates, currencies on axes and labels (HIG, MD) +- `touch-target-chart` - Interactive chart elements (points, segments) must have ≥44pt tap area or expand on touch (Apple HIG) +- `no-pie-overuse` - Avoid pie/donut for >5 categories; switch to bar chart for clarity +- `contrast-data` - Data lines/bars vs background ≥3:1; data text labels ≥4.5:1 (WCAG) +- `legend-interactive` - Legends should be clickable to toggle series visibility (MD) +- `direct-labeling` - For small datasets, label values directly on the chart to reduce eye travel +- `tooltip-keyboard` - Tooltip content must be keyboard-reachable and not rely on hover alone (WCAG) +- `sortable-table` - Data tables must support sorting with aria-sort indicating current sort state (WCAG) +- `axis-readability` - Axis ticks must not be cramped; maintain readable spacing, auto-skip on small screens +- `data-density` - Limit information density per chart to avoid cognitive overload; split into multiple charts if needed +- `trend-emphasis` - Emphasize data trends over decoration; avoid heavy gradients/shadows that obscure the data +- `gridline-subtle` - Grid lines should be low-contrast (e.g. gray-200) so they don't compete with data +- `focusable-elements` - Interactive chart elements (points, bars, slices) must be keyboard-navigable (WCAG) +- `screen-reader-summary` - Provide a text summary or aria-label describing the chart's key insight for screen readers (WCAG) +- `error-state-chart` - Data load failure must show error message with retry action, not a broken/empty chart +- `export-option` - For data-heavy products, offer CSV/image export of chart data +- `drill-down-consistency` - Drill-down interactions must maintain a clear back-path and hierarchy breadcrumb +- `time-scale-clarity` - Time series charts must clearly label time granularity (day/week/month) and allow switching + +## How to Use + +Search specific domains using the CLI tool below. + +--- + +## Prerequisites + +Check if Python is installed: + +```bash +python3 --version || python --version +``` + +If Python is not installed, install it based on user's OS: + +**macOS:** +```bash +brew install python3 +``` + +**Ubuntu/Debian:** +```bash +sudo apt update && sudo apt install python3 +``` + +**Windows:** +```powershell +winget install Python.Python.3.12 +``` + +> **Note:** On Windows, use `python` instead of `python3` to run scripts (e.g., `python scripts/search.py` instead of `python3 scripts/search.py`). + +--- + +## How to Use This Skill + +Use this skill when the user requests any of the following: + +| Scenario | Trigger Examples | Start From | +|----------|-----------------|------------| +| **New project / page** | "Build a landing page", "Build a dashboard" | Step 1 → Step 2 (design system) | +| **New component** | "Create a pricing card", "Add a modal" | Step 3 (domain search: style, ux) | +| **Choose style / color / font** | "What style fits a fintech app?", "Recommend a color palette" | Step 2 (design system) | +| **Review existing UI** | "Review this page for UX issues", "Check accessibility" | Quick Reference checklist above | +| **Fix a UI bug** | "Button hover is broken", "Layout shifts on load" | Quick Reference → relevant section | +| **Improve / optimize** | "Make this faster", "Improve mobile experience" | Step 3 (domain search: ux, react) | +| **Implement dark mode** | "Add dark mode support" | Step 3 (domain: style "dark mode") | +| **Add charts / data viz** | "Add an analytics dashboard chart" | Step 3 (domain: chart) | +| **Stack best practices** | "React performance tips"、"SwiftUI navigation" | Step 4 (stack search) | + +Follow this workflow: + +### Step 1: Analyze User Requirements + +Extract key information from user request: +- **Product type**: Entertainment (social, video, music, gaming), Tool (scanner, editor, converter), Productivity (task manager, notes, calendar), or hybrid +- **Target audience**: C-end consumer users; consider age group, usage context (commute, leisure, work) +- **Style keywords**: playful, vibrant, minimal, dark mode, content-first, immersive, etc. +- **Stack**: Match the project's framework. The engine ships guidance for many stacks (see [Available Stacks](#available-stacks) below) — pass the matching `--stack` (e.g. `nextjs`, `react`, `shadcn`, `vue`, `svelte`, `astro`, `swiftui`, `flutter`, `react-native`). + +### Step 2: Generate Design System (REQUIRED) + +**Always start with `--design-system`** to get comprehensive recommendations with reasoning: + +```bash +python3 skills/ui-ux-pro-max/scripts/search.py " " --design-system [-p "Project Name"] +``` + +This command: +1. Searches domains in parallel (product, style, color, landing, typography) +2. Applies reasoning rules from `ui-reasoning.csv` to select best matches +3. Returns complete design system: pattern, style, colors, typography, effects +4. Includes anti-patterns to avoid + +**Example:** +```bash +python3 skills/ui-ux-pro-max/scripts/search.py "beauty spa wellness service" --design-system -p "Serenity Spa" +``` + +### Step 2b: Persist Design System (Master + Overrides Pattern) + +To save the design system for **hierarchical retrieval across sessions**, add `--persist`: + +```bash +python3 skills/ui-ux-pro-max/scripts/search.py "" --design-system --persist -p "Project Name" +``` + +This creates: +- `design-system/MASTER.md` — Global Source of Truth with all design rules +- `design-system/pages/` — Folder for page-specific overrides + +**With page-specific override:** +```bash +python3 skills/ui-ux-pro-max/scripts/search.py "" --design-system --persist -p "Project Name" --page "dashboard" +``` + +This also creates: +- `design-system/pages/dashboard.md` — Page-specific deviations from Master + +**How hierarchical retrieval works:** +1. When building a specific page (e.g., "Checkout"), first check `design-system/pages/checkout.md` +2. If the page file exists, its rules **override** the Master file +3. If not, use `design-system/MASTER.md` exclusively + +**Context-aware retrieval prompt:** +``` +I am building the [Page Name] page. Please read design-system/MASTER.md. +Also check if design-system/pages/[page-name].md exists. +If the page file exists, prioritize its rules. +If not, use the Master rules exclusively. +Now, generate the code... +``` + +### Step 2c: Design Dials (optional) + +Three optional 1-10 sliders that tune `--design-system` output without changing your query. Add any combination of them to the same command: + +```bash +python3 skills/ui-ux-pro-max/scripts/search.py "" --design-system --variance <1-10> --motion <1-10> --density <1-10> +``` + +| Dial | Low (1-3) | Mid (4-7) | High (8-10) | +|------|-----------|-----------|-------------| +| `--variance` | Centered / minimal (biases toward Minimalism-style categories) | Balanced / modern | Bold / asymmetric (biases toward Brutalism, Bento Grids) | +| `--motion` | Subtle micro-interactions | Standard scroll/stagger motion | Complex choreography (pin, Flip, SplitText) | +| `--density` | Spacious (24-96px spacing scale) | Standard (16-64px, current default) | Dense/dashboard (8-32px spacing scale) | + +- `--motion` attaches a ready-to-use GSAP snippet (with framework notes, Do/Don't, and performance notes) pulled from `--domain gsap`, matched to the resolved tier (Subtle/Standard/Complex). +- `--density` overrides the `--space-*` CSS variable table in the ASCII/markdown/MASTER.md output — use it for dashboards (high) vs. marketing pages (low) without hand-editing tokens. +- Leaving a dial unset keeps that part of the output exactly as it was before (no behavior change). + +**Example:** +```bash +python3 skills/ui-ux-pro-max/scripts/search.py "internal analytics dashboard" --design-system --variance 8 --motion 7 --density 8 -p "Ops Console" +``` + +### Step 3: Supplement with Detailed Searches (as needed) + +After getting the design system, use domain searches to get additional details: + +```bash +python3 skills/ui-ux-pro-max/scripts/search.py "" --domain [-n ] +``` + +**When to use detailed searches:** + +| Need | Domain | Example | +|------|--------|---------| +| Product type patterns | `product` | `--domain product "entertainment social"` | +| More style options | `style` | `--domain style "glassmorphism dark"` | +| Color palettes | `color` | `--domain color "entertainment vibrant"` | +| Font pairings | `typography` | `--domain typography "playful modern"` | +| Chart recommendations | `chart` | `--domain chart "real-time dashboard"` | +| UX best practices | `ux` | `--domain ux "animation accessibility"` | +| Alternative fonts | `typography` | `--domain typography "elegant luxury"` | +| Individual Google Fonts | `google-fonts` | `--domain google-fonts "sans serif popular variable"` | +| Landing structure | `landing` | `--domain landing "hero social-proof"` | +| React Native perf | `react` | `--domain react "rerender memo list"` | +| App interface a11y | `web` | `--domain web "accessibilityLabel touch safe-areas"` | +| AI prompt / CSS keywords | `prompt` | `--domain prompt "minimalism"` | + +### Step 4: Stack Guidelines (match your framework) + +Get implementation-specific best practices for the stack you're building in. +Pass the `--stack` that matches the project's framework: + +```bash +python3 skills/ui-ux-pro-max/scripts/search.py "" --stack +# e.g. --stack nextjs | react | shadcn | vue | svelte | astro | swiftui | flutter | react-native +``` + +--- + +## Search Reference + +### Available Domains + +| Domain | Use For | Example Keywords | +|--------|---------|------------------| +| `product` | Product type recommendations | SaaS, e-commerce, portfolio, healthcare, beauty, service | +| `style` | UI styles, colors, effects | glassmorphism, minimalism, dark mode, brutalism | +| `typography` | Font pairings, Google Fonts | elegant, playful, professional, modern | +| `color` | Color palettes by product type | saas, ecommerce, healthcare, beauty, fintech, service | +| `landing` | Page structure, CTA strategies | hero, hero-centric, testimonial, pricing, social-proof | +| `chart` | Chart types, library recommendations | trend, comparison, timeline, funnel, pie | +| `ux` | Best practices, anti-patterns | animation, accessibility, z-index, loading | +| `gsap` | GSAP animation skeletons by intensity tier | scroll reveal, stagger, magnetic cursor, page transition | +| `google-fonts` | Individual Google Fonts lookup | sans serif, monospace, japanese, variable font, popular | +| `react` | React/Next.js performance | waterfall, bundle, suspense, memo, rerender, cache | +| `web` | App interface guidelines (iOS/Android/React Native) | accessibilityLabel, touch targets, safe areas, Dynamic Type | +| `prompt` | AI prompts, CSS keywords | (style name) | + +### Available Stacks + +Run `ls /data/stacks/` to see the live set. Shipped stacks: + +| Stack | Focus | +|-------|-------| +| `react` | Components, hooks, render performance | +| `nextjs` | App Router, RSC, Server Actions, rendering | +| `vue` | Components, Composition API, reactivity | +| `nuxtjs` | Nuxt app patterns, SSR data fetching | +| `nuxt-ui` | Nuxt UI component patterns | +| `svelte` | Components, stores, transitions | +| `astro` | Islands, content, partial hydration | +| `shadcn` | shadcn/ui primitives, composition | +| `html-tailwind` | Tailwind utility patterns | +| `angular` | Components, signals, services | +| `laravel` | Blade / server-rendered UI patterns | +| `swiftui` | Views, state, navigation (iOS/macOS) | +| `flutter` | Widgets, state, navigation | +| `jetpack-compose` | Composables, state, navigation (Android) | +| `react-native` | Components, Navigation, Lists | +| `threejs` | 3D scenes, materials, performance | + +--- + +## Example Workflow + +**User request:** "Make an AI search homepage." + +### Step 1: Analyze Requirements +- Product type: Tool (AI search engine) +- Target audience: C-end users looking for fast, intelligent search +- Style keywords: modern, minimal, content-first, dark mode +- Stack: Next.js (a homepage is a web surface; use a web `--stack`) + +### Step 2: Generate Design System (REQUIRED) + +```bash +python3 skills/ui-ux-pro-max/scripts/search.py "AI search tool modern minimal" --design-system -p "AI Search" +``` + +**Output:** Complete design system with pattern, style, colors, typography, effects, and anti-patterns. + +### Step 3: Supplement with Detailed Searches (as needed) + +```bash +# Get style options for a modern tool product +python3 skills/ui-ux-pro-max/scripts/search.py "minimalism dark mode" --domain style + +# Get UX best practices for search interaction and loading +python3 skills/ui-ux-pro-max/scripts/search.py "search loading animation" --domain ux +``` + +### Step 4: Stack Guidelines + +```bash +python3 skills/ui-ux-pro-max/scripts/search.py "list performance navigation" --stack nextjs +``` + +**Then:** Synthesize design system + detailed searches and implement the design. + +--- + +## Output Formats + +The `--design-system` flag supports two output formats: + +```bash +# ASCII box (default) - best for terminal display +python3 skills/ui-ux-pro-max/scripts/search.py "fintech crypto" --design-system + +# Markdown - best for documentation +python3 skills/ui-ux-pro-max/scripts/search.py "fintech crypto" --design-system -f markdown +``` + +--- + +## Tips for Better Results + +### Query Strategy + +- Use **multi-dimensional keywords** — combine product + industry + tone + density: `"entertainment social vibrant content-dense"` not just `"app"` +- Try different keywords for the same need: `"playful neon"` → `"vibrant dark"` → `"content-first minimal"` +- Use `--design-system` first for full recommendations, then `--domain` to deep-dive any dimension you're unsure about +- Add the `--stack` that matches the project's framework for implementation-specific guidance + +### Common Sticking Points + +| Problem | What to Do | +|---------|------------| +| Can't decide on style/color | Re-run `--design-system` with different keywords | +| Dark mode contrast issues | Quick Reference §6: `color-dark-mode` + `color-accessible-pairs` | +| Animations feel unnatural | Quick Reference §7: `spring-physics` + `easing` + `exit-faster-than-enter` | +| Form UX is poor | Quick Reference §8: `inline-validation` + `error-clarity` + `focus-management` | +| Navigation feels confusing | Quick Reference §9: `nav-hierarchy` + `bottom-nav-limit` + `back-behavior` | +| Layout breaks on small screens | Quick Reference §5: `mobile-first` + `breakpoint-consistency` | +| Performance / jank | Quick Reference §3: `virtualize-lists` + `main-thread-budget` + `debounce-throttle` | + +### Pre-Delivery Checklist + +- Run `--domain ux "animation accessibility z-index loading"` as a UX validation pass before implementation +- Run through Quick Reference **§1–§3** (CRITICAL + HIGH) as a final review +- Test on 375px (small phone) and landscape orientation +- Verify behavior with **reduced-motion** enabled and **Dynamic Type** at largest size +- Check dark mode contrast independently (don't assume light mode values work) +- Confirm all touch targets ≥44pt and no content hidden behind safe areas + +--- + +## Common Rules for Professional UI + +These are frequently overlooked issues that make UI look unprofessional: +Scope notice: The rules below are for App UI (iOS/Android/React Native/Flutter), not desktop-web interaction patterns. + +### Icons & Visual Elements + +| Rule | Standard | Avoid | Why It Matters | +|------|----------|--------|----------------| +| **No Emoji as Structural Icons** | Use vector-based icons (e.g., Lucide, react-native-vector-icons, @expo/vector-icons). | Using emojis (🎨 🚀 ⚙️) for navigation, settings, or system controls. | Emojis are font-dependent, inconsistent across platforms, and cannot be controlled via design tokens. | +| **Vector-Only Assets** | Use SVG or platform vector icons that scale cleanly and support theming. | Raster PNG icons that blur or pixelate. | Ensures scalability, crisp rendering, and dark/light mode adaptability. | +| **Stable Interaction States** | Use color, opacity, or elevation transitions for press states without changing layout bounds. | Layout-shifting transforms that move surrounding content or trigger visual jitter. | Prevents unstable interactions and preserves smooth motion/perceived quality on mobile. | +| **Correct Brand Logos** | Use official brand assets and follow their usage guidelines (spacing, color, clear space). | Guessing logo paths, recoloring unofficially, or modifying proportions. | Prevents brand misuse and ensures legal/platform compliance. | +| **Consistent Icon Sizing** | Define icon sizes as design tokens (e.g., icon-sm, icon-md = 24pt, icon-lg). | Mixing arbitrary values like 20pt / 24pt / 28pt randomly. | Maintains rhythm and visual hierarchy across the interface. | +| **Stroke Consistency** | Use a consistent stroke width within the same visual layer (e.g., 1.5px or 2px). | Mixing thick and thin stroke styles arbitrarily. | Inconsistent strokes reduce perceived polish and cohesion. | +| **Filled vs Outline Discipline** | Use one icon style per hierarchy level. | Mixing filled and outline icons at the same hierarchy level. | Maintains semantic clarity and stylistic coherence. | +| **Touch Target Minimum** | Minimum 44×44pt interactive area (use hitSlop if icon is smaller). | Small icons without expanded tap area. | Meets accessibility and platform usability standards. | +| **Icon Alignment** | Align icons to text baseline and maintain consistent padding. | Misaligned icons or inconsistent spacing around them. | Prevents subtle visual imbalance that reduces perceived quality. | +| **Icon Contrast** | Follow WCAG contrast standards: 4.5:1 for small elements, 3:1 minimum for larger UI glyphs. | Low-contrast icons that blend into the background. | Ensures accessibility in both light and dark modes. | + + +### Interaction (App) + +| Rule | Do | Don't | +|------|----|----- | +| **Tap feedback** | Provide clear pressed feedback (ripple/opacity/elevation) within 80-150ms | No visual response on tap | +| **Animation timing** | Keep micro-interactions around 150-300ms with platform-native easing | Instant transitions or slow animations (>500ms) | +| **Accessibility focus** | Ensure screen reader focus order matches visual order and labels are descriptive | Unlabeled controls or confusing focus traversal | +| **Disabled state clarity** | Use disabled semantics (`disabled`/native disabled props), reduced emphasis, and no tap action | Controls that look tappable but do nothing | +| **Touch target minimum** | Keep tap areas >=44x44pt (iOS) or >=48x48dp (Android), expand hit area when icon is smaller | Tiny tap targets or icon-only hit areas without padding | +| **Gesture conflict prevention** | Keep one primary gesture per region and avoid nested tap/drag conflicts | Overlapping gestures causing accidental actions | +| **Semantic native controls** | Prefer native interactive primitives (`Button`, `Pressable`, platform equivalents) with proper accessibility roles | Generic containers used as primary controls without semantics | + +### Light/Dark Mode Contrast + +| Rule | Do | Don't | +|------|----|----- | +| **Surface readability (light)** | Keep cards/surfaces clearly separated from background with sufficient opacity/elevation | Overly transparent surfaces that blur hierarchy | +| **Text contrast (light)** | Maintain body text contrast >=4.5:1 against light surfaces | Low-contrast gray body text | +| **Text contrast (dark)** | Maintain primary text contrast >=4.5:1 and secondary text >=3:1 on dark surfaces | Dark mode text that blends into background | +| **Border and divider visibility** | Ensure separators are visible in both themes (not just light mode) | Theme-specific borders disappearing in one mode | +| **State contrast parity** | Keep pressed/focused/disabled states equally distinguishable in light and dark themes | Defining interaction states for one theme only | +| **Token-driven theming** | Use semantic color tokens mapped per theme across app surfaces/text/icons | Hardcoded per-screen hex values | +| **Scrim and modal legibility** | Use a modal scrim strong enough to isolate foreground content (typically 40-60% black) | Weak scrim that leaves background visually competing | + +### Layout & Spacing + +| Rule | Do | Don't | +|------|----|----- | +| **Safe-area compliance** | Respect top/bottom safe areas for all fixed headers, tab bars, and CTA bars | Placing fixed UI under notch, status bar, or gesture area | +| **System bar clearance** | Add spacing for status/navigation bars and gesture home indicator | Let tappable content collide with OS chrome | +| **Consistent content width** | Keep predictable content width per device class (phone/tablet) | Mixing arbitrary widths between screens | +| **8dp spacing rhythm** | Use a consistent 4/8dp spacing system for padding/gaps/section spacing | Random spacing increments with no rhythm | +| **Readable text measure** | Keep long-form text readable on large devices (avoid edge-to-edge paragraphs on tablets) | Full-width long text that hurts readability | +| **Section spacing hierarchy** | Define clear vertical rhythm tiers (e.g., 16/24/32/48) by hierarchy | Similar UI levels with inconsistent spacing | +| **Adaptive gutters by breakpoint** | Increase horizontal insets on larger widths and in landscape | Same narrow gutter on all device sizes/orientations | +| **Scroll and fixed element coexistence** | Add bottom/top content insets so lists are not hidden behind fixed bars | Scroll content obscured by sticky headers/footers | + +--- + +## Pre-Delivery Checklist + +Before delivering UI code, verify these items: +Scope notice: This checklist is for App UI (iOS/Android/React Native/Flutter). + +### Visual Quality +- [ ] No emojis used as icons (use SVG instead) +- [ ] All icons come from a consistent icon family and style +- [ ] Official brand assets are used with correct proportions and clear space +- [ ] Pressed-state visuals do not shift layout bounds or cause jitter +- [ ] Semantic theme tokens are used consistently (no ad-hoc per-screen hardcoded colors) + +### Interaction +- [ ] All tappable elements provide clear pressed feedback (ripple/opacity/elevation) +- [ ] Touch targets meet minimum size (>=44x44pt iOS, >=48x48dp Android) +- [ ] Micro-interaction timing stays in the 150-300ms range with native-feeling easing +- [ ] Disabled states are visually clear and non-interactive +- [ ] Screen reader focus order matches visual order, and interactive labels are descriptive +- [ ] Gesture regions avoid nested/conflicting interactions (tap/drag/back-swipe conflicts) + +### Light/Dark Mode +- [ ] Primary text contrast >=4.5:1 in both light and dark mode +- [ ] Secondary text contrast >=3:1 in both light and dark mode +- [ ] Dividers/borders and interaction states are distinguishable in both modes +- [ ] Modal/drawer scrim opacity is strong enough to preserve foreground legibility (typically 40-60% black) +- [ ] Both themes are tested before delivery (not inferred from a single theme) + +### Layout +- [ ] Safe areas are respected for headers, tab bars, and bottom CTA bars +- [ ] Scroll content is not hidden behind fixed/sticky bars +- [ ] Verified on small phone, large phone, and tablet (portrait + landscape) +- [ ] Horizontal insets/gutters adapt correctly by device size and orientation +- [ ] 4/8dp spacing rhythm is maintained across component, section, and page levels +- [ ] Long-form text measure remains readable on larger devices (no edge-to-edge paragraphs) + +### Accessibility +- [ ] All meaningful images/icons have accessibility labels +- [ ] Form fields have labels, hints, and clear error messages +- [ ] Color is not the only indicator +- [ ] Reduced motion and dynamic text size are supported without layout breakage +- [ ] Accessibility traits/roles/states (selected, disabled, expanded) are announced correctly + +--- +_Imported into astroagent from **ui-ux-pro-max-skill** by nextlevelbuilder_ +_(github.com/nextlevelbuilder/ui-ux-pro-max-skill). The searchable CSV/script_ +_engine is omitted (no shell in the sandbox); this is the guidance layer._ diff --git a/app/.github/FUNDING.yml b/app/.github/FUNDING.yml new file mode 100644 index 0000000..489e677 --- /dev/null +++ b/app/.github/FUNDING.yml @@ -0,0 +1 @@ +ko_fi: andreialba diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..7144365 --- /dev/null +++ b/app/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +.astro +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/app/.prettierignore b/app/.prettierignore new file mode 100644 index 0000000..4c7d4aa --- /dev/null +++ b/app/.prettierignore @@ -0,0 +1,5 @@ +node_modules +dist +.astro +pnpm-lock.yaml +package-lock.json diff --git a/app/.prettierrc b/app/.prettierrc new file mode 100644 index 0000000..90abee2 --- /dev/null +++ b/app/.prettierrc @@ -0,0 +1,6 @@ +{ + "printWidth": 100, + "semi": true, + "singleQuote": false, + "trailingComma": "all" +} diff --git a/app/AGENTS.md b/app/AGENTS.md new file mode 100644 index 0000000..049ce03 --- /dev/null +++ b/app/AGENTS.md @@ -0,0 +1,241 @@ +# Instructions for Astro Theme Development + +These instructions apply to all Astro theme work. Prioritize clean, reusable, accessible, fast, SEO-friendly code. Treat the theme as something that may be reused across multiple websites, not as a one-off implementation. + +## General Principles + +* Prefer simple, maintainable Astro components over unnecessary abstractions. +* Keep the default Astro advantage: mostly static HTML, minimal JavaScript, and hydration only where needed. +* Do not add client-side JavaScript unless there is a clear user-facing reason. +* Avoid unnecessary dependencies. Before adding a package, check whether the same result can be achieved with Astro, HTML, CSS, or a small utility. +* Keep components reusable, documented, and easy to override. +* Use TypeScript where helpful, especially for props, content schemas, config objects, and reusable utilities. +* Favor progressive enhancement. The site should remain usable even if JavaScript fails. +* Keep markup clean, semantic, and easy to crawl. +* Never solve layout or behavior problems in a way that harms accessibility, SEO, or performance. +* Follow README layout as here https://github.com/andreialba/maria must include the title, preview image with a link to the preview URL, those cards with versions, preview link, short description of the theme, list of features, and how to set things up. +* Add MIT license under Andrei Alba + +## Astro-Specific Guidelines + +* Use `.astro` components for static and content-focused UI. +* Use islands/client hydration only when interactivity is required. +* Avoid `client:load` unless the component truly needs to run immediately. +* Prefer `client:visible`, `client:idle`, or no hydration when possible. +* Keep layout components responsible for page structure, shared metadata, global slots, and theme-level wrappers. +* Keep UI components small and focused. +* Use `Astro.props` with typed props where possible. +* Use content collections for structured content like posts, pages, projects, docs, testimonials, FAQs, and changelogs. +* Validate frontmatter with schemas instead of relying on loose optional fields. +* Keep route structure clean and predictable. +* Do not hardcode production URLs inside components. Use site config, constants, or environment-aware helpers. +* Make sure the theme works with a configurable `site` value in `astro.config.*`. + +## Accessibility Requirements + +* Use semantic HTML first. Do not use ARIA when a native HTML element solves the problem. +* Use landmarks properly: `header`, `nav`, `main`, `section`, `article`, `aside`, and `footer` where appropriate. +* Each page should have one clear `h1`. +* Preserve logical heading order. Do not skip heading levels for visual styling. +* All interactive elements must be keyboard accessible. +* Use real buttons for actions and real links for navigation. +* Every form control must have an associated label. +* Inputs, errors, help text, and validation states must be understandable to screen readers. +* Add visible focus styles. Never remove outlines without replacing them with an accessible focus state. +* Provide a skip link for keyboard users when the layout has repeated navigation. +* Use descriptive link text. Avoid vague text like “click here” or “read more” without context. +* Images must have useful `alt` text when meaningful. +* Decorative images should use empty alt text. +* Icons used as buttons or links must have accessible names. +* Ensure sufficient color contrast for text, icons, borders, and states. +* Do not rely on color alone to communicate meaning. +* Respect `prefers-reduced-motion`. +* Avoid auto-playing motion, carousels, or animations unless they are user-controlled and accessible. +* Modals, menus, accordions, tabs, dropdowns, and mobile navigation must handle focus, keyboard interaction, and escape/close behavior correctly. +* Test important templates with keyboard navigation and screen reader-friendly markup in mind. + +## SEO Requirements + +* Every page should have a unique, descriptive ``. +* Every indexable page should have a useful meta description. +* Use a reusable SEO or Head component for metadata. +* Include canonical URLs where appropriate. +* Support Open Graph metadata for social sharing. +* Support Twitter/X card metadata where appropriate. +* Use absolute URLs for canonical and social image URLs. +* Configure `site` in `astro.config.*` so canonical URLs and sitemap generation work correctly. +* Include sitemap support for production themes. +* Include sensible robots handling. +* Avoid duplicate metadata across pages. +* Avoid duplicate content caused by inconsistent trailing slashes, canonical paths, or pagination. +* Use clean, descriptive URLs. +* Add structured data where useful, such as `WebSite`, `Organization`, `Article`, `BreadcrumbList`, `Product`, `FAQPage`, or `LocalBusiness`, depending on the theme. +* Do not add fake schema data. Structured data must match visible page content. +* Use proper heading structure to reflect the content hierarchy. +* Ensure important content is present in the HTML, not hidden behind client-only rendering. +* Use descriptive image filenames where possible. +* Add alt text and dimensions for content images. +* Include pagination metadata where relevant. +* Support multilingual SEO only when the theme actually supports multiple languages. If it does, include proper `lang`, canonical, and alternate/hreflang handling. +* Keep internal links crawlable with real `<a href="">` links. +* Avoid JavaScript-only navigation for normal pages. + +## Performance Requirements + +* Keep JavaScript minimal. +* Avoid shipping framework runtime code unless needed. +* Hydrate components selectively. +* Prefer static rendering where possible. +* Avoid large global scripts. +* Avoid large CSS bundles. +* Keep CSS scoped, layered, or organized in a predictable way. +* Remove unused CSS and unused components. +* Optimize images with Astro’s image tools where appropriate. +* Always include image width and height to reduce layout shift. +* Use responsive images for large visual assets. +* Lazy-load below-the-fold images. +* Do not lazy-load critical above-the-fold hero images unless there is a good reason. +* Use modern image formats when appropriate. +* Avoid layout shifts from images, ads, embeds, cookie banners, and late-loading UI. +* Keep third-party scripts optional and documented. +* Load analytics, embeds, chat widgets, and marketing scripts only when explicitly enabled. +* Avoid blocking render with unnecessary scripts or styles. +* Keep Core Web Vitals in mind, especially LCP, CLS, and INP. + +## Font Optimization + +* Prefer self-hosted fonts for production themes. +* Use only the font families actually needed by the theme. +* Include only the font weights and styles actually used. +* Prefer modern formats such as `woff2`. +* Use `font-display: swap` or another intentional rendering strategy. +* Preload only critical fonts used above the fold. +* Do not preload every font file. +* Define fallback font stacks that closely match the custom font metrics. +* Avoid layout shift caused by late-loading fonts. +* Do not load fonts from external providers by default unless the user explicitly chooses that option. +* Keep font configuration centralized so users can replace or disable custom fonts easily. + +## CSS and Design System Guidelines + +* Use design tokens or CSS custom properties for colors, spacing, typography, radii, shadows, and layout values. +* Keep theme customization simple. +* Avoid scattering hardcoded colors and spacing values throughout components. +* Support light and dark modes only if the theme is designed for both. +* Respect user system preference when dark mode is supported. +* Ensure color tokens meet accessibility contrast requirements. +* Keep responsive behavior consistent across components. +* Use fluid and responsive typography where appropriate. +* Avoid unnecessary wrappers and deeply nested markup. +* Keep animations subtle, optional, and respectful of reduced-motion preferences. + +## Content and Markdown Guidelines + +* Content should be easy to manage through Markdown, MDX, or content collections. +* Validate required frontmatter fields. +* Provide sensible defaults for optional metadata. +* Avoid requiring users to duplicate the same SEO fields in many places when defaults can be generated safely. +* Support draft or unpublished content only when the theme explicitly needs it. +* Make dates, authors, categories, tags, and excerpts consistent. +* Make sure generated archive, tag, category, author, and pagination pages have useful metadata. +* Avoid rendering empty UI sections when content is missing. + +## Image and Media Guidelines + +* Use optimized local images where possible. +* Provide responsive sizes for theme-controlled images. +* Include `alt` text fields in content schemas where images are user-provided. +* Do not use background images for meaningful content unless an accessible text alternative exists. +* Avoid enormous default hero images. +* Provide predictable aspect ratios to prevent layout shift. +* Lazy-load media that is not immediately visible. +* Make video/audio embeds accessible with labels, captions, transcripts, or surrounding explanatory content when relevant. + +## Component Guidelines + +* Components should have clear responsibilities. +* Props should be typed and documented when not obvious. +* Use sensible defaults. +* Avoid components that silently fail or render broken markup when required props are missing. +* Avoid coupling generic components to one specific page. +* Keep class names predictable. +* Make components easy to copy, remove, or override. +* Do not introduce global side effects from small components. +* For interactive components, document keyboard behavior and accessibility expectations. + +## Forms + +* Use semantic form markup. +* Every input must have a label. +* Required fields must be indicated accessibly. +* Error messages must be connected to the relevant fields. +* Success and error states should be announced or clearly visible. +* Do not rely only on placeholder text as a label. +* Use appropriate input types such as `email`, `tel`, `url`, `search`, and `number`. +* Keep forms usable without unnecessary JavaScript where possible. +* Do not include a form provider by default unless it is configurable. + +## Navigation + +* Use real links for navigation. +* Mark the current page or section when possible. +* Ensure mobile navigation works with keyboard and screen readers. +* Trap focus only when appropriate, such as inside an open modal menu. +* Restore focus after closing menus or dialogs when relevant. +* Make dropdowns and submenus accessible. +* Do not hide navigation from assistive technology unless it is truly inactive. + +## Build, Config, and DX + +* Keep configuration centralized and documented. +* Provide clear theme constants for site name, default title, description, social links, navigation, and footer data. +* Avoid requiring users to edit many files for common changes. +* Use environment variables only where they are actually needed. +* Do not expose secrets in client-side code. +* Keep the README accurate. +* Include setup, development, build, preview, customization, and deployment instructions. +* Add comments only where they clarify non-obvious decisions. +* Keep generated examples realistic and production-friendly. +* Make sure the theme builds cleanly without warnings or broken links. + +## Testing and QA Checklist + +Before considering work complete, verify: + +* The project builds successfully. +* Pages render without console errors. +* No unnecessary client JavaScript is shipped. +* Navigation works with keyboard only. +* Focus states are visible. +* Forms have labels and accessible states. +* Images have correct alt text and dimensions. +* Metadata is present and unique per page. +* Canonical URLs are correct. +* Sitemap generation works. +* Social preview metadata is valid. +* The layout is responsive. +* Dark mode works if supported. +* Reduced motion is respected. +* Lighthouse or similar checks do not reveal obvious accessibility, SEO, or performance issues. +* There are no broken internal links. +* There is no placeholder content left in production-facing defaults. + +## Things to Avoid + +* Do not use `<div>` and `<span>` for everything when semantic HTML exists. +* Do not add ARIA roles to elements that already have correct native semantics. +* Do not remove focus outlines without accessible replacements. +* Do not add heavy animation libraries for simple transitions. +* Do not add global JavaScript for isolated UI behavior. +* Do not load all font weights “just in case.” +* Do not load external fonts by default. +* Do not use client-only rendering for content that should be crawlable. +* Do not hide important content behind JavaScript. +* Do not hardcode metadata across every page. +* Do not ship large demo assets as required production assets. +* Do not introduce dependencies without a clear reason. +* Do not sacrifice accessibility for visual polish. + +## Preferred Outcome + +The final Astro theme should be fast, accessible, SEO-ready, easy to customize, and pleasant to maintain. It should provide strong defaults while staying lightweight and flexible. diff --git a/app/CHANGELOG.md b/app/CHANGELOG.md new file mode 100644 index 0000000..4ca2862 --- /dev/null +++ b/app/CHANGELOG.md @@ -0,0 +1,20 @@ +# Changelog + +All notable changes to Quiet Pages will be documented in this file. + +## [1.0.0] - 2026-06-19 + +### Added + +- Initial public release of Quiet Pages, an Astro magazine theme for essays, field notes, blogs, and long-form editorial sites. +- Editorial homepage with a full-bleed visual lead story, featured post section, latest posts, and newsletter CTA. +- MDX blog posts powered by Astro content collections with validated frontmatter. +- Blog archive with client-side search, category filters, tag filters, and load-more pagination. +- Category, tag, and author archive pages. +- Article pages with breadcrumbs, table of contents, featured image captions, sharing links, author cards, related posts, and previous/next navigation. +- RSS feed, XML sitemap, and dynamic robots.txt route. +- SEO defaults including canonical URLs, Open Graph metadata, Twitter card metadata, and article JSON-LD. +- Light and dark mode with system preference support. +- Self-hosted Inter, Fraunces, and JetBrains Mono fonts. +- Responsive images through Astro's image pipeline. +- Accessibility defaults including semantic landmarks, skip link, visible focus styles, current-page navigation state, keyboard-friendly search/menu controls, and reduced-motion handling. \ No newline at end of file diff --git a/app/LICENSE b/app/LICENSE new file mode 100644 index 0000000..b5bdc97 --- /dev/null +++ b/app/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Andrei Alba + +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/app/README.md b/app/README.md new file mode 100644 index 0000000..ea9769f --- /dev/null +++ b/app/README.md @@ -0,0 +1,139 @@ +# QuietPages - Astro Magazine Theme + +[![Quiet Pages theme preview](./preview.webp)](https://quietpages-eta.vercel.app/) + +![Astro 6](https://img.shields.io/badge/Astro-6-ff5d01?style=for-the-badge&logo=astro&logoColor=white) +![Tailwind CSS 4](https://img.shields.io/badge/Tailwind_CSS-4-38bdf8?style=for-the-badge&logo=tailwindcss&logoColor=white) +![MDX](https://img.shields.io/badge/MDX-enabled-1b1f24?style=for-the-badge&logo=mdx&logoColor=white) +![License MIT](https://img.shields.io/badge/License-MIT-111827?style=for-the-badge) + +Preview: [quietpages-eta.vercel.app](https://quietpages-eta.vercel.app/) + +QuietPages is a calm Astro theme for independent magazines, personal journals, and long-form editorial sites. It keeps the reading experience simple and fast while including the pieces a production-ready publication needs: archives, taxonomy pages, author pages, RSS, sitemap, structured metadata, and self-hosted fonts. + +## Features + +- Editorial homepage with a full-bleed visual lead story +- Blog archive with search, category filters, tag filters, and load-more pagination +- MDX blog posts powered by Astro content collections +- Category, tag, and author index pages +- Article pages with breadcrumbs, table of contents, sharing actions, related posts, and adjacent navigation +- RSS feed, XML sitemap, and dynamic robots.txt +- Canonical URLs, Open Graph tags, Twitter card metadata, and article JSON-LD +- Light and dark modes with system preference support +- Self-hosted Inter, Fraunces, and JetBrains Mono fonts +- Accessible landmarks, visible focus states, skip link, and reduced-motion handling +- Responsive images through Astro's image pipeline +- Contact page and custom 404 page + +## Tech Stack + +- Astro 6 +- Tailwind CSS 4 via the Vite plugin +- MDX +- Astro content collections +- Self-hosted `woff2` fonts + +## Getting Started + +Install dependencies: + +```bash +npm install +``` + +Start the development server: + +```bash +npm run dev +``` + +Build for production: + +```bash +npm run build +``` + +Preview the production build locally: + +```bash +npm run preview +``` + +## Theme Setup + +The main theme settings live in [`src/lib/blog-data.js`](./src/lib/blog-data.js): + +- `SITE.name` +- `SITE.description` +- `SITE.url` +- navigation-adjacent data such as authors, categories, and tags + +Set your production URL before deploying: + +```bash +SITE_URL=https://your-domain.com +``` + +You can also use: + +```bash +PUBLIC_SITE_URL=https://your-domain.com +``` + +This keeps canonical URLs, Open Graph URLs, RSS links, robots.txt, and the sitemap aligned with the deployed domain. + +## Content + +Blog posts live in [`src/content/blog`](./src/content/blog). Each post uses an `index.mdx` file inside its own folder, with local images stored beside the content. + +Required frontmatter is validated in [`src/content.config.js`](./src/content.config.js): + +- `title` +- `excerpt` +- `date` +- `readingTime` +- `category` +- `tags` +- `author` +- `thumbnail` + +## SEO + +QuietPages includes: + +- unique page titles and descriptions +- canonical URLs generated from the configured site URL +- Open Graph and Twitter card metadata +- article JSON-LD on post pages +- XML sitemap at `/sitemap.xml` +- RSS feed at `/rss.xml` +- robots.txt with a sitemap reference + +Main SEO files: + +- [`src/layouts/BaseLayout.astro`](./src/layouts/BaseLayout.astro) +- [`src/pages/sitemap.xml.js`](./src/pages/sitemap.xml.js) +- [`src/pages/robots.txt.js`](./src/pages/robots.txt.js) +- [`src/pages/rss.xml.js`](./src/pages/rss.xml.js) + +## Images and Assets + +The repository includes [`preview.webp`](./preview.webp) for the README preview. Content images live beside each MDX post, and shared theme assets live in [`src/assets`](./src/assets). + +Fonts are self-hosted in [`public/fonts`](./public/fonts). Replace those files and the `@font-face` declarations in [`src/styles.css`](./src/styles.css) if you want a different type system. + +## Customization + +- Edit theme colors, typography tokens, radii, and prose styles in [`src/styles.css`](./src/styles.css). +- Update authors, categories, tags, and site defaults in [`src/lib/blog-data.js`](./src/lib/blog-data.js). +- Add or remove navigation items in [`src/components/Header.astro`](./src/components/Header.astro) and [`src/components/Footer.astro`](./src/components/Footer.astro). +- Replace example posts in [`src/content/blog`](./src/content/blog) with your own MDX content. + +## Deployment + +QuietPages works anywhere Astro can deploy. For Vercel, Netlify, or another static host, set `SITE_URL` to the production domain before building so metadata and feeds use absolute URLs. + +## License + +This project is licensed under the [MIT License](./LICENSE). diff --git a/app/astro.config.mjs b/app/astro.config.mjs new file mode 100644 index 0000000..bfe9230 --- /dev/null +++ b/app/astro.config.mjs @@ -0,0 +1,23 @@ +import { defineConfig } from "astro/config"; +import mdx from "@astrojs/mdx"; +import tailwindcss from "@tailwindcss/vite"; + +const site = + process.env.SITE_URL || process.env.PUBLIC_SITE_URL || "https://comiida.com"; + +export default defineConfig({ + site, + // astroagent overrides outDir for isolated preview builds (PREVIEW_OUT); + // falls back to the live output for normal/cron builds. + outDir: process.env.PREVIEW_OUT || "../public", + // For previews, PREVIEW_BASE prefixes asset paths (/_preview/<id>) so the + // preview is self-contained and doesn't pull _astro/fonts from the live site. + base: process.env.PREVIEW_BASE || undefined, + integrations: [mdx()], + vite: { + plugins: [tailwindcss()], + build: { + emptyOutDir: false, + }, + }, +}); diff --git a/app/package-lock.json b/app/package-lock.json new file mode 100644 index 0000000..75f57d2 --- /dev/null +++ b/app/package-lock.json @@ -0,0 +1,6201 @@ +{ + "name": "quiet-pages", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "quiet-pages", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "@astrojs/mdx": "^6.0.3", + "@tailwindcss/vite": "^4.3.1", + "astro": "^6.4.7", + "tailwindcss": "^4.3.1", + "tw-animate-css": "^1.4.0" + }, + "devDependencies": { + "prettier": "^3.8.4", + "vite": "^7.3.5" + } + }, + "node_modules/@astrojs/compiler": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@astrojs/compiler/-/compiler-4.0.0.tgz", + "integrity": "sha512-eouss7G8ygdZqHuke033VMcVw5HTZUu+PXd/h06DGDUg/jt5btPYPqh66ENWw/mU78rBrf/oeC4oqoBwMtDMNA==", + "license": "MIT" + }, + "node_modules/@astrojs/internal-helpers": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@astrojs/internal-helpers/-/internal-helpers-0.10.0.tgz", + "integrity": "sha512-Ry2R3VPeIN4uPCSA4xQc+e+vsJXkalKpEbDc07hV+a/o5Bs2N/s/uDcPJH/05L19DKh9tAy7e6JM3YZ6Cxfezw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.4", + "@types/mdast": "^4.0.4", + "js-yaml": "^4.1.1", + "picomatch": "^4.0.4", + "retext-smartypants": "^6.2.0", + "shiki": "^4.0.2", + "smol-toml": "^1.6.0", + "unified": "^11.0.5" + } + }, + "node_modules/@astrojs/markdown-remark": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@astrojs/markdown-remark/-/markdown-remark-7.2.0.tgz", + "integrity": "sha512-+YxmVQu1Bd+MFfSzjq1rOJvD9+nIOJzz5YIIhdIH01RrxRkKbyKoEgyIqP3yv51MhzMDgd79QaPv+kCVPT8vHw==", + "license": "MIT", + "dependencies": { + "@astrojs/internal-helpers": "0.10.0", + "@astrojs/prism": "4.0.2", + "github-slugger": "^2.0.0", + "hast-util-from-html": "^2.0.3", + "hast-util-to-text": "^4.0.2", + "mdast-util-definitions": "^6.0.0", + "rehype-raw": "^7.0.0", + "rehype-stringify": "^10.0.1", + "remark-gfm": "^4.0.1", + "remark-parse": "^11.0.0", + "remark-rehype": "^11.1.2", + "remark-smartypants": "^3.0.2", + "unified": "^11.0.5", + "unist-util-remove-position": "^5.0.0", + "unist-util-visit": "^5.1.0", + "unist-util-visit-parents": "^6.0.2", + "vfile": "^6.0.3" + } + }, + "node_modules/@astrojs/mdx": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@astrojs/mdx/-/mdx-6.0.3.tgz", + "integrity": "sha512-+4P3ZvwsRAqAbBgY+uZMewFo3ficlIBPZfu/Luk+v4ia/ZOuFhpsw7r+7672uT2Fc1UPdp7yW0eU5egvSq0wbw==", + "license": "MIT", + "dependencies": { + "@astrojs/internal-helpers": "0.10.0", + "@astrojs/markdown-remark": "7.2.0", + "@mdx-js/mdx": "^3.1.1", + "acorn": "^8.16.0", + "es-module-lexer": "^2.0.0", + "estree-util-visit": "^2.0.0", + "hast-util-to-html": "^9.0.5", + "piccolore": "^0.1.3", + "rehype-raw": "^7.0.0", + "remark-gfm": "^4.0.1", + "remark-smartypants": "^3.0.2", + "source-map": "^0.7.6", + "unist-util-visit": "^5.1.0", + "vfile": "^6.0.3" + }, + "engines": { + "node": ">=22.12.0" + }, + "peerDependencies": { + "@astrojs/markdown-satteri": "0.3.0", + "astro": "^6.4.0" + }, + "peerDependenciesMeta": { + "@astrojs/markdown-satteri": { + "optional": true + } + } + }, + "node_modules/@astrojs/prism": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@astrojs/prism/-/prism-4.0.2.tgz", + "integrity": "sha512-KTivpmnz6lDsC6o9H4+DNm2SrE/GHzw8cNAvEJwAvUT+eoaEnn/4NtbDNfRRaxaJHdp15gf+tfHAWiXR4wB3BA==", + "license": "MIT", + "dependencies": { + "prismjs": "^1.30.0" + }, + "engines": { + "node": ">=22.12.0" + } + }, + "node_modules/@astrojs/telemetry": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/@astrojs/telemetry/-/telemetry-3.3.2.tgz", + "integrity": "sha512-j8DNruA8ors99Al39RYZPJK4DC1bKkoNm93mAMuBhY9TCNC4R8n1q7ovFnJ5qhGh5Lsh7pa1gpQVpYpsJPeTHQ==", + "license": "MIT", + "dependencies": { + "ci-info": "^4.4.0", + "dset": "^3.1.4", + "is-docker": "^4.0.0", + "is-wsl": "^3.1.1", + "which-pm-runs": "^1.1.0" + }, + "engines": { + "node": "18.20.8 || ^20.3.0 || >=22.0.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@capsizecss/unpack": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@capsizecss/unpack/-/unpack-4.0.1.tgz", + "integrity": "sha512-CuNiSqg7+e1cO/GjffyMOm5Tt2jUF9CWHHnvQ/UkqvtkGfHdgwEC0wpmq7fkN3gxwpRnrAN0WzO3vREKmNolMQ==", + "license": "MIT", + "dependencies": { + "fontkitten": "^1.0.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@clack/core": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@clack/core/-/core-1.4.1.tgz", + "integrity": "sha512-FILJa1gGKEFTGZAJE9RpVhrjKz3c3h4ar60dSv6cGuDqufQ84YEIS3GAGvZiN+H6yaLbbvTFNejjCC4tXpZEuw==", + "license": "MIT", + "dependencies": { + "fast-wrap-ansi": "^0.2.0", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 20.12.0" + } + }, + "node_modules/@clack/prompts": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@clack/prompts/-/prompts-1.5.1.tgz", + "integrity": "sha512-zccHj2z2oCCO4yrDiRSlFOxWerGqRiysP7a5jPK6uoI9URKAquwY42Dd/iUP8JWHxEzdRe4TlbvZCo8z1/mhrw==", + "license": "MIT", + "dependencies": { + "@clack/core": "1.4.1", + "fast-string-width": "^3.0.2", + "fast-wrap-ansi": "^0.2.0", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 20.12.0" + } + }, + "node_modules/@emnapi/core": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", + "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.1", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz", + "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", + "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", + "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz", + "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", + "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz", + "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", + "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", + "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", + "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", + "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", + "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", + "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", + "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", + "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", + "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", + "cpu": [ + "mips64el" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", + "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", + "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", + "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", + "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", + "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", + "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", + "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", + "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", + "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", + "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", + "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", + "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", + "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/colour": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", + "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", + "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", + "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", + "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", + "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", + "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", + "cpu": [ + "arm" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", + "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", + "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", + "cpu": [ + "ppc64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", + "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", + "cpu": [ + "riscv64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", + "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", + "cpu": [ + "s390x" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", + "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", + "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", + "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", + "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", + "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", + "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", + "cpu": [ + "ppc64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", + "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", + "cpu": [ + "riscv64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", + "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", + "cpu": [ + "s390x" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", + "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", + "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", + "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", + "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.7.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", + "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", + "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", + "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "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==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@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==", + "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==", + "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==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@mdx-js/mdx": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@mdx-js/mdx/-/mdx-3.1.1.tgz", + "integrity": "sha512-f6ZO2ifpwAQIpzGWaBQT2TXxPv6z3RBzQKpVftEWN78Vl/YweF1uwussDx8ECAXVtr3Rs89fKyG9YlzUs9DyGQ==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdx": "^2.0.0", + "acorn": "^8.0.0", + "collapse-white-space": "^2.0.0", + "devlop": "^1.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "estree-util-scope": "^1.0.0", + "estree-walker": "^3.0.0", + "hast-util-to-jsx-runtime": "^2.0.0", + "markdown-extensions": "^2.0.0", + "recma-build-jsx": "^1.0.0", + "recma-jsx": "^1.0.0", + "recma-stringify": "^1.0.0", + "rehype-recma": "^1.0.0", + "remark-mdx": "^3.0.0", + "remark-parse": "^11.0.0", + "remark-rehype": "^11.0.0", + "source-map": "^0.7.0", + "unified": "^11.0.0", + "unist-util-position-from-estree": "^2.0.0", + "unist-util-stringify-position": "^4.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.5.tgz", + "integrity": "sha512-AWPoBRJ9tsnVhor4sjO7rkni+7p+2IAEFj6cx06UgP10jkQHqay/36uRV/bFkgrh18D9vb4cr8Q0Pthskgzy+Q==", + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, + "node_modules/@oslojs/encoding": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@oslojs/encoding/-/encoding-1.1.0.tgz", + "integrity": "sha512-70wQhgYmndg4GCPxPPxPGevRKqTIJ2Nh4OkiMWmDAVYsTQ+Ta7Sq+rPevXyXGdzr30/qZBnyOalCszoMxlyldQ==", + "license": "MIT" + }, + "node_modules/@rollup/pluginutils": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.4.0.tgz", + "integrity": "sha512-MfPp06CjRLfXQ3wY0R8vJDYBy/MvVcc9OulEfR0B8Iv9ko+GCNaRZ+EpJYFl27LhKsZK0o420sYCRHCjfCgeUg==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/pluginutils/node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.0.tgz", + "integrity": "sha512-IPIQ55ythEHkfEd9jMEi32OQ7SxURsGA43JI22lj01OLZNt2NUbJX8YUHxkVWyQ6daHPNn0truF5nSj3DQp6YQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.0.tgz", + "integrity": "sha512-M6s9cr10MibETyo8JsOkq+Lo1+lU6hcvb1MApnUql5qte/5hMEgzlN8/ReIKNfRV8rrqX50W1BX9zoUhC192RA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.0.tgz", + "integrity": "sha512-BqCoMoIbn0keKys+dEAdBa70EtOwV1bEsQCUgU9FdiZmmMge/Zk7LlkYGqbrdHR+Frnt0E1FOanly+rlwvvQzw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.0.tgz", + "integrity": "sha512-SIMzST3VFNXDAbeIWDWiFCNM5qncUBDWaEV7NfE7oZbDt2mgfW4MvbKdbYiGOLoM32gbTv608UMd0XktEYSD7w==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.0.tgz", + "integrity": "sha512-ezjfSQMP7ArdUsbBwbQIfwAlhE84I2iVnzQNCFSveqV42q+BmKlzVpf7mxv5EchLcoWU4y6/heFzVg1F+hodUQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.0.tgz", + "integrity": "sha512-9+qTWGW9AZRhnUgwtTwzNwcPlL87ngkeN0LA+q1bADvmY9aNvWaF2TFW8BZgnQPYxpDI7+rMVLivcd4V737TAQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.0.tgz", + "integrity": "sha512-T1dMEQhXA/jkJ/jyMIw9IovK8bSUq7A8kLIlvZTb/6YIVsp2zLavr4F3oyllHWo7eIVJRyE5n3tUjQJEbE1IuQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.0.tgz", + "integrity": "sha512-2as0LgT7qQpyceQq6VUJYnumUMUrgGQCWIiDIN9DE0/tglsk6o66uCB4f3djRawAltvfCNLyZZrsqbPA6inCsA==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.0.tgz", + "integrity": "sha512-bVURMg+6eNN9C/yc0aVjooZcwTTtYF4YW3xta5pP0//r3o1V8gXEHXWCndj47w/HhwsFroZrFhR+6uQP5T0n0g==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.0.tgz", + "integrity": "sha512-Ful8pM/2yYI83PViWdFdpZhdI8HJ5qsXANe5atypbHDf+KIBBDsZsbyy8hbXnULVvW9NsTh5DHwbcBftyLTfiw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.0.tgz", + "integrity": "sha512-9Gp/DgrkzfUBmNPVTyPTvay+4xEP7M/clXpj3efXBcm6uTIVIgDg4rqUpqKXvLEuFRVuEpSAOkhgNeecvaZ4Cg==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.0.tgz", + "integrity": "sha512-m9tsJz54LUXkSYM8+8PG81B9IKK5r+2T0clMq4QrS16xFosufU7firBDAZEsDheDs7wTlP7h3++S7lMsU955HA==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.0.tgz", + "integrity": "sha512-3UvJ5PNVU16aJf6M3tFI24pWzAl2/ynfbyRN3ICyQajK1lSkrnVYNnLz3v04J32qKa0FczJc22zeToc0lr2A3w==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.0.tgz", + "integrity": "sha512-vRWUAbYLGHBZS6Q8Msb2sfnf1fvJf+47t8l/TwOerM2qArzy+IeNMTHrYLHXh95h8MoatPHI5hhSZNs+mGXKPg==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.0.tgz", + "integrity": "sha512-c00T5SYENHAt86cfW47URaP3Us5vLC/4QO7GYud1G5VNRffCwwCuBspwqYrriuJB+5m0WFzClCn9wed0FBjKvg==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.0.tgz", + "integrity": "sha512-krrCDilhXOwFkSkO3Wm9I/f9H0L92XHHwy2fwxjukxIbh0dem8gZqOW5Y8BsHrpJv5qwlRBV+Wl4ZFyRWhUpwg==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.0.tgz", + "integrity": "sha512-7pfYFSTc4/rUC/FtAI0Qp6QthDBCIi6/AuP1xYqFk5vanI6KnL5dWKP60OM/05LOsbwTmIcvr6eXC4CJuJ75IA==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.0.tgz", + "integrity": "sha512-7SDIalKeIpG0Ifogbbdn58HmSotYMlf23K3dCJEmiVd9Fg36Vmni82iPQec27N3wY4Bvbxftkxz6vSx9OcouTg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.0.tgz", + "integrity": "sha512-eRZevouTH2i1HeAVLqJuLnt256krQkGY0TN6WsTmsIhuzbh457HuWDMakKwmi0Cjadux983CoSr8Lim2QhUIFw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.0.tgz", + "integrity": "sha512-3oVS7FLGa4U1qcvao9ylGxrjXZyUQqR8UwxEcnUEyPX53O/C/mKDZegNXTdHCP+h3e6ta/f1EN38Yif1mmZHYg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.0.tgz", + "integrity": "sha512-yTB9TgfWj5wHe5QgktAgXTLLot1gvEjl1NiPPAUiCs4oPrIWFl5V4nC3GrkNdj9LaAU4s94nVrGbGOCqUpyWsg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.0.tgz", + "integrity": "sha512-5LOhoaesY3doG1c+ac/2JtgREpKoJr5bUHH8tKY0V8di7+uSV6BwLs2PlR0/yzefGOkR+wE7ZolZphHCsyG5Rw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.0.tgz", + "integrity": "sha512-yYkWHhmbhRTWTnWos5HC4GcPQfjlzzCNbM9e/+GXrLuaBXYA3qSDR9f0Vgufd5S8yX81U8jPKp7ZnAjZFMtRnw==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.0.tgz", + "integrity": "sha512-SoTb6lPg25xZlA2ibwQ++ahCCnH+FP0qmEuafMJ4gznZKOlXioKEAeJLgCrqjM98ACziXM9V1amFjICVL4IFoA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.0.tgz", + "integrity": "sha512-5L+T1fMX4RIEBoZzT0+sQ0PhTS36NULFmMXtl1TZo44TMAROIMHbZufSOjVWt/Y622BtxgxtaNOokbTDvfsrZA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@shikijs/core": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-4.2.0.tgz", + "integrity": "sha512-Hc87Ab1Ld/vEbZRCbwx344I5v+4RU8CVToUTRkqXL1+TjbuOp9U5Xa0M23V4GEWHxVn+yO5otb+HkQVm3ptWQQ==", + "license": "MIT", + "dependencies": { + "@shikijs/primitive": "4.2.0", + "@shikijs/types": "4.2.0", + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4", + "hast-util-to-html": "^9.0.5" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@shikijs/engine-javascript": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@shikijs/engine-javascript/-/engine-javascript-4.2.0.tgz", + "integrity": "sha512-fjETeq1k5ffyXqRgS6+3hpvqseLalp1kjNfRbXpUgWR8FpZ1CmQfiNHovc5lncYjt/Vg5JK/WJEmLahjwMa0og==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "4.2.0", + "@shikijs/vscode-textmate": "^10.0.2", + "oniguruma-to-es": "^4.3.6" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@shikijs/engine-oniguruma": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-4.2.0.tgz", + "integrity": "sha512-hTorK1dffPkpbMUk6Z+828PgRo7d07HbnizoP0hNPFjhxMHctj0Px/qoHeGMYafc6ju+u9iMldN4JbVzNQM++g==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "4.2.0", + "@shikijs/vscode-textmate": "^10.0.2" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@shikijs/langs": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-4.2.0.tgz", + "integrity": "sha512-bwrVRlJ0wUhZxAbVdvBbv2TTC9yLsh4C/IO5Ofz0T8MQntgDvyVnkbjw9vi50r1kx7RCIJdnJnjZAwmAsXFLZQ==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "4.2.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@shikijs/primitive": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@shikijs/primitive/-/primitive-4.2.0.tgz", + "integrity": "sha512-NOq+DtUkVBJtZMVXL5A0vI0Xk8nvDYaXetFHSJFlOqjDZIVhIPRYFdGkSoElDqNuegikcc3A76SNUa8dTqtAYA==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "4.2.0", + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@shikijs/themes": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-4.2.0.tgz", + "integrity": "sha512-RX8IHYeLv8Cu2W6ruc3RxUqWn0IYCqSrMBzi/uRGAmfyDNOnNO5BF/Px7o97n4XTpmFTo5GbRaazuOWj+2ak2w==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "4.2.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@shikijs/types": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-4.2.0.tgz", + "integrity": "sha512-VT/MKtlpOhEPZloSH3Pb9WCZEBDoQVMa9jedp5UAwmJOar1DVc9DRODAxmYPW9M93IK4ryuqRejFfmlvlVDemw==", + "license": "MIT", + "dependencies": { + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@shikijs/vscode-textmate": { + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/@shikijs/vscode-textmate/-/vscode-textmate-10.0.2.tgz", + "integrity": "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==", + "license": "MIT" + }, + "node_modules/@tailwindcss/node": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.1.tgz", + "integrity": "sha512-6NDaqRoAMSXD1mr/RXu0HBvNE9a2n5tHPsxu9XHLws8o4Twes5rBM2205SUUiJ9goAtadrN6xTGX0UDEwp/N4A==", + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "enhanced-resolve": "5.21.6", + "jiti": "^2.7.0", + "lightningcss": "1.32.0", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.3.1" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.3.1.tgz", + "integrity": "sha512-yVPyo8RNkabVr3O2EhHEE0Rewu7YKzc1DhIqfL46LKveFrmu9XbDazNOJY7/GRuvw1h6u3utWnR29H/p5JPlgA==", + "license": "MIT", + "engines": { + "node": ">= 20" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.3.1", + "@tailwindcss/oxide-darwin-arm64": "4.3.1", + "@tailwindcss/oxide-darwin-x64": "4.3.1", + "@tailwindcss/oxide-freebsd-x64": "4.3.1", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.1", + "@tailwindcss/oxide-linux-arm64-gnu": "4.3.1", + "@tailwindcss/oxide-linux-arm64-musl": "4.3.1", + "@tailwindcss/oxide-linux-x64-gnu": "4.3.1", + "@tailwindcss/oxide-linux-x64-musl": "4.3.1", + "@tailwindcss/oxide-wasm32-wasi": "4.3.1", + "@tailwindcss/oxide-win32-arm64-msvc": "4.3.1", + "@tailwindcss/oxide-win32-x64-msvc": "4.3.1" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.3.1.tgz", + "integrity": "sha512-SVlyf61g374l5cHyg8x9kf5xmLcOaxvOTsbsqDnSsDJaKOEFZ7GCvi84VAVGpxojYOs1+3K6M0UjXfqPU8vmOQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.1.tgz", + "integrity": "sha512-hVnWLwv+e/l7c4WKyVtHVrIPvYdqWHjRB3MDIqARynzFtnQg85kmQEFCbV9Ja0VVx4xXTIiDWY60Y7iz/iNoDA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.3.1.tgz", + "integrity": "sha512-Cf7abu0WVgbhU7ANgPUnSAvm7nCvMweusHb8FnaHlLfv/Caq4GYaEZg7ZImzzmjx4lIAfuS8q+eLIS7A7IzxIg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.3.1.tgz", + "integrity": "sha512-ZZqzX2Y+GXtXXfqSfpJhDm60OoZfvLHLCgm+J7NVqgHHJjG/m9ugZI77RwTsVd4fnBJuCFP6Ae6kTJb71UdS8g==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.3.1.tgz", + "integrity": "sha512-/Ah/xik0LaMYfv9DZ0S/t4pBlBNYOcqtRwusjgovHkvT8ixueWCLyJjsaF5kQIckjb4IT8Q6K6p/iPmZMixYgg==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.3.1.tgz", + "integrity": "sha512-gqdFoVJlw444GvpnheZLHmvTzSxI/cOUUh2KSNejQjTcYkW062SVD+En0rUgD+QV91bz1XGIGtt1HJd48xUGbQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.3.1.tgz", + "integrity": "sha512-Bwv9KwOvE0VKa86xPFif9b9c3Y1NxOV1P0gLti/IYaWEsQYZXDlxfGEtA8mdDZ7SG3wyNXAWYT5SIn3giL57oA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.3.1.tgz", + "integrity": "sha512-Ymi8O8T15HYQdOUWUtTI6ldN0neHP85FC+Qz32xTcZ7iJXtem/x8ITev0o1e9e5rkqj4lONZfTRLvkmin1+tKg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.3.1.tgz", + "integrity": "sha512-M+P/91qJ6uILLw4k2G93GMDRAXj61SMvFQYt39AqvUqYgExXpLL5aepfns7sj4HiAQeolirQF9E0lzRvdf4zPQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.3.1.tgz", + "integrity": "sha512-zsM8uOeqvVGHsAXsJxsT28ttosFahLJKCLOTUBqRAtKnVgGSRitds9T432QiT8b77Yga7JIBkulIRRlJPtYhRA==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.10.0", + "@emnapi/runtime": "^1.10.0", + "@emnapi/wasi-threads": "^1.2.1", + "@napi-rs/wasm-runtime": "^1.1.4", + "@tybys/wasm-util": "^0.10.2", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.1.tgz", + "integrity": "sha512-aiNvSq9BsVk8V513lDKlrCFAgf8qBMPZTpgEhInL+NwQqs97mYmupVMrPrgBBSL8Pv/0zXu9MrMF9rMun1ZeNg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.3.1.tgz", + "integrity": "sha512-xDEyu1rg290472FEGaKHnzyDyh5QH+AlWvsU5hMoMtPpzmKlRI0jaYKCgSHDYtaQWZOYbMaduSyCwFwY4n1HmA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/vite": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.3.1.tgz", + "integrity": "sha512-hItDHuIIlEV61R+faXu66s1K36aTurO/Qw0e45Vskz57gXl9pWOT6eg3zmcEui6CZXddbN7zd41bwmvag4JGwQ==", + "license": "MIT", + "dependencies": { + "@tailwindcss/node": "4.3.1", + "@tailwindcss/oxide": "4.3.1", + "tailwindcss": "4.3.1" + }, + "peerDependencies": { + "vite": "^5.2.0 || ^6 || ^7 || ^8" + } + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz", + "integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/debug": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz", + "integrity": "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==", + "license": "MIT", + "dependencies": { + "@types/ms": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "license": "MIT" + }, + "node_modules/@types/estree-jsx": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree-jsx/-/estree-jsx-1.0.5.tgz", + "integrity": "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==", + "license": "MIT", + "dependencies": { + "@types/estree": "*" + } + }, + "node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/mdx": { + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/@types/mdx/-/mdx-2.0.14.tgz", + "integrity": "sha512-T48PeuJtvLosNTPVhfnIp3i/n3a4g4Bad7YCq5k64D4u7NwDrAotikQ+5+sjtUvBmxCMlbo3dVL+C2dP0rWHzg==", + "license": "MIT" + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "license": "MIT" + }, + "node_modules/@types/nlcst": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/nlcst/-/nlcst-2.0.3.tgz", + "integrity": "sha512-vSYNSDe6Ix3q+6Z7ri9lyWqgGhJTmzRjZRqyq15N0Z/1/UnVsno9G/N40NBijoYx2seFDIl0+B2mgAb9mezUCA==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "license": "MIT" + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.1.tgz", + "integrity": "sha512-mUFwbeTqrVgDQxFveS+df2yfap6iuP20NAKAsBt5jDEoOTDew+zwLAOilHCeQJOVSvmgCX4ogqIrA0mnyr08yQ==", + "license": "ISC" + }, + "node_modules/acorn": { + "version": "8.17.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz", + "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==", + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "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==", + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/anymatch/node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" + }, + "node_modules/aria-query": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", + "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/array-iterate": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/array-iterate/-/array-iterate-2.0.1.tgz", + "integrity": "sha512-I1jXZMjAgCMmxT4qxXfPXa6SthSoE8h6gkSI9BGGNv8mP8G/v0blc+qFnZu6K42vTOiuME596QaLO0TP3Lk0xg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/astring": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/astring/-/astring-1.9.0.tgz", + "integrity": "sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg==", + "license": "MIT", + "bin": { + "astring": "bin/astring" + } + }, + "node_modules/astro": { + "version": "6.4.7", + "resolved": "https://registry.npmjs.org/astro/-/astro-6.4.7.tgz", + "integrity": "sha512-5vsXx0H52u23Jpshs9tM81D03Tb3Oh2Vt2Zo0bpqjXN+njkAWjFyGjTfmWJLAcrCQd9Q+iWB1eqfhR1sZJEaUA==", + "license": "MIT", + "dependencies": { + "@astrojs/compiler": "^4.0.0", + "@astrojs/internal-helpers": "0.10.0", + "@astrojs/markdown-remark": "7.2.0", + "@astrojs/telemetry": "3.3.2", + "@capsizecss/unpack": "^4.0.0", + "@clack/prompts": "^1.1.0", + "@oslojs/encoding": "^1.1.0", + "@rollup/pluginutils": "^5.3.0", + "aria-query": "^5.3.2", + "axobject-query": "^4.1.0", + "ci-info": "^4.4.0", + "clsx": "^2.1.1", + "common-ancestor-path": "^2.0.0", + "cookie": "^1.1.1", + "devalue": "^5.8.1", + "diff": "^8.0.3", + "dset": "^3.1.4", + "es-module-lexer": "^2.0.0", + "esbuild": "^0.27.3", + "flattie": "^1.1.1", + "fontace": "~0.4.1", + "get-tsconfig": "5.0.0-beta.4", + "github-slugger": "^2.0.0", + "html-escaper": "3.0.3", + "http-cache-semantics": "^4.2.0", + "js-yaml": "^4.1.1", + "jsonc-parser": "^3.3.1", + "magic-string": "^0.30.21", + "magicast": "^0.5.2", + "mrmime": "^2.0.1", + "neotraverse": "^0.6.18", + "obug": "^2.1.1", + "p-limit": "^7.3.0", + "p-queue": "^9.1.0", + "package-manager-detector": "^1.6.0", + "piccolore": "^0.1.3", + "picomatch": "^4.0.4", + "rehype": "^13.0.2", + "semver": "^7.7.4", + "shiki": "^4.0.2", + "smol-toml": "^1.6.0", + "svgo": "^4.0.1", + "tinyclip": "^0.1.12", + "tinyexec": "^1.0.4", + "tinyglobby": "^0.2.15", + "ultrahtml": "^1.6.0", + "unifont": "~0.7.4", + "unist-util-visit": "^5.1.0", + "unstorage": "^1.17.5", + "vfile": "^6.0.3", + "vite": "^7.3.2", + "vitefu": "^1.1.2", + "xxhash-wasm": "^1.1.0", + "yargs-parser": "^22.0.0", + "zod": "^4.3.6" + }, + "bin": { + "astro": "bin/astro.mjs" + }, + "engines": { + "node": ">=22.12.0", + "npm": ">=9.6.5", + "pnpm": ">=7.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/astrodotbuild" + }, + "optionalDependencies": { + "sharp": "^0.34.0" + } + }, + "node_modules/axobject-query": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", + "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/bail": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", + "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "license": "ISC" + }, + "node_modules/ccount": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", + "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz", + "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-html4": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz", + "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-legacy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", + "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-reference-invalid": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz", + "integrity": "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/chokidar": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", + "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", + "license": "MIT", + "dependencies": { + "readdirp": "^5.0.0" + }, + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/ci-info": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.4.0.tgz", + "integrity": "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/collapse-white-space": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/collapse-white-space/-/collapse-white-space-2.1.0.tgz", + "integrity": "sha512-loKTxY1zCOuG4j9f6EPnuyyYkf58RnhhWTvRoZEokgB+WbdXehfjFviyOVYkqzEWz1Q5kRiZdBYS5SwxbQYwzw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/comma-separated-tokens": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", + "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/commander": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz", + "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==", + "license": "MIT", + "engines": { + "node": ">=16" + } + }, + "node_modules/common-ancestor-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/common-ancestor-path/-/common-ancestor-path-2.0.0.tgz", + "integrity": "sha512-dnN3ibLeoRf2HNC+OlCiNc5d2zxbLJXOtiZUudNFSXZrNSydxcCsSpRzXwfu7BBWCIfHPw+xTayeBvJCP/D8Ng==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">= 18" + } + }, + "node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/cookie-es": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/cookie-es/-/cookie-es-1.2.3.tgz", + "integrity": "sha512-lXVyvUvrNXblMqzIRrxHb57UUVmqsSWlxqt3XIjCkUP0wDAf6uicO6KMbEgYrMNtEvWgWHwe42CKxPu9MYAnWw==", + "license": "MIT" + }, + "node_modules/crossws": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/crossws/-/crossws-0.3.5.tgz", + "integrity": "sha512-ojKiDvcmByhwa8YYqbQI/hg7MEU0NC03+pSdEq4ZUnZR9xXpwk7E43SMNGkn+JxJGPFtNvQ48+vV2p+P1ml5PA==", + "license": "MIT", + "dependencies": { + "uncrypto": "^0.1.3" + } + }, + "node_modules/css-select": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz", + "integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-tree": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz", + "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==", + "license": "MIT", + "dependencies": { + "mdn-data": "2.27.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, + "node_modules/css-what": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz", + "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/csso": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/csso/-/csso-5.0.5.tgz", + "integrity": "sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==", + "license": "MIT", + "dependencies": { + "css-tree": "~2.2.0" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/csso/node_modules/css-tree": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.2.1.tgz", + "integrity": "sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==", + "license": "MIT", + "dependencies": { + "mdn-data": "2.0.28", + "source-map-js": "^1.0.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/csso/node_modules/mdn-data": { + "version": "2.0.28", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.28.tgz", + "integrity": "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==", + "license": "CC0-1.0" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decode-named-character-reference": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.3.0.tgz", + "integrity": "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==", + "license": "MIT", + "dependencies": { + "character-entities": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/defu": { + "version": "6.1.7", + "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.7.tgz", + "integrity": "sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==", + "license": "MIT" + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/destr": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/destr/-/destr-2.0.5.tgz", + "integrity": "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==", + "license": "MIT" + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/devalue": { + "version": "5.8.1", + "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.8.1.tgz", + "integrity": "sha512-4CXDYRBGqN+57wVJkuXBYmpAVUSg3L6JAQa/DFqm238G73E1wuyc/JhGQJzN7vUf/CMphYau2zXbfWzDR5aTEw==", + "license": "MIT" + }, + "node_modules/devlop": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", + "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", + "license": "MIT", + "dependencies": { + "dequal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/diff": { + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/diff/-/diff-8.0.4.tgz", + "integrity": "sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/dom-serializer/node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/dset": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/dset/-/dset-3.1.4.tgz", + "integrity": "sha512-2QF/g9/zTaPDc3BjNcVTGoBbXBgYfMTTceLaYcFJ/W9kggFUkhxD/hMEeuLKbugyef9SqAx8cpgwlIP/jinUTA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/enhanced-resolve": { + "version": "5.21.6", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.21.6.tgz", + "integrity": "sha512-aNnGCvbJ/RIyWo1IuhNdVjnNF+EjH9wpzpNHt+ci/m9He9LJvUN8wrCcXjp9cWsGNAuvSpVFTx/vraAFQ8qGjQ==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-module-lexer": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.1.0.tgz", + "integrity": "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==", + "license": "MIT" + }, + "node_modules/esast-util-from-estree": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/esast-util-from-estree/-/esast-util-from-estree-2.0.0.tgz", + "integrity": "sha512-4CyanoAudUSBAn5K13H4JhsMH6L9ZP7XbLVe/dKybkxMO7eDyLsT8UHl9TRNrU2Gr9nz+FovfSIjuXWJ81uVwQ==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "devlop": "^1.0.0", + "estree-util-visit": "^2.0.0", + "unist-util-position-from-estree": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/esast-util-from-js": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/esast-util-from-js/-/esast-util-from-js-2.0.1.tgz", + "integrity": "sha512-8Ja+rNJ0Lt56Pcf3TAmpBZjmx8ZcK5Ts4cAzIOjsjevg9oSXJnl6SUQ2EevU8tv3h6ZLWmoKL5H4fgWvdvfETw==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "acorn": "^8.0.0", + "esast-util-from-estree": "^2.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/esbuild": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", + "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.7", + "@esbuild/android-arm": "0.27.7", + "@esbuild/android-arm64": "0.27.7", + "@esbuild/android-x64": "0.27.7", + "@esbuild/darwin-arm64": "0.27.7", + "@esbuild/darwin-x64": "0.27.7", + "@esbuild/freebsd-arm64": "0.27.7", + "@esbuild/freebsd-x64": "0.27.7", + "@esbuild/linux-arm": "0.27.7", + "@esbuild/linux-arm64": "0.27.7", + "@esbuild/linux-ia32": "0.27.7", + "@esbuild/linux-loong64": "0.27.7", + "@esbuild/linux-mips64el": "0.27.7", + "@esbuild/linux-ppc64": "0.27.7", + "@esbuild/linux-riscv64": "0.27.7", + "@esbuild/linux-s390x": "0.27.7", + "@esbuild/linux-x64": "0.27.7", + "@esbuild/netbsd-arm64": "0.27.7", + "@esbuild/netbsd-x64": "0.27.7", + "@esbuild/openbsd-arm64": "0.27.7", + "@esbuild/openbsd-x64": "0.27.7", + "@esbuild/openharmony-arm64": "0.27.7", + "@esbuild/sunos-x64": "0.27.7", + "@esbuild/win32-arm64": "0.27.7", + "@esbuild/win32-ia32": "0.27.7", + "@esbuild/win32-x64": "0.27.7" + } + }, + "node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/estree-util-attach-comments": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/estree-util-attach-comments/-/estree-util-attach-comments-3.0.0.tgz", + "integrity": "sha512-cKUwm/HUcTDsYh/9FgnuFqpfquUbwIqwKM26BVCGDPVgvaCl/nDCCjUfiLlx6lsEZ3Z4RFxNbOQ60pkaEwFxGw==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/estree-util-build-jsx": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/estree-util-build-jsx/-/estree-util-build-jsx-3.0.1.tgz", + "integrity": "sha512-8U5eiL6BTrPxp/CHbs2yMgP8ftMhR5ww1eIKoWRMlqvltHF8fZn5LRDvTKuxD3DUn+shRbLGqXemcP51oFCsGQ==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "devlop": "^1.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "estree-walker": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/estree-util-is-identifier-name": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz", + "integrity": "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/estree-util-scope": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/estree-util-scope/-/estree-util-scope-1.0.0.tgz", + "integrity": "sha512-2CAASclonf+JFWBNJPndcOpA8EMJwa0Q8LUFJEKqXLW6+qBvbFZuF5gItbQOs/umBUkjviCSDCbBwU2cXbmrhQ==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "devlop": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/estree-util-to-js": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/estree-util-to-js/-/estree-util-to-js-2.0.0.tgz", + "integrity": "sha512-WDF+xj5rRWmD5tj6bIqRi6CkLIXbbNQUcxQHzGysQzvHmdYG2G7p/Tf0J0gpxGgkeMZNTIjT/AoSvC9Xehcgdg==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "astring": "^1.8.0", + "source-map": "^0.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/estree-util-visit": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/estree-util-visit/-/estree-util-visit-2.0.0.tgz", + "integrity": "sha512-m5KgiH85xAhhW8Wta0vShLcUvOsh3LLPI2YVwcbio1l7E09NTLL1EyMZFM1OyWowoH0skScNbhOPl4kcBgzTww==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/eventemitter3": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", + "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", + "license": "MIT" + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT" + }, + "node_modules/fast-string-truncated-width": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/fast-string-truncated-width/-/fast-string-truncated-width-3.0.3.tgz", + "integrity": "sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g==", + "license": "MIT" + }, + "node_modules/fast-string-width": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/fast-string-width/-/fast-string-width-3.0.2.tgz", + "integrity": "sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg==", + "license": "MIT", + "dependencies": { + "fast-string-truncated-width": "^3.0.2" + } + }, + "node_modules/fast-wrap-ansi": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/fast-wrap-ansi/-/fast-wrap-ansi-0.2.2.tgz", + "integrity": "sha512-7F2Fl+TjRSenLqlU3UjSH0iyqopqoZIu7eZVpEirP2g1GtWa2G/ecEmBdgz31+Mxr+ELclgg6sokpSFIQiZ02Q==", + "license": "MIT", + "dependencies": { + "fast-string-width": "^3.0.2" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/flattie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/flattie/-/flattie-1.1.1.tgz", + "integrity": "sha512-9UbaD6XdAL97+k/n+N7JwX46K/M6Zc6KcFYskrYL8wbBV/Uyk0CTAMY0VT+qiK5PM7AIc9aTWYtq65U7T+aCNQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/fontace": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/fontace/-/fontace-0.4.1.tgz", + "integrity": "sha512-lDMvbAzSnHmbYMTEld5qdtvNH2/pWpICOqpean9IgC7vUbUJc3k+k5Dokp85CegamqQpFbXf0rAVkbzpyTA8aw==", + "license": "MIT", + "dependencies": { + "fontkitten": "^1.0.2" + } + }, + "node_modules/fontkitten": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/fontkitten/-/fontkitten-1.0.3.tgz", + "integrity": "sha512-Wp1zXWPVUPBmfoa3Cqc9ctaKuzKAV6uLstRqlR56kSjplf5uAce+qeyYym7F+PHbGTk+tCEdkCW6RD7DX/gBZw==", + "license": "MIT", + "dependencies": { + "tiny-inflate": "^1.0.3" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/get-tsconfig": { + "version": "5.0.0-beta.4", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-5.0.0-beta.4.tgz", + "integrity": "sha512-7nF7C9fIPFEMHgEMEfgIlO9wDdZ8CyHw27rWciFZfHvHDReIiPhsYuzPRXsfvBCqFy1l8RRyyWV7QLM+ZhUJsQ==", + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "engines": { + "node": ">=20.20.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/github-slugger": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/github-slugger/-/github-slugger-2.0.0.tgz", + "integrity": "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw==", + "license": "ISC" + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "node_modules/h3": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/h3/-/h3-1.15.11.tgz", + "integrity": "sha512-L3THSe2MPeBwgIZVSH5zLdBBU90TOxarvhK9d04IDY2AmVS8j2Jz2LIWtwsGOU3lu2I5jCN7FNvVfY2+XyF+mg==", + "license": "MIT", + "dependencies": { + "cookie-es": "^1.2.3", + "crossws": "^0.3.5", + "defu": "^6.1.6", + "destr": "^2.0.5", + "iron-webcrypto": "^1.2.1", + "node-mock-http": "^1.0.4", + "radix3": "^1.1.2", + "ufo": "^1.6.3", + "uncrypto": "^0.1.3" + } + }, + "node_modules/hast-util-from-html": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/hast-util-from-html/-/hast-util-from-html-2.0.3.tgz", + "integrity": "sha512-CUSRHXyKjzHov8yKsQjGOElXy/3EKpyX56ELnkHH34vDVw1N1XSQ1ZcAvTyAPtGqLTuKP/uxM+aLkSPqF/EtMw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "devlop": "^1.1.0", + "hast-util-from-parse5": "^8.0.0", + "parse5": "^7.0.0", + "vfile": "^6.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-from-parse5": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/hast-util-from-parse5/-/hast-util-from-parse5-8.0.3.tgz", + "integrity": "sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "devlop": "^1.0.0", + "hastscript": "^9.0.0", + "property-information": "^7.0.0", + "vfile": "^6.0.0", + "vfile-location": "^5.0.0", + "web-namespaces": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-is-element": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-is-element/-/hast-util-is-element-3.0.0.tgz", + "integrity": "sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-parse-selector": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-4.0.0.tgz", + "integrity": "sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-raw": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/hast-util-raw/-/hast-util-raw-9.1.0.tgz", + "integrity": "sha512-Y8/SBAHkZGoNkpzqqfCldijcuUKh7/su31kEBp67cFY09Wy0mTRgtsLYsiIxMJxlu0f6AA5SUTbDR8K0rxnbUw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "@ungap/structured-clone": "^1.0.0", + "hast-util-from-parse5": "^8.0.0", + "hast-util-to-parse5": "^8.0.0", + "html-void-elements": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "parse5": "^7.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0", + "web-namespaces": "^2.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-estree": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/hast-util-to-estree/-/hast-util-to-estree-3.1.3.tgz", + "integrity": "sha512-48+B/rJWAp0jamNbAAf9M7Uf//UVqAoMmgXhBdxTDJLGKY+LRnZ99qcG+Qjl5HfMpYNzS5v4EAwVEF34LeAj7w==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "devlop": "^1.0.0", + "estree-util-attach-comments": "^3.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "hast-util-whitespace": "^3.0.0", + "mdast-util-mdx-expression": "^2.0.0", + "mdast-util-mdx-jsx": "^3.0.0", + "mdast-util-mdxjs-esm": "^2.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "style-to-js": "^1.0.0", + "unist-util-position": "^5.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-html": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-9.0.5.tgz", + "integrity": "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-whitespace": "^3.0.0", + "html-void-elements": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "stringify-entities": "^4.0.0", + "zwitch": "^2.0.4" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-jsx-runtime": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz", + "integrity": "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "devlop": "^1.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "hast-util-whitespace": "^3.0.0", + "mdast-util-mdx-expression": "^2.0.0", + "mdast-util-mdx-jsx": "^3.0.0", + "mdast-util-mdxjs-esm": "^2.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "style-to-js": "^1.0.0", + "unist-util-position": "^5.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-parse5": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/hast-util-to-parse5/-/hast-util-to-parse5-8.0.1.tgz", + "integrity": "sha512-MlWT6Pjt4CG9lFCjiz4BH7l9wmrMkfkJYCxFwKQic8+RTZgWPuWxwAfjJElsXkex7DJjfSJsQIt931ilUgmwdA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "devlop": "^1.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "web-namespaces": "^2.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-text": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/hast-util-to-text/-/hast-util-to-text-4.0.2.tgz", + "integrity": "sha512-KK6y/BN8lbaq654j7JgBydev7wuNMcID54lkRav1P0CaE1e47P72AWWPiGKXTJU271ooYzcvTAn/Zt0REnvc7A==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "hast-util-is-element": "^3.0.0", + "unist-util-find-after": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-whitespace": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", + "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hastscript": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-9.0.1.tgz", + "integrity": "sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-parse-selector": "^4.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/html-escaper": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-3.0.3.tgz", + "integrity": "sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ==", + "license": "MIT" + }, + "node_modules/html-void-elements": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz", + "integrity": "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/http-cache-semantics": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", + "license": "BSD-2-Clause" + }, + "node_modules/inline-style-parser": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.7.tgz", + "integrity": "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==", + "license": "MIT" + }, + "node_modules/iron-webcrypto": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/iron-webcrypto/-/iron-webcrypto-1.2.1.tgz", + "integrity": "sha512-feOM6FaSr6rEABp/eDfVseKyTMDt+KGpeB35SkVn9Tyn0CqvVsY3EwI0v5i8nMHyJnzCIQf7nsy3p41TPkJZhg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/brc-dd" + } + }, + "node_modules/is-alphabetical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz", + "integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-alphanumerical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz", + "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==", + "license": "MIT", + "dependencies": { + "is-alphabetical": "^2.0.0", + "is-decimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-decimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz", + "integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-docker": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-4.0.0.tgz", + "integrity": "sha512-LHE+wROyG/Y/0ZnbktRCoTix2c1RhgWaZraMZ8o1Q7zCh0VSrICJQO5oqIIISrcSBtrXv0o233w1IYwsWCjTzA==", + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-hexadecimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz", + "integrity": "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-inside-container": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", + "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", + "license": "MIT", + "dependencies": { + "is-docker": "^3.0.0" + }, + "bin": { + "is-inside-container": "cli.js" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-inside-container/node_modules/is-docker": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", + "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-wsl": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.1.tgz", + "integrity": "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==", + "license": "MIT", + "dependencies": { + "is-inside-container": "^1.0.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jiti": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", + "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/js-yaml": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.2.0.tgz", + "integrity": "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsonc-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", + "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", + "license": "MIT" + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/longest-streak": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", + "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/lru-cache": { + "version": "11.5.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.1.tgz", + "integrity": "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==", + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/magicast": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.5.3.tgz", + "integrity": "sha512-pVKE4UdSQ7DvHzivsCIFx2BJn1mHG6KsyrFcaxFx6tONdneEuThrDx0Cj3AMg58KyN4pzYT+LHOotxDQDjNvkw==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.3", + "@babel/types": "^7.29.0", + "source-map-js": "^1.2.1" + } + }, + "node_modules/markdown-extensions": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/markdown-extensions/-/markdown-extensions-2.0.0.tgz", + "integrity": "sha512-o5vL7aDWatOTX8LzaS1WMoaoxIiLRQJuIKKe2wAw6IeULDHaqbiqiggmx+pKvZDb1Sj+pE46Sn1T7lCqfFtg1Q==", + "license": "MIT", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/markdown-table": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.4.tgz", + "integrity": "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/mdast-util-definitions": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-definitions/-/mdast-util-definitions-6.0.0.tgz", + "integrity": "sha512-scTllyX6pnYNZH/AIp/0ePz6s4cZtARxImwoPJ7kS42n+MnVsI4XbnG6d4ibehRIldYMWM2LD7ImQblVhUejVQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "unist-util-visit": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-find-and-replace": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.2.tgz", + "integrity": "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "escape-string-regexp": "^5.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-from-markdown": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.3.tgz", + "integrity": "sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark": "^4.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm/-/mdast-util-gfm-3.1.0.tgz", + "integrity": "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==", + "license": "MIT", + "dependencies": { + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-gfm-autolink-literal": "^2.0.0", + "mdast-util-gfm-footnote": "^2.0.0", + "mdast-util-gfm-strikethrough": "^2.0.0", + "mdast-util-gfm-table": "^2.0.0", + "mdast-util-gfm-task-list-item": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-autolink-literal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-2.0.1.tgz", + "integrity": "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "ccount": "^2.0.0", + "devlop": "^1.0.0", + "mdast-util-find-and-replace": "^3.0.0", + "micromark-util-character": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-footnote": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-2.1.0.tgz", + "integrity": "sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-strikethrough": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-2.0.0.tgz", + "integrity": "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-table": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-table/-/mdast-util-gfm-table-2.0.0.tgz", + "integrity": "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "markdown-table": "^3.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-task-list-item": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-2.0.0.tgz", + "integrity": "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-mdx/-/mdast-util-mdx-3.0.0.tgz", + "integrity": "sha512-JfbYLAW7XnYTTbUsmpu0kdBUVe+yKVJZBItEjwyYJiDJuZ9w4eeaqks4HQO+R7objWgS2ymV60GYpI14Ug554w==", + "license": "MIT", + "dependencies": { + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-mdx-expression": "^2.0.0", + "mdast-util-mdx-jsx": "^3.0.0", + "mdast-util-mdxjs-esm": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-expression": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.1.tgz", + "integrity": "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-jsx": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-3.2.0.tgz", + "integrity": "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "devlop": "^1.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "parse-entities": "^4.0.0", + "stringify-entities": "^4.0.0", + "unist-util-stringify-position": "^4.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdxjs-esm": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-2.0.1.tgz", + "integrity": "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-phrasing": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz", + "integrity": "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-hast": { + "version": "13.2.1", + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz", + "integrity": "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@ungap/structured-clone": "^1.0.0", + "devlop": "^1.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "trim-lines": "^3.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-markdown": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.2.tgz", + "integrity": "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "longest-streak": "^3.0.0", + "mdast-util-phrasing": "^4.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "unist-util-visit": "^5.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz", + "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdn-data": { + "version": "2.27.1", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz", + "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==", + "license": "CC0-1.0" + }, + "node_modules/micromark": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz", + "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/debug": "^4.0.0", + "debug": "^4.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-core-commonmark": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz", + "integrity": "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-factory-destination": "^2.0.0", + "micromark-factory-label": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-factory-title": "^2.0.0", + "micromark-factory-whitespace": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-html-tag-name": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-extension-gfm": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm/-/micromark-extension-gfm-3.0.0.tgz", + "integrity": "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==", + "license": "MIT", + "dependencies": { + "micromark-extension-gfm-autolink-literal": "^2.0.0", + "micromark-extension-gfm-footnote": "^2.0.0", + "micromark-extension-gfm-strikethrough": "^2.0.0", + "micromark-extension-gfm-table": "^2.0.0", + "micromark-extension-gfm-tagfilter": "^2.0.0", + "micromark-extension-gfm-task-list-item": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-autolink-literal": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-2.1.0.tgz", + "integrity": "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==", + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-footnote": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-2.1.0.tgz", + "integrity": "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-strikethrough": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-2.1.0.tgz", + "integrity": "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-table": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.1.1.tgz", + "integrity": "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-tagfilter": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-2.0.0.tgz", + "integrity": "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==", + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-task-list-item": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-2.1.0.tgz", + "integrity": "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-mdx-expression": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/micromark-extension-mdx-expression/-/micromark-extension-mdx-expression-3.0.1.tgz", + "integrity": "sha512-dD/ADLJ1AeMvSAKBwO22zG22N4ybhe7kFIZ3LsDI0GlsNr2A3KYxb0LdC1u5rj4Nw+CHKY0RVdnHX8vj8ejm4Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "devlop": "^1.0.0", + "micromark-factory-mdx-expression": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-events-to-acorn": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-extension-mdx-jsx": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/micromark-extension-mdx-jsx/-/micromark-extension-mdx-jsx-3.0.2.tgz", + "integrity": "sha512-e5+q1DjMh62LZAJOnDraSSbDMvGJ8x3cbjygy2qFEi7HCeUT4BDKCvMozPozcD6WmOt6sVvYDNBKhFSz3kjOVQ==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "devlop": "^1.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "micromark-factory-mdx-expression": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-events-to-acorn": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-mdx-md": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-mdx-md/-/micromark-extension-mdx-md-2.0.0.tgz", + "integrity": "sha512-EpAiszsB3blw4Rpba7xTOUptcFeBFi+6PY8VnJ2hhimH+vCQDirWgsMpz7w1XcZE7LVrSAUGb9VJpG9ghlYvYQ==", + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-mdxjs": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-mdxjs/-/micromark-extension-mdxjs-3.0.0.tgz", + "integrity": "sha512-A873fJfhnJ2siZyUrJ31l34Uqwy4xIFmvPY1oj+Ean5PHcPBYzEsvqvWGaWcfEIr11O5Dlw3p2y0tZWpKHDejQ==", + "license": "MIT", + "dependencies": { + "acorn": "^8.0.0", + "acorn-jsx": "^5.0.0", + "micromark-extension-mdx-expression": "^3.0.0", + "micromark-extension-mdx-jsx": "^3.0.0", + "micromark-extension-mdx-md": "^2.0.0", + "micromark-extension-mdxjs-esm": "^3.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-mdxjs-esm": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-mdxjs-esm/-/micromark-extension-mdxjs-esm-3.0.0.tgz", + "integrity": "sha512-DJFl4ZqkErRpq/dAPyeWp15tGrcrrJho1hKK5uBS70BCtfrIFg81sqcTVu3Ta+KD1Tk5vAtBNElWxtAa+m8K9A==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-events-to-acorn": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-position-from-estree": "^2.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-factory-destination": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz", + "integrity": "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-label": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz", + "integrity": "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-mdx-expression": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-factory-mdx-expression/-/micromark-factory-mdx-expression-2.0.3.tgz", + "integrity": "sha512-kQnEtA3vzucU2BkrIa8/VaSAsP+EJ3CKOvhMuJgOEGg9KDC6OAY6nSnNDVRiVNRqj7Y4SlSzcStaH/5jge8JdQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-events-to-acorn": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-position-from-estree": "^2.0.0", + "vfile-message": "^4.0.0" + } + }, + "node_modules/micromark-factory-space": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-title": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz", + "integrity": "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-whitespace": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz", + "integrity": "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-chunked": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz", + "integrity": "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-classify-character": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz", + "integrity": "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-combine-extensions": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz", + "integrity": "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-chunked": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-numeric-character-reference": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz", + "integrity": "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-string": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz", + "integrity": "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-encode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", + "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-events-to-acorn": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-util-events-to-acorn/-/micromark-util-events-to-acorn-2.0.3.tgz", + "integrity": "sha512-jmsiEIiZ1n7X1Rr5k8wVExBQCg5jy4UXVADItHmNk1zkwEVhBuIUKRu3fqv+hs4nxLISi2DQGlqIOGiFxgbfHg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "@types/unist": "^3.0.0", + "devlop": "^1.0.0", + "estree-util-visit": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "vfile-message": "^4.0.0" + } + }, + "node_modules/micromark-util-html-tag-name": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz", + "integrity": "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-normalize-identifier": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz", + "integrity": "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-resolve-all": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz", + "integrity": "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-sanitize-uri": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", + "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-subtokenize": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz", + "integrity": "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-types": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz", + "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/mrmime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", + "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "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/neotraverse": { + "version": "0.6.18", + "resolved": "https://registry.npmjs.org/neotraverse/-/neotraverse-0.6.18.tgz", + "integrity": "sha512-Z4SmBUweYa09+o6pG+eASabEpP6QkQ70yHj351pQoEXIs8uHbaU2DWVmzBANKgflPa47A50PtB2+NgRpQvr7vA==", + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/nlcst-to-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/nlcst-to-string/-/nlcst-to-string-4.0.0.tgz", + "integrity": "sha512-YKLBCcUYKAg0FNlOBT6aI91qFmSiFKiluk655WzPF+DDMA02qIyy8uiRqI8QXtcFpEvll12LpL5MXqEmAZ+dcA==", + "license": "MIT", + "dependencies": { + "@types/nlcst": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/node-fetch-native": { + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.7.tgz", + "integrity": "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==", + "license": "MIT" + }, + "node_modules/node-mock-http": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/node-mock-http/-/node-mock-http-1.0.4.tgz", + "integrity": "sha512-8DY+kFsDkNXy1sJglUfuODx1/opAGJGyrTuFqEoN90oRc2Vk0ZbD4K2qmKXBBEhZQzdKHIVfEJpDU8Ak2NJEvQ==", + "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==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/obug": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.3.tgz", + "integrity": "sha512-9miFgM2OFba7hB+pRgvtV84pYTBaoTHohvmIgiRt6dRIzbwEOIaNaP+dIlGs2fNFoB0SeISs0Jz5WFVRid6Xyg==", + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT", + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/ofetch": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/ofetch/-/ofetch-1.5.1.tgz", + "integrity": "sha512-2W4oUZlVaqAPAil6FUg/difl6YhqhUR7x2eZY4bQCko22UXg3hptq9KLQdqFClV+Wu85UX7hNtdGTngi/1BxcA==", + "license": "MIT", + "dependencies": { + "destr": "^2.0.5", + "node-fetch-native": "^1.6.7", + "ufo": "^1.6.1" + } + }, + "node_modules/ohash": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.11.tgz", + "integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==", + "license": "MIT" + }, + "node_modules/oniguruma-parser": { + "version": "0.12.2", + "resolved": "https://registry.npmjs.org/oniguruma-parser/-/oniguruma-parser-0.12.2.tgz", + "integrity": "sha512-6HVa5oIrgMC6aA6WF6XyyqbhRPJrKR02L20+2+zpDtO5QAzGHAUGw5TKQvwi5vctNnRHkJYmjAhRVQF2EKdTQw==", + "license": "MIT" + }, + "node_modules/oniguruma-to-es": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/oniguruma-to-es/-/oniguruma-to-es-4.3.6.tgz", + "integrity": "sha512-csuQ9x3Yr0cEIs/Zgx/OEt9iBw9vqIunAPQkx19R/fiMq2oGVTgcMqO/V3Ybqefr1TBvosI6jU539ksaBULJyA==", + "license": "MIT", + "dependencies": { + "oniguruma-parser": "^0.12.2", + "regex": "^6.1.0", + "regex-recursion": "^6.0.2" + } + }, + "node_modules/p-limit": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-7.3.0.tgz", + "integrity": "sha512-7cIXg/Z0M5WZRblrsOla88S4wAK+zOQQWeBYfV3qJuJXMr+LnbYjaadrFaS0JILfEDPVqHyKnZ1Z/1d6J9VVUw==", + "license": "MIT", + "dependencies": { + "yocto-queue": "^1.2.1" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-queue": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-9.3.0.tgz", + "integrity": "sha512-7NED7xhQ74Ngp4JP/2e0VZHp7vSWfJfqeiR92jPgxsz6m0Se4P03YoTKa9dDXyZ3r6P616gUXttrB6nnHYKang==", + "license": "MIT", + "dependencies": { + "eventemitter3": "^5.0.4", + "p-timeout": "^7.0.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-timeout": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-7.0.1.tgz", + "integrity": "sha512-AxTM2wDGORHGEkPCt8yqxOTMgpfbEHqF51f/5fJCmwFC3C/zNcGT63SymH2ttOAaiIws2zVg4+izQCjrakcwHg==", + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/package-manager-detector": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-1.6.0.tgz", + "integrity": "sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA==", + "license": "MIT" + }, + "node_modules/parse-entities": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz", + "integrity": "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "character-entities-legacy": "^3.0.0", + "character-reference-invalid": "^2.0.0", + "decode-named-character-reference": "^1.0.0", + "is-alphanumerical": "^2.0.0", + "is-decimal": "^2.0.0", + "is-hexadecimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/parse-entities/node_modules/@types/unist": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", + "license": "MIT" + }, + "node_modules/parse-latin": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/parse-latin/-/parse-latin-7.0.0.tgz", + "integrity": "sha512-mhHgobPPua5kZ98EF4HWiH167JWBfl4pvAIXXdbaVohtK7a6YBOy56kvhCqduqyo/f3yrHFWmqmiMg/BkBkYYQ==", + "license": "MIT", + "dependencies": { + "@types/nlcst": "^2.0.0", + "@types/unist": "^3.0.0", + "nlcst-to-string": "^4.0.0", + "unist-util-modify-children": "^4.0.0", + "unist-util-visit-children": "^3.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/piccolore": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/piccolore/-/piccolore-0.1.3.tgz", + "integrity": "sha512-o8bTeDWjE086iwKrROaDf31K0qC/BENdm15/uH9usSC/uZjJOKb2YGiVHfLY4GhwsERiPI1jmwI2XrA7ACOxVw==", + "license": "ISC" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.15", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", + "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.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prettier": { + "version": "3.8.4", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.4.tgz", + "integrity": "sha512-N2MylSdi48+5N/6S5j+maeHbUSIzzZ5uOcX5Hm4QpV8Dkb1HFjfAKTKX6yNPJQD9AhcT3ifHNB66tWTTJDi11Q==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prismjs": { + "version": "1.30.0", + "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.30.0.tgz", + "integrity": "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/property-information": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.2.0.tgz", + "integrity": "sha512-IAtzIB6sUiWaJYrX9smp3V46pBGbBeLFRGdh25kg1334VcBlD8HzhPeNIWQH9zhGmo2itIe25EHt9dQP7G5hmg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/radix3": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/radix3/-/radix3-1.1.2.tgz", + "integrity": "sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA==", + "license": "MIT" + }, + "node_modules/readdirp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz", + "integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==", + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/recma-build-jsx": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/recma-build-jsx/-/recma-build-jsx-1.0.0.tgz", + "integrity": "sha512-8GtdyqaBcDfva+GUKDr3nev3VpKAhup1+RvkMvUxURHpW7QyIvk9F5wz7Vzo06CEMSilw6uArgRqhpiUcWp8ew==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-util-build-jsx": "^3.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/recma-jsx": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/recma-jsx/-/recma-jsx-1.0.1.tgz", + "integrity": "sha512-huSIy7VU2Z5OLv6oFLosQGGDqPqdO1iq6bWNAdhzMxSJP7RAso4fCZ1cKu8j9YHCZf3TPrq4dw3okhrylgcd7w==", + "license": "MIT", + "dependencies": { + "acorn-jsx": "^5.0.0", + "estree-util-to-js": "^2.0.0", + "recma-parse": "^1.0.0", + "recma-stringify": "^1.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + }, + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/recma-parse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/recma-parse/-/recma-parse-1.0.0.tgz", + "integrity": "sha512-OYLsIGBB5Y5wjnSnQW6t3Xg7q3fQ7FWbw/vcXtORTnyaSFscOtABg+7Pnz6YZ6c27fG1/aN8CjfwoUEUIdwqWQ==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "esast-util-from-js": "^2.0.0", + "unified": "^11.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/recma-stringify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/recma-stringify/-/recma-stringify-1.0.0.tgz", + "integrity": "sha512-cjwII1MdIIVloKvC9ErQ+OgAtwHBmcZ0Bg4ciz78FtbT8In39aAYbaA7zvxQ61xVMSPE8WxhLwLbhif4Js2C+g==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-util-to-js": "^2.0.0", + "unified": "^11.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/regex/-/regex-6.1.0.tgz", + "integrity": "sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg==", + "license": "MIT", + "dependencies": { + "regex-utilities": "^2.3.0" + } + }, + "node_modules/regex-recursion": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/regex-recursion/-/regex-recursion-6.0.2.tgz", + "integrity": "sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==", + "license": "MIT", + "dependencies": { + "regex-utilities": "^2.3.0" + } + }, + "node_modules/regex-utilities": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/regex-utilities/-/regex-utilities-2.3.0.tgz", + "integrity": "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==", + "license": "MIT" + }, + "node_modules/rehype": { + "version": "13.0.2", + "resolved": "https://registry.npmjs.org/rehype/-/rehype-13.0.2.tgz", + "integrity": "sha512-j31mdaRFrwFRUIlxGeuPXXKWQxet52RBQRvCmzl5eCefn/KGbomK5GMHNMsOJf55fgo3qw5tST5neDuarDYR2A==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "rehype-parse": "^9.0.0", + "rehype-stringify": "^10.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rehype-parse": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/rehype-parse/-/rehype-parse-9.0.1.tgz", + "integrity": "sha512-ksCzCD0Fgfh7trPDxr2rSylbwq9iYDkSn8TCDmEJ49ljEUBxDVCzCHv7QNzZOfODanX4+bWQ4WZqLCRWYLfhag==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-from-html": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rehype-raw": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/rehype-raw/-/rehype-raw-7.0.0.tgz", + "integrity": "sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-raw": "^9.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rehype-recma": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/rehype-recma/-/rehype-recma-1.0.0.tgz", + "integrity": "sha512-lqA4rGUf1JmacCNWWZx0Wv1dHqMwxzsDWYMTowuplHF3xH0N/MmrZ/G3BDZnzAkRmxDadujCjaKM2hqYdCBOGw==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "@types/hast": "^3.0.0", + "hast-util-to-estree": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rehype-stringify": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/rehype-stringify/-/rehype-stringify-10.0.1.tgz", + "integrity": "sha512-k9ecfXHmIPuFVI61B9DeLPN0qFHfawM6RsuX48hoqlaKSF61RskNjSm1lI8PhBEM0MRdLxVVm4WmTqJQccH9mA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-to-html": "^9.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-gfm": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.1.tgz", + "integrity": "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-gfm": "^3.0.0", + "micromark-extension-gfm": "^3.0.0", + "remark-parse": "^11.0.0", + "remark-stringify": "^11.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-mdx": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/remark-mdx/-/remark-mdx-3.1.1.tgz", + "integrity": "sha512-Pjj2IYlUY3+D8x00UJsIOg5BEvfMyeI+2uLPn9VO9Wg4MEtN/VTIq2NEJQfde9PnX15KgtHyl9S0BcTnWrIuWg==", + "license": "MIT", + "dependencies": { + "mdast-util-mdx": "^3.0.0", + "micromark-extension-mdxjs": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-parse": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz", + "integrity": "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-rehype": { + "version": "11.1.2", + "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-11.1.2.tgz", + "integrity": "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "mdast-util-to-hast": "^13.0.0", + "unified": "^11.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-smartypants": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/remark-smartypants/-/remark-smartypants-3.0.2.tgz", + "integrity": "sha512-ILTWeOriIluwEvPjv67v7Blgrcx+LZOkAUVtKI3putuhlZm84FnqDORNXPPm+HY3NdZOMhyDwZ1E+eZB/Df5dA==", + "license": "MIT", + "dependencies": { + "retext": "^9.0.0", + "retext-smartypants": "^6.0.0", + "unified": "^11.0.4", + "unist-util-visit": "^5.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/remark-stringify": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-11.0.0.tgz", + "integrity": "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-to-markdown": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/retext": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/retext/-/retext-9.0.0.tgz", + "integrity": "sha512-sbMDcpHCNjvlheSgMfEcVrZko3cDzdbe1x/e7G66dFp0Ff7Mldvi2uv6JkJQzdRcvLYE8CA8Oe8siQx8ZOgTcA==", + "license": "MIT", + "dependencies": { + "@types/nlcst": "^2.0.0", + "retext-latin": "^4.0.0", + "retext-stringify": "^4.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/retext-latin": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/retext-latin/-/retext-latin-4.0.0.tgz", + "integrity": "sha512-hv9woG7Fy0M9IlRQloq/N6atV82NxLGveq+3H2WOi79dtIYWN8OaxogDm77f8YnVXJL2VD3bbqowu5E3EMhBYA==", + "license": "MIT", + "dependencies": { + "@types/nlcst": "^2.0.0", + "parse-latin": "^7.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/retext-smartypants": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/retext-smartypants/-/retext-smartypants-6.2.0.tgz", + "integrity": "sha512-kk0jOU7+zGv//kfjXEBjdIryL1Acl4i9XNkHxtM7Tm5lFiCog576fjNC9hjoR7LTKQ0DsPWy09JummSsH1uqfQ==", + "license": "MIT", + "dependencies": { + "@types/nlcst": "^2.0.0", + "nlcst-to-string": "^4.0.0", + "unist-util-visit": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/retext-stringify": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/retext-stringify/-/retext-stringify-4.0.0.tgz", + "integrity": "sha512-rtfN/0o8kL1e+78+uxPTqu1Klt0yPzKuQ2BfWwwfgIUSayyzxpM1PJzkKt4V8803uB9qSy32MvI7Xep9khTpiA==", + "license": "MIT", + "dependencies": { + "@types/nlcst": "^2.0.0", + "nlcst-to-string": "^4.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rollup": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.0.tgz", + "integrity": "sha512-nc72Wgq62I7rtDV4izT5/aaS0zxy3kttkinf9586ApknY3jZO9NYsmtc24fUckA0X7Q2v+ML4a15pdUlV5V/jA==", + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.62.0", + "@rollup/rollup-android-arm64": "4.62.0", + "@rollup/rollup-darwin-arm64": "4.62.0", + "@rollup/rollup-darwin-x64": "4.62.0", + "@rollup/rollup-freebsd-arm64": "4.62.0", + "@rollup/rollup-freebsd-x64": "4.62.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.0", + "@rollup/rollup-linux-arm-musleabihf": "4.62.0", + "@rollup/rollup-linux-arm64-gnu": "4.62.0", + "@rollup/rollup-linux-arm64-musl": "4.62.0", + "@rollup/rollup-linux-loong64-gnu": "4.62.0", + "@rollup/rollup-linux-loong64-musl": "4.62.0", + "@rollup/rollup-linux-ppc64-gnu": "4.62.0", + "@rollup/rollup-linux-ppc64-musl": "4.62.0", + "@rollup/rollup-linux-riscv64-gnu": "4.62.0", + "@rollup/rollup-linux-riscv64-musl": "4.62.0", + "@rollup/rollup-linux-s390x-gnu": "4.62.0", + "@rollup/rollup-linux-x64-gnu": "4.62.0", + "@rollup/rollup-linux-x64-musl": "4.62.0", + "@rollup/rollup-openbsd-x64": "4.62.0", + "@rollup/rollup-openharmony-arm64": "4.62.0", + "@rollup/rollup-win32-arm64-msvc": "4.62.0", + "@rollup/rollup-win32-ia32-msvc": "4.62.0", + "@rollup/rollup-win32-x64-gnu": "4.62.0", + "@rollup/rollup-win32-x64-msvc": "4.62.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/sax": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.0.tgz", + "integrity": "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=11.0.0" + } + }, + "node_modules/semver": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.4.tgz", + "integrity": "sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/sharp": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", + "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", + "hasInstallScript": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@img/colour": "^1.0.0", + "detect-libc": "^2.1.2", + "semver": "^7.7.3" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.34.5", + "@img/sharp-darwin-x64": "0.34.5", + "@img/sharp-libvips-darwin-arm64": "1.2.4", + "@img/sharp-libvips-darwin-x64": "1.2.4", + "@img/sharp-libvips-linux-arm": "1.2.4", + "@img/sharp-libvips-linux-arm64": "1.2.4", + "@img/sharp-libvips-linux-ppc64": "1.2.4", + "@img/sharp-libvips-linux-riscv64": "1.2.4", + "@img/sharp-libvips-linux-s390x": "1.2.4", + "@img/sharp-libvips-linux-x64": "1.2.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", + "@img/sharp-libvips-linuxmusl-x64": "1.2.4", + "@img/sharp-linux-arm": "0.34.5", + "@img/sharp-linux-arm64": "0.34.5", + "@img/sharp-linux-ppc64": "0.34.5", + "@img/sharp-linux-riscv64": "0.34.5", + "@img/sharp-linux-s390x": "0.34.5", + "@img/sharp-linux-x64": "0.34.5", + "@img/sharp-linuxmusl-arm64": "0.34.5", + "@img/sharp-linuxmusl-x64": "0.34.5", + "@img/sharp-wasm32": "0.34.5", + "@img/sharp-win32-arm64": "0.34.5", + "@img/sharp-win32-ia32": "0.34.5", + "@img/sharp-win32-x64": "0.34.5" + } + }, + "node_modules/shiki": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/shiki/-/shiki-4.2.0.tgz", + "integrity": "sha512-hjNax6o/ylDy9lefQEaSDtzaT3iVNtZ3WmpQnbuQNoG4xvnSKf2kSKbihZVO4JRG1TTMejs7CmNRYlWgAL66pQ==", + "license": "MIT", + "dependencies": { + "@shikijs/core": "4.2.0", + "@shikijs/engine-javascript": "4.2.0", + "@shikijs/engine-oniguruma": "4.2.0", + "@shikijs/langs": "4.2.0", + "@shikijs/themes": "4.2.0", + "@shikijs/types": "4.2.0", + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "license": "MIT" + }, + "node_modules/smol-toml": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/smol-toml/-/smol-toml-1.6.1.tgz", + "integrity": "sha512-dWUG8F5sIIARXih1DTaQAX4SsiTXhInKf1buxdY9DIg4ZYPZK5nGM1VRIYmEbDbsHt7USo99xSLFu5Q1IqTmsg==", + "license": "BSD-3-Clause", + "engines": { + "node": ">= 18" + }, + "funding": { + "url": "https://github.com/sponsors/cyyynthia" + } + }, + "node_modules/source-map": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", + "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">= 12" + } + }, + "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==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/space-separated-tokens": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", + "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/stringify-entities": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", + "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==", + "license": "MIT", + "dependencies": { + "character-entities-html4": "^2.0.0", + "character-entities-legacy": "^3.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/style-to-js": { + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.21.tgz", + "integrity": "sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ==", + "license": "MIT", + "dependencies": { + "style-to-object": "1.0.14" + } + }, + "node_modules/style-to-object": { + "version": "1.0.14", + "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.14.tgz", + "integrity": "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==", + "license": "MIT", + "dependencies": { + "inline-style-parser": "0.2.7" + } + }, + "node_modules/svgo": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/svgo/-/svgo-4.0.1.tgz", + "integrity": "sha512-XDpWUOPC6FEibaLzjfe0ucaV0YrOjYotGJO1WpF0Zd+n6ZGEQUsSugaoLq9QkEZtAfQIxT42UChcssDVPP3+/w==", + "license": "MIT", + "dependencies": { + "commander": "^11.1.0", + "css-select": "^5.1.0", + "css-tree": "^3.0.1", + "css-what": "^6.1.0", + "csso": "^5.0.5", + "picocolors": "^1.1.1", + "sax": "^1.5.0" + }, + "bin": { + "svgo": "bin/svgo.js" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/svgo" + } + }, + "node_modules/tailwindcss": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.1.tgz", + "integrity": "sha512-hk+TB1m+K8CYNrP6rjQaq/Y+4Zylwpa87mLYBKCunwnnQ9p+fHb7kmSfGqyEJoxF/O6CDyABWVFEafNSYKll+Q==", + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", + "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/tiny-inflate": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.3.tgz", + "integrity": "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==", + "license": "MIT" + }, + "node_modules/tinyclip": { + "version": "0.1.14", + "resolved": "https://registry.npmjs.org/tinyclip/-/tinyclip-0.1.14.tgz", + "integrity": "sha512-F1oWdz8tjT17qe1d5JgDK6z03WGOhYYAN0lK3/D/fzNiy93xswLLEw7pk+3g05onhAy6Bsc6PLNUGhdgVjemMQ==", + "license": "MIT", + "engines": { + "node": "^16.14.0 || >= 17.3.0" + } + }, + "node_modules/tinyexec": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.2.4.tgz", + "integrity": "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/trim-lines": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", + "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/trough": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz", + "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD", + "optional": true + }, + "node_modules/tw-animate-css": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/tw-animate-css/-/tw-animate-css-1.4.0.tgz", + "integrity": "sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Wombosvideo" + } + }, + "node_modules/ufo": { + "version": "1.6.4", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.4.tgz", + "integrity": "sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==", + "license": "MIT" + }, + "node_modules/ultrahtml": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/ultrahtml/-/ultrahtml-1.6.0.tgz", + "integrity": "sha512-R9fBn90VTJrqqLDwyMph+HGne8eqY1iPfYhPzZrvKpIfwkWZbcYlfpsb8B9dTvBfpy1/hqAD7Wi8EKfP9e8zdw==", + "license": "MIT" + }, + "node_modules/uncrypto": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/uncrypto/-/uncrypto-0.1.3.tgz", + "integrity": "sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q==", + "license": "MIT" + }, + "node_modules/unified": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", + "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "bail": "^2.0.0", + "devlop": "^1.0.0", + "extend": "^3.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unifont": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/unifont/-/unifont-0.7.4.tgz", + "integrity": "sha512-oHeis4/xl42HUIeHuNZRGEvxj5AaIKR+bHPNegRq5LV1gdc3jundpONbjglKpihmJf+dswygdMJn3eftGIMemg==", + "license": "MIT", + "dependencies": { + "css-tree": "^3.1.0", + "ofetch": "^1.5.1", + "ohash": "^2.0.11" + } + }, + "node_modules/unist-util-find-after": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-find-after/-/unist-util-find-after-5.0.0.tgz", + "integrity": "sha512-amQa0Ep2m6hE2g72AugUItjbuM8X8cGQnFoHk0pGfrFeT9GZhzN5SW8nRsiGKK7Aif4CrACPENkA6P/Lw6fHGQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-is": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz", + "integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-modify-children": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-modify-children/-/unist-util-modify-children-4.0.0.tgz", + "integrity": "sha512-+tdN5fGNddvsQdIzUF3Xx82CU9sMM+fA0dLgR9vOmT0oPT2jH+P1nd5lSqfCfXAw+93NhcXNY2qqvTUtE4cQkw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "array-iterate": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-position": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz", + "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-position-from-estree": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unist-util-position-from-estree/-/unist-util-position-from-estree-2.0.0.tgz", + "integrity": "sha512-KaFVRjoqLyF6YXCbVLNad/eS4+OfPQQn2yOd7zF/h5T/CSL2v8NpN6a5TPvtbXthAGw5nG+PuTtq+DdIZr+cRQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-remove-position": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-remove-position/-/unist-util-remove-position-5.0.0.tgz", + "integrity": "sha512-Hp5Kh3wLxv0PHj9m2yZhhLt58KzPtEYKQQ4yxfYFEO7EvHwzyDYnduhHnY1mDxoqr7VUwVuHXk9RXKIiYS1N8Q==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-visit": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.1.0.tgz", + "integrity": "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit-children": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/unist-util-visit-children/-/unist-util-visit-children-3.0.0.tgz", + "integrity": "sha512-RgmdTfSBOg04sdPcpTSD1jzoNBjt9a80/ZCzp5cI9n1qPzLZWF9YdvWGN2zmTumP1HWhXKdUWexjy/Wy/lJ7tA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit-parents": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz", + "integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unstorage": { + "version": "1.17.5", + "resolved": "https://registry.npmjs.org/unstorage/-/unstorage-1.17.5.tgz", + "integrity": "sha512-0i3iqvRfx29hkNntHyQvJTpf5W9dQ9ZadSoRU8+xVlhVtT7jAX57fazYO9EHvcRCfBCyi5YRya7XCDOsbTgkPg==", + "license": "MIT", + "dependencies": { + "anymatch": "^3.1.3", + "chokidar": "^5.0.0", + "destr": "^2.0.5", + "h3": "^1.15.10", + "lru-cache": "^11.2.7", + "node-fetch-native": "^1.6.7", + "ofetch": "^1.5.1", + "ufo": "^1.6.3" + }, + "peerDependencies": { + "@azure/app-configuration": "^1.8.0", + "@azure/cosmos": "^4.2.0", + "@azure/data-tables": "^13.3.0", + "@azure/identity": "^4.6.0", + "@azure/keyvault-secrets": "^4.9.0", + "@azure/storage-blob": "^12.26.0", + "@capacitor/preferences": "^6 || ^7 || ^8", + "@deno/kv": ">=0.9.0", + "@netlify/blobs": "^6.5.0 || ^7.0.0 || ^8.1.0 || ^9.0.0 || ^10.0.0", + "@planetscale/database": "^1.19.0", + "@upstash/redis": "^1.34.3", + "@vercel/blob": ">=0.27.1", + "@vercel/functions": "^2.2.12 || ^3.0.0", + "@vercel/kv": "^1 || ^2 || ^3", + "aws4fetch": "^1.0.20", + "db0": ">=0.2.1", + "idb-keyval": "^6.2.1", + "ioredis": "^5.4.2", + "uploadthing": "^7.4.4" + }, + "peerDependenciesMeta": { + "@azure/app-configuration": { + "optional": true + }, + "@azure/cosmos": { + "optional": true + }, + "@azure/data-tables": { + "optional": true + }, + "@azure/identity": { + "optional": true + }, + "@azure/keyvault-secrets": { + "optional": true + }, + "@azure/storage-blob": { + "optional": true + }, + "@capacitor/preferences": { + "optional": true + }, + "@deno/kv": { + "optional": true + }, + "@netlify/blobs": { + "optional": true + }, + "@planetscale/database": { + "optional": true + }, + "@upstash/redis": { + "optional": true + }, + "@vercel/blob": { + "optional": true + }, + "@vercel/functions": { + "optional": true + }, + "@vercel/kv": { + "optional": true + }, + "aws4fetch": { + "optional": true + }, + "db0": { + "optional": true + }, + "idb-keyval": { + "optional": true + }, + "ioredis": { + "optional": true + }, + "uploadthing": { + "optional": true + } + } + }, + "node_modules/vfile": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", + "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-location": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/vfile-location/-/vfile-location-5.0.3.tgz", + "integrity": "sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-message": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz", + "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vite": { + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.5.tgz", + "integrity": "sha512-KuOaNhcnGFN2zIPGA7wRmzF+lJA1sea7rHq17aiJ++9lzY1WWG6Jpwqwe1KNbRVPIqHmr8GLYx7jbrQcN/7/ww==", + "license": "MIT", + "dependencies": { + "esbuild": "^0.27.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vitefu": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-1.1.3.tgz", + "integrity": "sha512-ub4okH7Z5KLjb6hDyjqrGXqWtWvoYdU3IGm/NorpgHncKoLTCfRIbvlhBm7r0YstIaQRYlp4yEbFqDcKSzXSSg==", + "license": "MIT", + "workspaces": [ + "tests/deps/*", + "tests/projects/*", + "tests/projects/workspace/packages/*" + ], + "peerDependencies": { + "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "vite": { + "optional": true + } + } + }, + "node_modules/web-namespaces": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/web-namespaces/-/web-namespaces-2.0.1.tgz", + "integrity": "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/which-pm-runs": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/which-pm-runs/-/which-pm-runs-1.1.0.tgz", + "integrity": "sha512-n1brCuqClxfFfq/Rb0ICg9giSZqCS+pLtccdag6C2HyufBrh3fBOiy9nb6ggRMvWOVH5GrdJskj5iGTZNxd7SA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/xxhash-wasm": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/xxhash-wasm/-/xxhash-wasm-1.1.0.tgz", + "integrity": "sha512-147y/6YNh+tlp6nd/2pWq38i9h6mz/EuQ6njIrmW8D1BS5nCqs0P6DG+m6zTGnNz5I+uhZ0SHxBs9BsPrwcKDA==", + "license": "MIT" + }, + "node_modules/yargs-parser": { + "version": "22.0.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-22.0.0.tgz", + "integrity": "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==", + "license": "ISC", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=23" + } + }, + "node_modules/yocto-queue": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.2.2.tgz", + "integrity": "sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ==", + "license": "MIT", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", + "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zwitch": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", + "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + } + } +} diff --git a/app/package.json b/app/package.json new file mode 100644 index 0000000..2ae8453 --- /dev/null +++ b/app/package.json @@ -0,0 +1,25 @@ +{ + "name": "quiet-pages", + "version": "1.0.0", + "description": "A quiet Astro magazine theme for essays, field notes, and long-form writing.", + "private": true, + "license": "MIT", + "type": "module", + "scripts": { + "dev": "astro dev", + "build": "astro build", + "preview": "astro preview", + "format": "prettier --write ." + }, + "dependencies": { + "@astrojs/mdx": "^6.0.3", + "@tailwindcss/vite": "^4.3.1", + "astro": "^6.4.7", + "tailwindcss": "^4.3.1", + "tw-animate-css": "^1.4.0" + }, + "devDependencies": { + "prettier": "^3.8.4", + "vite": "^7.3.5" + } +} diff --git a/app/preview.webp b/app/preview.webp new file mode 100644 index 0000000..4e3fafc Binary files /dev/null and b/app/preview.webp differ diff --git a/app/src/assets/hero-placeholder.png b/app/src/assets/hero-placeholder.png new file mode 100644 index 0000000..6640456 Binary files /dev/null and b/app/src/assets/hero-placeholder.png differ diff --git a/app/src/components/Breadcrumbs.astro b/app/src/components/Breadcrumbs.astro new file mode 100644 index 0000000..af2a383 --- /dev/null +++ b/app/src/components/Breadcrumbs.astro @@ -0,0 +1,20 @@ +--- +import Icon from "./Icon.astro"; + +const { items = [] } = Astro.props; +--- + +<nav aria-label="Breadcrumb" class="flex flex-wrap items-center gap-1 text-xs text-muted-foreground"> + { + items.map((item, index) => ( + <span class="flex items-center gap-1"> + {index > 0 && <Icon name="chevron-right" class="h-3 w-3" />} + {item.to ? ( + <a href={item.to} class="hover:text-foreground">{item.label}</a> + ) : ( + <span class="text-foreground">{item.label}</span> + )} + </span> + )) + } +</nav> diff --git a/app/src/components/DevConsole.astro b/app/src/components/DevConsole.astro new file mode 100644 index 0000000..4c65d7c --- /dev/null +++ b/app/src/components/DevConsole.astro @@ -0,0 +1,221 @@ +--- +/** + * astroagent in-site console drawer — a multi-turn chat. + * Inject once in the base layout (before </body>). Ships to every visitor but + * stays hidden/inert unless /devconsole/ping reports the visitor is authed + * (token cookie). Real security is server-side on every endpoint; the DOM + * hiding is only UX. Unlock once by visiting any page with ?devkey=<TOKEN>. + */ +const route = "/devconsole"; +--- + +<div id="aa-console" data-route={route} hidden> + <button id="aa-handle" type="button" aria-label="Open developer console">▲ astroagent</button> + <section id="aa-panel" hidden aria-label="Developer console"> + <header> + <span class="aa-title">astroagent</span> + <span id="aa-state" class="aa-state"></span> + <button id="aa-key" type="button" aria-label="Agent auth token" title="Set agent auth token">🔑</button> + <button id="aa-new" type="button" aria-label="New conversation" title="New conversation (clear)">✚</button> + <button id="aa-lock" type="button" aria-label="Lock console" title="Lock (hide until next devkey)">🔒</button> + <button id="aa-close" type="button" aria-label="Collapse">▼</button> + </header> + <div id="aa-chat" aria-live="polite"></div> + <div id="aa-actions" hidden> + <a id="aa-preview" href="#" target="_blank" rel="noopener">Open preview ↗</a> + <button id="aa-publish" type="button">Publish</button> + </div> + <form id="aa-form"> + <textarea id="aa-prompt" rows="2" placeholder="Ask or change anything — “what pages do I have?”, “make the buttons green”, then “now make them bigger”"></textarea> + <button id="aa-run" type="submit">Send</button> + </form> + </section> + + <div id="aa-authmodal" hidden> + <div class="aa-modal"> + <div class="aa-modal-h">Agent sign-in required</div> + <p class="aa-modal-p">The agent's Claude session was lost. Paste a long-lived token — generate one on the server with <code>claude setup-token</code>.</p> + <input id="aa-authinput" type="password" placeholder="Paste auth token" autocomplete="off" spellcheck="false" /> + <div id="aa-authmsg" class="aa-modal-msg"></div> + <div class="aa-modal-btns"> + <button id="aa-authcancel" type="button">Cancel</button> + <button id="aa-authsave" type="button">Save & verify</button> + </div> + </div> + </div> +</div> + +<style> + #aa-console { position: fixed; right: 16px; bottom: 16px; z-index: 2147483000; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; } + #aa-handle { background:#111; color:#e6e6e6; border:1px solid #333; border-radius:8px; padding:8px 12px; font-size:12px; cursor:pointer; box-shadow:0 4px 16px rgba(0,0,0,.3); } + #aa-handle:hover { background:#1b1b1b; } + #aa-panel { width: min(700px, calc(100vw - 32px)); height: min(64vh, 560px); background:#0c0c0d; color:#d6d6d6; border:1px solid #2a2a2a; border-radius:10px; display:flex; flex-direction:column; box-shadow:0 12px 40px rgba(0,0,0,.5); overflow:hidden; } + #aa-panel header { display:flex; align-items:center; gap:8px; padding:8px 10px; background:#141416; border-bottom:1px solid #2a2a2a; font-size:12px; } + #aa-panel .aa-title { font-weight:600; color:#fff; } + #aa-panel .aa-state { margin-left:auto; color:#8a8a8a; font-size:11px; } + #aa-close, #aa-lock, #aa-key, #aa-new { background:none; border:none; color:#8a8a8a; cursor:pointer; font-size:12px; padding:2px 4px; } + #aa-lock:hover, #aa-close:hover, #aa-key:hover, #aa-new:hover { color:#fff; } + #aa-chat { flex:1; overflow:auto; padding:12px; display:flex; flex-direction:column; gap:8px; } + .aa-msg { max-width:86%; padding:8px 11px; border-radius:9px; font-size:12px; line-height:1.5; white-space:pre-wrap; word-break:break-word; } + .aa-user { align-self:flex-end; background:rgba(31,111,235,.22); border:1px solid rgba(31,111,235,.45); color:#dbeafe; } + .aa-bot { align-self:flex-start; background:#161b22; border:1px solid #2a2a2a; color:#d6d6d6; } + .aa-bot .aa-tool { display:block; color:#6cb6ff; opacity:.75; font-size:11px; } + .aa-bot .aa-txt { display:block; margin:2px 0; } + .aa-bot .aa-emsg { color:#ff7b72; } + .aa-sys { align-self:center; color:#8a8a8a; font-size:11px; } + .aa-sys.ok { color:#7ee787; } + #aa-actions { display:flex; gap:8px; align-items:center; padding:8px 10px; border-top:1px solid #2a2a2a; } + #aa-actions a { color:#6cb6ff; font-size:12px; text-decoration:none; margin-right:auto; } + #aa-form { display:flex; gap:8px; padding:10px; border-top:1px solid #2a2a2a; } + #aa-prompt { flex:1; resize:none; background:#141416; color:#e6e6e6; border:1px solid #2a2a2a; border-radius:6px; padding:8px; font:inherit; font-size:12px; } + #aa-panel button:not(#aa-close):not(#aa-lock):not(#aa-key):not(#aa-new) { background:#238636; color:#fff; border:none; border-radius:6px; padding:8px 14px; font-size:12px; cursor:pointer; } + #aa-panel button:disabled { opacity:.5; cursor:default; } + #aa-authmodal { position:fixed; inset:0; background:rgba(0,0,0,.55); display:flex; align-items:center; justify-content:center; z-index:2147483001; } + #aa-authmodal .aa-modal { background:#141416; color:#e6e6e6; border:1px solid #2a2a2a; border-radius:10px; padding:18px; width:min(440px, calc(100vw - 32px)); box-shadow:0 12px 40px rgba(0,0,0,.6); } + .aa-modal-h { font-weight:600; color:#fff; margin-bottom:6px; } + .aa-modal-p { font-size:12px; color:#9a9a9a; margin:0 0 12px; line-height:1.5; } + .aa-modal-p code { color:#6cb6ff; } + #aa-authinput { width:100%; box-sizing:border-box; background:#0c0c0d; color:#e6e6e6; border:1px solid #2a2a2a; border-radius:6px; padding:9px; font:inherit; font-size:12px; } + .aa-modal-msg { font-size:12px; min-height:16px; margin:8px 0; } + .aa-modal-btns { display:flex; gap:8px; justify-content:flex-end; } + #aa-authsave { background:#238636; color:#fff; border:none; border-radius:6px; padding:8px 14px; font-size:12px; cursor:pointer; } + #aa-authcancel { background:#30363d !important; color:#fff; border:none; border-radius:6px; padding:8px 14px; font-size:12px; cursor:pointer; } +</style> + +<script> + const root = document.getElementById("aa-console"); + const route = root.dataset.route; + const $ = (id) => document.getElementById(id); + const api = (p) => route + p; + + async function boot() { + const devkey = new URLSearchParams(location.search).get("devkey"); + let authed = false; + try { + const r = await fetch(api("/ping") + (devkey ? "?key=" + encodeURIComponent(devkey) : ""), { credentials: "same-origin" }); + authed = (await r.json()).authed; + } catch { authed = false; } + if (!authed) { root.remove(); return; } + root.hidden = false; + wire(); + } + + function wire() { + const handle = $("aa-handle"), panel = $("aa-panel"); + const chat = $("aa-chat"), stateEl = $("aa-state"), actions = $("aa-actions"); + const previewLink = $("aa-preview"), runBtn = $("aa-run"), promptEl = $("aa-prompt"); + let convId = null, es = null, bot = null, running = false; + + const open = () => { panel.hidden = false; handle.hidden = true; promptEl.focus(); }; + const close = () => { panel.hidden = true; handle.hidden = false; }; + handle.onclick = open; $("aa-close").onclick = close; + $("aa-lock").onclick = async () => { + try { await fetch(api("/logout"), { method:"POST", credentials:"same-origin" }); } catch {} + root.remove(); + }; + + const scroll = () => { chat.scrollTop = chat.scrollHeight; }; + const setState = (s) => (stateEl.textContent = s || ""); + const busy = (b) => { running = b; runBtn.disabled = b; }; + + function bubble(cls) { const d = document.createElement("div"); d.className = "aa-msg " + cls; chat.appendChild(d); scroll(); return d; } + function userMsg(t) { const d = bubble("aa-user"); d.textContent = t; } + function sysMsg(t, ok) { const d = bubble("aa-sys" + (ok ? " ok" : "")); d.textContent = t; } + function botStart() { bot = bubble("aa-bot"); return bot; } + function botText(t) { if (!bot) botStart(); const s = document.createElement("span"); s.className = "aa-txt"; s.textContent = t; bot.appendChild(s); scroll(); } + function botTool(t) { if (!bot) botStart(); const s = document.createElement("span"); s.className = "aa-tool"; s.textContent = "· " + t; bot.appendChild(s); scroll(); } + function botErr(t) { if (!bot) botStart(); const s = document.createElement("span"); s.className = "aa-txt aa-emsg"; s.textContent = "✗ " + t; bot.appendChild(s); scroll(); } + + function ensureStream() { + if (es || !convId) return; + es = new EventSource(api("/stream") + "?conversationId=" + encodeURIComponent(convId)); + es.onmessage = (m) => { + let ev; try { ev = JSON.parse(m.data); } catch { return; } + switch (ev.type) { + case "turn_start": setState("working…"); break; + case "progress": + if (ev.kind === "text") botText(ev.text); + else if (ev.kind === "tool") botTool(ev.text); + else if (ev.kind === "log") setState("building preview…"); + break; // ignore 'done' (duplicate of final text) + case "preview": + previewLink.href = ev.url; actions.hidden = false; + sysMsg("✓ preview updated", true); + break; + case "turn_end": setState(""); busy(false); bot = null; break; + case "published": sysMsg("✓ published — " + (ev.commit || "").slice(0,8) + " (live)", true); actions.hidden = true; break; + case "auth_required": setState(""); busy(false); botErr("agent not signed in"); openAuth(); break; + case "error": setState(""); busy(false); botErr(ev.text || "error"); break; + case "end": es && es.close(); es = null; convId = null; busy(false); setState(""); break; + } + }; + es.onerror = () => { /* keep-alive; browser auto-reconnects */ }; + } + + $("aa-form").onsubmit = async (e) => { + e.preventDefault(); + if (running) return; + const message = promptEl.value.trim(); + if (!message) return; + promptEl.value = ""; + userMsg(message); botStart(); busy(true); setState("working…"); + try { + const r = await fetch(api("/run"), { + method:"POST", credentials:"same-origin", headers:{ "content-type":"application/json" }, + body: JSON.stringify(convId ? { conversationId: convId, message } : { message }), + }); + const d = await r.json(); + if (!r.ok) throw new Error(d.error || "run failed"); + convId = d.conversationId; + ensureStream(); + } catch (err) { botErr(err.message); busy(false); setState(""); } + }; + + $("aa-publish").onclick = async () => { + if (!convId || running) return; + setState("publishing…"); + try { + const r = await fetch(api("/publish"), { method:"POST", credentials:"same-origin", headers:{ "content-type":"application/json" }, body: JSON.stringify({ conversationId: convId }) }); + const d = await r.json(); + if (!r.ok) throw new Error(d.error || "publish failed"); + } catch (err) { sysMsg("✗ " + err.message); } + setState(""); + }; + + $("aa-new").onclick = async () => { + if (running) return; + try { await fetch(api("/discard"), { method:"POST", credentials:"same-origin", headers:{ "content-type":"application/json" }, body: JSON.stringify({ conversationId: convId }) }); } catch {} + if (es) { es.close(); es = null; } + convId = null; bot = null; chat.textContent = ""; actions.hidden = true; setState(""); + sysMsg("new conversation"); + promptEl.focus(); + }; + + // Enter to send, Shift+Enter for newline. + promptEl.addEventListener("keydown", (e) => { + if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); $("aa-form").requestSubmit(); } + }); + + // --- agent auth token modal (pops up on auth loss, or via the 🔑 button) --- + const authModal = $("aa-authmodal"), authInput = $("aa-authinput"), authMsg = $("aa-authmsg"); + const openAuth = () => { authMsg.textContent = ""; authInput.value = ""; authModal.hidden = false; authInput.focus(); }; + const closeAuth = () => { authModal.hidden = true; }; + $("aa-key").onclick = openAuth; + $("aa-authcancel").onclick = closeAuth; + $("aa-authsave").onclick = async () => { + const token = authInput.value.trim(); + if (!token) return; + authMsg.style.color = "#9a9a9a"; authMsg.textContent = "verifying…"; + try { + const r = await fetch(api("/auth"), { method:"POST", credentials:"same-origin", headers:{ "content-type":"application/json" }, body: JSON.stringify({ token }) }); + const d = await r.json(); + if (d.ok) { authMsg.style.color = "#7ee787"; authMsg.textContent = "✓ saved & verified — send your message again"; setTimeout(closeAuth, 1300); } + else { authMsg.style.color = "#ff7b72"; authMsg.textContent = "✗ " + (d.error || "failed"); } + } catch (err) { authMsg.style.color = "#ff7b72"; authMsg.textContent = "✗ " + err.message; } + }; + // expose for the stream handler + window.__aaOpenAuth = openAuth; + } + + boot(); +</script> diff --git a/app/src/components/Footer.astro b/app/src/components/Footer.astro new file mode 100644 index 0000000..3a5d6de --- /dev/null +++ b/app/src/components/Footer.astro @@ -0,0 +1,51 @@ +--- +import Icon from "./Icon.astro"; +import { SITE, categories } from "../lib/blog-data.js"; + +const { flush = false } = Astro.props; +--- + +<footer class:list={[flush ? "mt-0" : "mt-24", "border-t border-border/60"]}> + <div class="mx-auto grid max-w-6xl gap-10 px-5 py-12 md:grid-cols-4"> + <div class="md:col-span-2"> + <div class="font-serif text-lg font-semibold">{SITE.name}</div> + <p class="mt-3 max-w-sm text-sm leading-relaxed text-muted-foreground"> + {SITE.description} + </p> + <div class="mt-4 flex items-center gap-3 text-muted-foreground"> + <a href="/rss.xml" aria-label="RSS" class="hover:text-foreground"><Icon name="rss" class="h-4 w-4" /></a> + <a href="https://x.com/hicarlosarias" aria-label="Comiida on X" class="hover:text-foreground"><Icon name="twitter" class="h-4 w-4" /></a> + </div> + </div> + <div> + <div class="text-xs font-medium uppercase tracking-wider text-muted-foreground">Sections</div> + <ul class="mt-3 space-y-2 text-sm"> + { + categories.slice(0, 5).map((category) => ( + <li> + <a href={`/categories/${category.slug}`} class="text-foreground/80 hover:text-foreground"> + {category.name} + </a> + </li> + )) + } + </ul> + </div> + <div> + <div class="text-xs font-medium uppercase tracking-wider text-muted-foreground">The site</div> + <ul class="mt-3 space-y-2 text-sm"> + <li><a href="/about" class="hover:text-foreground">About</a></li> + <li><a href="/faq" class="hover:text-foreground">FAQ</a></li> + <li><a href="/contact" class="hover:text-foreground">Contact</a></li> + <li><a href="/blog" class="hover:text-foreground">Archive</a></li> + <li><a href="/rss.xml" class="hover:text-foreground">RSS feed</a></li> + </ul> + </div> + </div> + <div class="border-t border-border/60"> + <div class="mx-auto flex max-w-6xl flex-col items-start justify-between gap-2 px-5 py-5 text-xs text-muted-foreground sm:flex-row sm:items-center"> + <div>© {new Date().getFullYear()} {SITE.name}. Dining in Medellín.</div> + <div>Set in Fraunces & Inter.</div> + </div> + </div> +</footer> diff --git a/app/src/components/Header.astro b/app/src/components/Header.astro new file mode 100644 index 0000000..f4c3dc8 --- /dev/null +++ b/app/src/components/Header.astro @@ -0,0 +1,200 @@ +--- +import Icon from "./Icon.astro"; +import { SITE } from "../lib/blog-data.js"; + +const nav = [ + { to: "/", label: "Home" }, + { to: "/blog", label: "Writing" }, + { to: "/about", label: "About" }, + { to: "/services", label: "Services" }, + { to: "/contact", label: "Contact" }, +]; + +const current = Astro.url.pathname.replace(/\/$/, "") || "/"; +const isHome = current === "/"; +const activeClass = isHome ? "home-header-link is-active" : "text-foreground"; +const inactiveClass = isHome + ? "home-header-link" + : "text-muted-foreground transition-colors hover:text-foreground"; +const headerClass = isHome + ? "sticky top-0 z-40 border-b border-transparent bg-transparent text-white" + : "sticky top-0 z-40 border-b border-border/60 bg-background/80 backdrop-blur-md"; +const iconClass = isHome + ? "home-header-action inline-flex h-9 w-9 items-center justify-center rounded-full transition-colors" + : "inline-flex h-9 w-9 items-center justify-center rounded-full text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"; +--- + +<header class={headerClass} data-home-header={isHome ? "true" : undefined} data-scrolled="false"> + <div class="relative mx-auto flex h-16 max-w-6xl items-center justify-between px-5"> + <a href="/" class="flex items-center gap-2"> + <span class={`font-serif text-xl font-semibold tracking-tight ${isHome ? "home-header-brand" : ""}`}>{SITE.name}</span> + </a> + + <nav class="hidden items-center gap-8 md:flex" aria-label="Primary navigation"> + { + nav.map((item) => { + const exact = item.to === "/"; + const active = exact ? current === "/" : current.startsWith(item.to); + return ( + <a + href={item.to} + class={`text-sm ${active ? activeClass : inactiveClass}`} + aria-current={active ? "page" : undefined} + > + {item.label} + </a> + ); + }) + } + </nav> + + <div class="flex items-center gap-1"> + <button + type="button" + data-search-toggle + aria-label="Search" + aria-controls="site-search" + aria-expanded="false" + class={iconClass} + > + <Icon name="search" class="h-4 w-4" /> + </button> + <a + href="/rss.xml" + aria-label="RSS feed" + class={`${iconClass} hidden sm:inline-flex`} + > + <Icon name="rss" class="h-4 w-4" /> + </a> + <button + type="button" + data-theme-toggle + aria-label="Toggle dark mode" + class={iconClass} + > + <span data-theme-icon="moon"><Icon name="moon" class="h-4 w-4" /></span> + <span data-theme-icon="sun" hidden><Icon name="sun" class="h-4 w-4" /></span> + </button> + <button + type="button" + data-menu-toggle + aria-label="Menu" + aria-controls="mobile-menu" + aria-expanded="false" + class={`${iconClass} md:hidden`} + > + <span data-menu-icon="menu"><Icon name="menu" class="h-4 w-4" /></span> + <span data-menu-icon="x" hidden><Icon name="x" class="h-4 w-4" /></span> + </button> + </div> + </div> + + <div id="site-search" data-search-panel class="border-t border-border/60 bg-background text-foreground" hidden> + <form action="/blog" method="get" class="mx-auto max-w-6xl px-5 py-3"> + <input + data-search-input + name="q" + aria-label="Search essays, field notes, interviews" + placeholder="Search essays, field notes, interviews..." + class="w-full border-0 bg-transparent py-2 font-serif text-lg outline-none placeholder:text-muted-foreground" + /> + </form> + </div> + + <div id="mobile-menu" data-menu-panel class="border-t border-border/60 bg-background text-foreground md:hidden" hidden> + <nav class="mx-auto flex max-w-6xl flex-col gap-1 px-5 py-3" aria-label="Mobile navigation"> + { + nav.map((item) => { + const exact = item.to === "/"; + const active = exact ? current === "/" : current.startsWith(item.to); + return ( + <a + href={item.to} + class={`rounded-md px-2 py-2 text-sm hover:bg-muted hover:text-foreground ${active ? "text-foreground" : "text-muted-foreground"}`} + aria-current={active ? "page" : undefined} + > + {item.label} + </a> + ); + }) + } + </nav> + </div> +</header> + +<script> + const searchToggle = document.querySelector("[data-search-toggle]"); + const searchPanel = document.querySelector("[data-search-panel]"); + const searchInput = document.querySelector("[data-search-input]"); + const menuToggle = document.querySelector("[data-menu-toggle]"); + const menuPanel = document.querySelector("[data-menu-panel]"); + const menuIcon = document.querySelector('[data-menu-icon="menu"]'); + const closeIcon = document.querySelector('[data-menu-icon="x"]'); + const themeToggle = document.querySelector("[data-theme-toggle]"); + const moonIcon = document.querySelector('[data-theme-icon="moon"]'); + const sunIcon = document.querySelector('[data-theme-icon="sun"]'); + const homeHeader = document.querySelector("[data-home-header]"); + + const setPanelOpen = () => { + if (!homeHeader) return; + const open = !searchPanel.hidden || !menuPanel.hidden; + homeHeader.dataset.panelOpen = String(open); + }; + + const setMenu = (open) => { + menuPanel.hidden = !open; + menuIcon.hidden = open; + closeIcon.hidden = !open; + menuToggle?.setAttribute("aria-expanded", String(open)); + setPanelOpen(); + }; + + const setSearch = (open) => { + searchPanel.hidden = !open; + searchToggle?.setAttribute("aria-expanded", String(open)); + if (open) searchInput?.focus(); + setPanelOpen(); + }; + + const setThemeIcon = () => { + const dark = document.documentElement.classList.contains("dark"); + moonIcon.hidden = dark; + sunIcon.hidden = !dark; + }; + + setThemeIcon(); + + const setHeaderScrolled = () => { + if (!homeHeader) return; + homeHeader.dataset.scrolled = window.scrollY > 8 ? "true" : "false"; + }; + setHeaderScrolled(); + window.addEventListener("scroll", setHeaderScrolled, { passive: true }); + + searchToggle?.addEventListener("click", () => { + setSearch(searchPanel.hidden); + }); + + menuToggle?.addEventListener("click", () => setMenu(menuPanel.hidden)); + + document.addEventListener("keydown", (event) => { + if (event.key !== "Escape") return; + + if (!searchPanel.hidden) { + setSearch(false); + searchToggle?.focus(); + } + + if (!menuPanel.hidden) { + setMenu(false); + menuToggle?.focus(); + } + }); + + themeToggle?.addEventListener("click", () => { + const next = !document.documentElement.classList.contains("dark"); + document.documentElement.classList.toggle("dark", next); + localStorage.setItem("theme", next ? "dark" : "light"); + setThemeIcon(); + }); +</script> diff --git a/app/src/components/Icon.astro b/app/src/components/Icon.astro new file mode 100644 index 0000000..910140c --- /dev/null +++ b/app/src/components/Icon.astro @@ -0,0 +1,38 @@ +--- +const { name, class: className = "" } = Astro.props; + +const paths = { + "arrow-left": '<path d="m12 19-7-7 7-7"></path><path d="M19 12H5"></path>', + "arrow-right": '<path d="M5 12h14"></path><path d="m12 5 7 7-7 7"></path>', + check: '<path d="M20 6 9 17l-5-5"></path>', + "chevron-right": '<path d="m9 18 6-6-6-6"></path>', + copy: '<rect width="14" height="14" x="8" y="8" rx="2" ry="2"></rect><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"></path>', + github: '<path d="M15 22v-4a4.8 4.8 0 0 0-1-3.5c3 0 6-2 6-5.5.08-1.25-.27-2.48-1-3.5.28-1.15.28-2.35 0-3.5 0 0-1 0-3 1.5a10.4 10.4 0 0 0-5 0C9 2 8 2 8 2c-.3 1.15-.3 2.35 0 3.5A5.4 5.4 0 0 0 7 9c0 3.5 3 5.5 6 5.5-.39.49-.68 1.05-.85 1.65-.17.6-.22 1.23-.15 1.85v4"></path><path d="M9 18c-4.5 2-5-2-7-2"></path>', + link: '<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>', + linkedin: '<path d="M16 8a6 6 0 0 1 6 6v7h-4v-7a2 2 0 0 0-4 0v7h-4v-7a6 6 0 0 1 6-6z"></path><rect width="4" height="12" x="2" y="9"></rect><circle cx="4" cy="4" r="2"></circle>', + mail: '<rect width="20" height="16" x="2" y="4" rx="2"></rect><path d="m22 7-10 5L2 7"></path>', + menu: '<path d="M4 12h16"></path><path d="M4 6h16"></path><path d="M4 18h16"></path>', + message: '<path d="M21 15a4 4 0 0 1-4 4H7l-4 4V7a4 4 0 0 1 4-4h10a4 4 0 0 1 4 4z"></path>', + moon: '<path d="M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z"></path>', + rss: '<path d="M4 11a9 9 0 0 1 9 9"></path><path d="M4 4a16 16 0 0 1 16 16"></path><circle cx="5" cy="19" r="1"></circle>', + search: '<path d="m21 21-4.34-4.34"></path><circle cx="11" cy="11" r="8"></circle>', + sun: '<circle cx="12" cy="12" r="4"></circle><path d="M12 2v2"></path><path d="M12 20v2"></path><path d="m4.93 4.93 1.41 1.41"></path><path d="m17.66 17.66 1.41 1.41"></path><path d="M2 12h2"></path><path d="M20 12h2"></path><path d="m6.34 17.66-1.41 1.41"></path><path d="m19.07 4.93-1.41 1.41"></path>', + twitter: '<path d="M22 4.01c-.77.35-1.6.58-2.47.69a4.3 4.3 0 0 0 1.89-2.38 8.6 8.6 0 0 1-2.73 1.04A4.28 4.28 0 0 0 11.4 7.27c0 .34.04.67.11.99A12.14 12.14 0 0 1 2.69 3.8a4.28 4.28 0 0 0 1.32 5.72 4.2 4.2 0 0 1-1.94-.54v.05a4.28 4.28 0 0 0 3.44 4.2 4.3 4.3 0 0 1-1.93.07 4.29 4.29 0 0 0 4 2.97A8.6 8.6 0 0 1 2.25 18.1c-.35 0-.7-.02-1.04-.06A12.13 12.13 0 0 0 7.77 20c7.87 0 12.18-6.52 12.18-12.18v-.56A8.7 8.7 0 0 0 22 4.01Z"></path>', + x: '<path d="M18 6 6 18"></path><path d="m6 6 12 12"></path>', +}; +--- + +<svg + class={className} + xmlns="http://www.w3.org/2000/svg" + width="24" + height="24" + viewBox="0 0 24 24" + fill="none" + stroke="currentColor" + stroke-width="2" + stroke-linecap="round" + stroke-linejoin="round" + aria-hidden="true" + set:html={paths[name] ?? ""} +/> diff --git a/app/src/components/Newsletter.astro b/app/src/components/Newsletter.astro new file mode 100644 index 0000000..493a11c --- /dev/null +++ b/app/src/components/Newsletter.astro @@ -0,0 +1,61 @@ +--- +const { compact = false } = Astro.props; +--- + +{ + compact ? ( + <div> + <div class="text-xs font-medium uppercase tracking-wider text-muted-foreground">Newsletter</div> + <p class="mt-2 text-sm text-muted-foreground">Medellín dining in your inbox — new guides and openings.</p> + <form data-newsletter-form class="mt-3 flex gap-2"> + <input + type="email" + required + aria-label="Email address" + placeholder="you@email.com" + class="min-w-0 flex-1 rounded-md border border-input bg-background px-3 py-2 text-sm outline-none focus:border-primary" + /> + <button type="submit" class="rounded-md bg-foreground px-3 py-2 text-sm font-medium text-background transition-opacity hover:opacity-90"> + Join + </button> + </form> + <p data-newsletter-done class="mt-2 text-xs text-primary" role="status" hidden>Thanks — check your inbox.</p> + </div> + ) : ( + <section data-newsletter-cta class="border-t border-border/60 pt-16 pb-10"> + <div class="mx-auto max-w-2xl px-5 text-center"> + <h2 class="font-serif text-3xl font-semibold tracking-tight sm:text-4xl">Eat well in Medellín.</h2> + <p class="mt-3 text-muted-foreground"> + One email when it’s worth it — new restaurant guides, openings, and data-driven dining picks for Medellín. + </p> + <form data-newsletter-form class="mx-auto mt-6 flex max-w-md flex-col gap-2 sm:flex-row"> + <input + type="email" + required + aria-label="Email address" + placeholder="you@email.com" + class="min-w-0 flex-1 rounded-md border border-input bg-background px-4 py-3 text-sm outline-none focus:border-primary" + /> + <button type="submit" class="rounded-md bg-foreground px-5 py-3 text-sm font-medium text-background transition-opacity hover:opacity-90"> + Subscribe + </button> + </form> + <p data-newsletter-done class="mt-3 text-sm text-primary" role="status" hidden>Thanks — check your inbox to confirm.</p> + <p class="mt-3 text-xs text-muted-foreground">Free. Unsubscribe in one click.</p> + </div> + </section> + ) +} + +<script> + document.addEventListener("submit", (event) => { + const form = event.target; + if (!(form instanceof HTMLFormElement) || !form.matches("[data-newsletter-form]")) return; + event.preventDefault(); + const input = form.querySelector("input"); + if (!input?.value) return; + input.value = ""; + const done = form.parentElement?.querySelector("[data-newsletter-done]"); + if (done) done.hidden = false; + }); +</script> diff --git a/app/src/components/PostCard.astro b/app/src/components/PostCard.astro new file mode 100644 index 0000000..adb0a69 --- /dev/null +++ b/app/src/components/PostCard.astro @@ -0,0 +1,148 @@ +--- +import { Image } from "astro:assets"; +import { getAuthor, getCategory, formatDate } from "../lib/blog-data.js"; + +const { post, variant = "default", hidden = false } = Astro.props; +const author = getAuthor(post.author); +const category = getCategory(post.category); +const searchable = `${post.title} ${post.excerpt}`.toLowerCase(); +const thumbnailIsString = typeof post.thumbnail === "string"; +--- + +{ + variant === "compact" ? ( + <a + href={`/blog/${post.slug}`} + class="group block" + data-post-card + data-category={post.category} + data-tags={post.tags.join(" ")} + data-search={searchable} + hidden={hidden} + > + <div class="text-xs uppercase tracking-wider text-muted-foreground">{category?.name}</div> + <h3 class="mt-1 font-serif text-lg font-semibold leading-snug tracking-tight text-foreground group-hover:text-primary"> + {post.title} + </h3> + <div class="mt-1 text-xs text-muted-foreground"> + {formatDate(post.date)} · {post.readingTime} min + </div> + </a> + ) : variant === "list" ? ( + <article + class="group grid gap-6 border-b border-border/60 py-8 sm:grid-cols-[1fr_220px]" + data-post-card + data-category={post.category} + data-tags={post.tags.join(" ")} + data-search={searchable} + hidden={hidden} + > + <div> + <div class="flex items-center gap-3 text-xs uppercase tracking-wider text-muted-foreground"> + {category && ( + <a href={`/categories/${category.slug}`} class="hover:text-foreground"> + {category.name} + </a> + )} + <span>·</span> + <time datetime={post.date}>{formatDate(post.date)}</time> + </div> + <a href={`/blog/${post.slug}`}> + <h2 class="mt-2 font-serif text-2xl font-semibold leading-tight tracking-tight group-hover:text-primary sm:text-3xl"> + {post.title} + </h2> + </a> + <p class="mt-3 text-muted-foreground">{post.excerpt}</p> + <div class="mt-4 flex items-center gap-3 text-xs text-muted-foreground"> + {author && ( + <a href={`/authors/${author.slug}`} class="hover:text-foreground"> + {author.name} + </a> + )} + <span>·</span> + <span>{post.readingTime} min read</span> + </div> + </div> + {post.thumbnail && ( + <a href={`/blog/${post.slug}`} class="block" aria-label={`Read ${post.title}`}> + <div class="aspect-[4/3] overflow-hidden rounded-md bg-muted sm:aspect-square"> + { + thumbnailIsString ? ( + <img + src={post.thumbnail} + alt="" + width="440" + height="330" + loading="lazy" + class="h-full w-full object-cover transition-transform duration-500 group-hover:scale-[1.02]" + /> + ) : ( + <Image + src={post.thumbnail} + alt="" + loading="lazy" + class="h-full w-full object-cover transition-transform duration-500 group-hover:scale-[1.02]" + /> + ) + } + </div> + </a> + )} + </article> + ) : ( + <article + class="group" + data-post-card + data-category={post.category} + data-tags={post.tags.join(" ")} + data-search={searchable} + hidden={hidden} + > + {post.thumbnail && ( + <a href={`/blog/${post.slug}`} class="block" aria-label={`Read ${post.title}`}> + <div class="aspect-[16/10] overflow-hidden rounded-md bg-muted"> + { + thumbnailIsString ? ( + <img + src={post.thumbnail} + alt="" + width="640" + height="400" + loading="lazy" + class="h-full w-full object-cover transition-transform duration-500 group-hover:scale-[1.02]" + /> + ) : ( + <Image + src={post.thumbnail} + alt="" + loading="lazy" + class="h-full w-full object-cover transition-transform duration-500 group-hover:scale-[1.02]" + /> + ) + } + </div> + </a> + )} + <div class="mt-4"> + <div class="flex items-center gap-2 text-xs uppercase tracking-wider text-muted-foreground"> + {category && ( + <a href={`/categories/${category.slug}`} class="hover:text-foreground"> + {category.name} + </a> + )} + <span>·</span> + <time datetime={post.date}>{formatDate(post.date)}</time> + </div> + <a href={`/blog/${post.slug}`}> + <h3 class="mt-2 font-serif text-xl font-semibold leading-snug tracking-tight group-hover:text-primary"> + {post.title} + </h3> + </a> + <p class="mt-2 line-clamp-2 text-sm text-muted-foreground">{post.excerpt}</p> + <div class="mt-3 text-xs text-muted-foreground"> + {author?.name} · {post.readingTime} min + </div> + </div> + </article> + ) +} diff --git a/app/src/components/Sidebar.astro b/app/src/components/Sidebar.astro new file mode 100644 index 0000000..5724c18 --- /dev/null +++ b/app/src/components/Sidebar.astro @@ -0,0 +1,74 @@ +--- +import Icon from "./Icon.astro"; +import PostCard from "./PostCard.astro"; +import Newsletter from "./Newsletter.astro"; +import { categories, tags, popularPosts, sortedPosts, authors } from "../lib/blog-data.js"; + +const recent = (await sortedPosts()).slice(0, 4); +const popular = await popularPosts(); +const author = authors[0]; +--- + +<aside class="space-y-10"> + <div> + <div class="flex items-center gap-3"> + <img src={author.avatar} alt="" width="40" height="40" class="h-10 w-10 rounded-full" /> + <div> + <div class="text-sm font-medium">{author.name}</div> + <div class="text-xs text-muted-foreground">Editor</div> + </div> + </div> + <p class="mt-3 text-sm text-muted-foreground">{author.bio}</p> + </div> + + <div> + <div class="text-xs font-medium uppercase tracking-wider text-muted-foreground">Categories</div> + <ul class="mt-3 space-y-2 text-sm"> + { + categories.map((category) => ( + <li> + <a href={`/categories/${category.slug}`} class="text-foreground/80 hover:text-primary"> + {category.name} + </a> + </li> + )) + } + </ul> + </div> + + <div> + <div class="text-xs font-medium uppercase tracking-wider text-muted-foreground">Popular</div> + <div class="mt-3 space-y-4"> + {popular.map((post) => <PostCard post={post} variant="compact" />)} + </div> + </div> + + <div> + <div class="text-xs font-medium uppercase tracking-wider text-muted-foreground">Recent</div> + <div class="mt-3 space-y-4"> + {recent.map((post) => <PostCard post={post} variant="compact" />)} + </div> + </div> + + <div> + <div class="text-xs font-medium uppercase tracking-wider text-muted-foreground">Tags</div> + <div class="mt-3 flex flex-wrap gap-2"> + { + tags.map((tag) => ( + <a + href={`/tags/${tag.slug}`} + class="rounded-full border border-border px-2.5 py-1 text-xs text-muted-foreground hover:border-primary hover:text-primary" + > + {tag.name} + </a> + )) + } + </div> + </div> + + <Newsletter compact /> + + <a href="/rss.xml" class="inline-flex items-center gap-2 text-sm text-muted-foreground hover:text-primary"> + <Icon name="rss" class="h-3.5 w-3.5" /> Subscribe via RSS + </a> +</aside> diff --git a/app/src/components/TableOfContents.astro b/app/src/components/TableOfContents.astro new file mode 100644 index 0000000..5afcd08 --- /dev/null +++ b/app/src/components/TableOfContents.astro @@ -0,0 +1,63 @@ +--- +const { items = [] } = Astro.props; +--- + +{ + items.length > 0 && ( + <div data-toc> + <div class="text-xs font-medium uppercase tracking-wider text-muted-foreground">On this page</div> + <ul class="mt-3 space-y-2 border-l border-border"> + {items.map((item) => ( + <li data-toc-item class="relative" style={`padding-left: ${item.level === 3 ? 24 : 12}px`}> + <a + href={`#${item.id}`} + data-toc-link={item.id} + class="block py-0.5 text-sm text-muted-foreground transition-colors hover:text-foreground" + > + {item.text} + </a> + </li> + ))} + </ul> + </div> + ) +} + +<style> + [data-toc-item].is-active::before { + background: var(--color-primary); + bottom: 0.125rem; + content: ""; + left: -1px; + position: absolute; + top: 0.125rem; + width: 1px; + } +</style> + +<script> + const toc = document.querySelector("[data-toc]"); + if (toc) { + const links = [...toc.querySelectorAll("[data-toc-link]")]; + const setActive = (id) => { + links.forEach((link) => { + const active = link.getAttribute("data-toc-link") === id; + link.classList.toggle("text-primary", active); + link.classList.toggle("text-muted-foreground", !active); + link.closest("[data-toc-item]")?.classList.toggle("is-active", active); + }); + }; + const observer = new IntersectionObserver( + (entries) => { + const visible = entries.filter((entry) => entry.isIntersecting); + if (visible[0]) setActive(visible[0].target.id); + }, + { rootMargin: "-80px 0px -70% 0px" }, + ); + links.forEach((link) => { + const id = link.getAttribute("data-toc-link"); + const heading = id ? document.getElementById(id) : null; + if (heading) observer.observe(heading); + }); + } +</script> diff --git a/app/src/content.config.js b/app/src/content.config.js new file mode 100644 index 0000000..2bf7881 --- /dev/null +++ b/app/src/content.config.js @@ -0,0 +1,35 @@ +import { defineCollection } from "astro:content"; +import { glob } from "astro/loaders"; +import { z } from "astro/zod"; + +const blog = defineCollection({ + loader: glob({ + pattern: "**/index.mdx", + base: "./src/content/blog", + generateId: ({ entry }) => entry.replace(/[\\/]index\.mdx$/, "").replace(/\\/g, "/"), + }), + schema: ({ image }) => + z.object({ + title: z.string(), + excerpt: z.string(), + date: z.coerce.date(), + updated: z.coerce.date().optional(), + readingTime: z.number().int().positive(), + category: z.string(), + tags: z.array(z.string()).default([]), + author: z.string(), + thumbnail: image(), + imageCredit: z + .object({ + caption: z.string().optional(), + author: z.string(), + authorUrl: z.string().url(), + source: z.string().optional(), + sourceUrl: z.string().url().optional(), + }) + .optional(), + featured: z.boolean().default(false), + }), +}); + +export const collections = { blog }; diff --git a/app/src/layouts/BaseLayout.astro b/app/src/layouts/BaseLayout.astro new file mode 100644 index 0000000..89cf184 --- /dev/null +++ b/app/src/layouts/BaseLayout.astro @@ -0,0 +1,95 @@ +--- +import "../styles.css"; +import Header from "../components/Header.astro"; +import Footer from "../components/Footer.astro"; +import DevConsole from "../components/DevConsole.astro"; +import { SITE, imageSrc } from "../lib/blog-data.js"; + +const { + title = `${SITE.name} - Medellín's restaurant scene for expats, nomads & travelers`, + description = SITE.description, + canonical, + ogType = "website", + ogImage, + jsonLd, + flushFooter = false, +} = Astro.props; + +const siteUrl = Astro.site?.toString() || SITE.url; +const absoluteUrl = (value) => { + const src = imageSrc(value) || value; + if (!src) return undefined; + + try { + return new URL(src, siteUrl).toString(); + } catch { + return src; + } +}; +const canonicalUrl = absoluteUrl(canonical || Astro.url.pathname); +const ogImageUrl = absoluteUrl(ogImage); +--- + +<!doctype html> +<html lang="en"> + <head> + <meta charset="utf-8" /> + <meta name="viewport" content="width=device-width, initial-scale=1" /> + <title>{title} + + + + + + + {canonicalUrl && } + {ogImageUrl && } + + + + {ogImageUrl && } + {canonicalUrl && } + + + + { + jsonLd && ( + + + + +
+
+
+ +
+
+
+ + + diff --git a/app/src/lib/blog-data.js b/app/src/lib/blog-data.js new file mode 100644 index 0000000..255fff6 --- /dev/null +++ b/app/src/lib/blog-data.js @@ -0,0 +1,176 @@ +import { getCollection } from "astro:content"; + +const siteUrl = ( + import.meta.env.SITE_URL || + import.meta.env.PUBLIC_SITE_URL || + "https://quietpages-eta.vercel.app" +).replace(/\/$/, ""); + +export const authors = [ + { + slug: "carlos-arias", + name: "Carlos Arias", + bio: "AI engineer and digital strategist with 25+ years building software and AI systems; founder of CarlosArias&Co and engineer behind Medellín.co.", + longBio: + "Carlos Arias is an AI engineer and strategist with over two decades across software engineering, digital marketing, and AI. He is the founder of CarlosArias&Co and Snoopi.io, and an engineer and strategist behind the Medellín.co city guide. Based between South Florida and Latin America, he builds AI-driven systems for local visibility and revenue-tracked growth. Comiida's articles are researched from public sources and AI-assisted under his editorial oversight.", + avatar: "https://carlosarias.com/wp-content/uploads/2023/02/cropped-clos.jpg", + jobTitle: "Founder & Editor, Comiida", + knowsAbout: [ + "Medellín restaurants", + "Colombian cuisine", + "dining guides", + "local SEO", + ], + sameAs: [ + "https://x.com/hicarlosarias", + "https://www.linkedin.com/in/hicarlosarias/", + "https://www.instagram.com/carlosarias.co", + "https://www.facebook.com/hicarlosarias", + "https://carlosarias.com", + ], + }, +]; + +export const categories = [ + { slug: "guides", name: "Guides" }, + { slug: "news", name: "News" }, + { slug: "reviews", name: "Reviews" }, + { slug: "neighborhoods", name: "Neighborhoods" }, +]; + +export const tags = [ + { slug: "food-guide", name: "Food Guide" }, + { slug: "ciudad-del-rio", name: "Ciudad Del Rio" }, + { slug: "things-to-do", name: "Things To Do" }, + { slug: "terminal-del-sur", name: "Terminal Del Sur" }, + { slug: "specialty-coffee", name: "Specialty Coffee" }, + { slug: "craft-beer", name: "Craft Beer" }, + { slug: "rionegro", name: "Rionegro" }, + { slug: "antioquia", name: "Antioquia" }, + { slug: "day-trip", name: "Day Trip" }, + { slug: "oriente-antioqueno", name: "Oriente Antioqueno" }, + { slug: "food-festival", name: "Food Festival" }, + { slug: "marinilla", name: "Marinilla" }, + { slug: "wake-medellin", name: "Wake Medellin" }, + { slug: "peruvian-cuisine", name: "Peruvian Cuisine" }, + { slug: "steakhouse", name: "Steakhouse" }, + { slug: "gastronomy", name: "Gastronomy" }, + { slug: "time-out", name: "Time Out" }, + { slug: "food-ranking", name: "Food Ranking" }, + { slug: "chef", name: "Chef" }, + { slug: "colombian-cuisine", name: "Colombian Cuisine" }, + { slug: "new-restaurant", name: "New Restaurant" }, + { slug: "legal-news", name: "Legal News" }, + { slug: "fine-dining", name: "Fine Dining" }, + { slug: "hidden-kitchen", name: "Hidden Kitchen" }, + { slug: "chefs-table", name: "Chefs Table" }, + { slug: "tasting-menu", name: "Tasting Menu" }, + { slug: "new-openings", name: "New Openings" }, + { slug: "restaurant-news", name: "Restaurant News" }, + { slug: "restaurants", name: "Restaurants" }, + { slug: "coffee", name: "Coffee" }, + { slug: "el-poblado", name: "El Poblado" }, + { slug: "brunch", name: "Brunch" }, + { slug: "expats", name: "Expats" }, + { slug: "food-safety", name: "Food Safety" }, + { slug: "street-food", name: "Street Food" }, + { slug: "medellin", name: "Medellín" }, + { slug: "travel", name: "Travel" }, +]; + +export const imageSrc = (image) => (typeof image === "string" ? image : image?.src); + +// Keep the FULL ISO timestamp (not date-only) so posts published on the same day order by +// their real publish time, and so Article schema / sitemap / RSS get precise datePublished. +export const normalizePost = (entry) => ({ + slug: entry.id, + ...entry.data, + date: entry.data.date?.toISOString(), + updated: entry.data.updated?.toISOString(), +}); + +export const posts = async () => (await getCollection("blog")).map(normalizePost); + +export const getPost = async (slug) => (await posts()).find((post) => post.slug === slug); +export const getAuthor = (slug) => authors.find((author) => author.slug === slug); +export const getCategory = (slug) => categories.find((category) => category.slug === slug); +export const getTag = (slug) => tags.find((tag) => tag.slug === slug); +export const postsByCategory = async (slug) => + (await sortedPosts()).filter((post) => post.category === slug); +export const postsByTag = async (slug) => + (await sortedPosts()).filter((post) => post.tags.includes(slug)); +export const postsByAuthor = async (slug) => + (await sortedPosts()).filter((post) => post.author === slug); +export const sortedPosts = async () => + [...(await posts())].sort((a, b) => { + const diff = new Date(b.date) - new Date(a.date); // newest first + return diff !== 0 ? diff : a.slug.localeCompare(b.slug); // stable tiebreaker + }); +export const featuredPost = async () => { + const sorted = await sortedPosts(); + return sorted.find((post) => post.featured) ?? sorted[0]; +}; +export const popularPosts = async () => (await sortedPosts()).slice(0, 4); +export const relatedPosts = async (post, n = 3) => + (await sortedPosts()) + .filter((candidate) => candidate.slug !== post.slug) + .sort((a, b) => { + const score = (candidate) => + (candidate.category === post.category ? 2 : 0) + + candidate.tags.filter((tag) => post.tags.includes(tag)).length; + return score(b) - score(a); + }) + .slice(0, n); + +export const adjacentPosts = async (post) => { + const sorted = await sortedPosts(); + const index = sorted.findIndex((candidate) => candidate.slug === post.slug); + return { prev: sorted[index + 1], next: sorted[index - 1] }; +}; + +export const formatDate = (iso) => + new Date(iso).toLocaleDateString("en-US", { + year: "numeric", + month: "long", + day: "numeric", + }); + +export const SITE = { + name: "Comiida", + description: + "Medellín's restaurant scene for English-speaking expats, nomads, and travelers — guides, news, and data-driven dining picks.", + url: siteUrl, +}; + +// ---- JSON-LD schema builders (no @context — nest these, or add @context at the top level) ---- +export const organizationSchema = () => ({ + "@type": "Organization", + "@id": `${SITE.url}/#organization`, + name: SITE.name, + url: `${SITE.url}/`, + description: SITE.description, + logo: `${SITE.url}/favicon.svg`, +}); + +export const websiteSchema = () => ({ + "@type": "WebSite", + "@id": `${SITE.url}/#website`, + name: SITE.name, + url: `${SITE.url}/`, + description: SITE.description, + inLanguage: "en", + publisher: { "@id": `${SITE.url}/#organization` }, +}); + +export const personSchema = (author) => + author && { + "@type": "Person", + "@id": `${SITE.url}/authors/${author.slug}#person`, + name: author.name, + url: `${SITE.url}/authors/${author.slug}`, + image: author.avatar, + description: author.longBio || author.bio, + ...(author.jobTitle ? { jobTitle: author.jobTitle } : {}), + ...(author.knowsAbout ? { knowsAbout: author.knowsAbout } : {}), + ...(author.sameAs ? { sameAs: author.sameAs } : {}), + }; diff --git a/app/src/pages/404.astro b/app/src/pages/404.astro new file mode 100644 index 0000000..552741e --- /dev/null +++ b/app/src/pages/404.astro @@ -0,0 +1,21 @@ +--- +import BaseLayout from "../layouts/BaseLayout.astro"; +--- + + +
+
404
+

Lost in the margins

+

+ The page you're looking for has wandered off. It may have been moved, retitled, or never existed at all. +

+ +
+
diff --git a/app/src/pages/about.astro b/app/src/pages/about.astro new file mode 100644 index 0000000..fc9af21 --- /dev/null +++ b/app/src/pages/about.astro @@ -0,0 +1,62 @@ +--- +import BaseLayout from "../layouts/BaseLayout.astro"; +import Breadcrumbs from "../components/Breadcrumbs.astro"; +import { SITE, authors } from "../lib/blog-data.js"; +--- + + +
+ + +
+
About
+

+ A restaurant publication that runs itself. +

+
+ +
+

+ {SITE.name} is an experiment in fully autonomous publishing. Every part of this site — the research, the writing, the cover images, the SEO, even the editorial calendar — is produced and managed by a system of AI agents that run on their own, every single day. No human writes the articles. +

+

How it works

+

Each day, with no one at the keyboard, the agents:

+
    +
  • scan for timely Medellín food news and new openings, and keep an evergreen topic calendar stocked;
  • +
  • write each article to SEO and EEAT standards, with real, linked sources;
  • +
  • generate an original cover illustration;
  • +
  • audit their own work against a quality & SEO rubric — and revise it until it passes;
  • +
  • then publish on a schedule.
  • +
+

Honest by design

+

+ Quality and honesty matter even when no human is in the loop. The agents work from real, citable signals — aggregate ratings, published menus and prices, awards, and local press — never invented first-hand experiences, and every factual claim links to its source. Cover images are AI-generated illustrations, always labeled as such. +

+

Who built it

+

+ {SITE.name} was built by Carlos Arias, an AI engineer. He doesn’t write the articles — he built the agents that do. {SITE.name} is his passion project: an experiment in how far autonomous, agentic publishing can go, pointed at a subject worth covering — Medellín’s restaurant scene, for the English-speaking expats, nomads, and travelers trying to figure out where to eat. +

+
+ +
+

The engineer behind Comiida

+
+ { + authors.map((author) => ( + + +
+
{author.name}
+
{author.bio}
+
+
+ )) + } +
+
+
+
diff --git a/app/src/pages/authors/[slug].astro b/app/src/pages/authors/[slug].astro new file mode 100644 index 0000000..0f83497 --- /dev/null +++ b/app/src/pages/authors/[slug].astro @@ -0,0 +1,46 @@ +--- +import BaseLayout from "../../layouts/BaseLayout.astro"; +import Breadcrumbs from "../../components/Breadcrumbs.astro"; +import PostCard from "../../components/PostCard.astro"; +import { SITE, authors, postsByAuthor, personSchema } from "../../lib/blog-data.js"; + +export function getStaticPaths() { + return authors.map((author) => ({ params: { slug: author.slug }, props: { author } })); +} + +const { author } = Astro.props; +const list = await postsByAuthor(author.slug); +const jsonLd = { + "@context": "https://schema.org", + "@type": "ProfilePage", + mainEntity: personSchema(author), +}; +--- + + +
+ + +
+ +
+
Author
+

{author.name}

+

{author.longBio}

+
+
+ +
+
+ {list.length} {list.length === 1 ? "piece" : "pieces"} by {author.name.split(" ")[0]} +
+ {list.map((post) => )} +
+
+
diff --git a/app/src/pages/blog/[slug].astro b/app/src/pages/blog/[slug].astro new file mode 100644 index 0000000..9d48a3a --- /dev/null +++ b/app/src/pages/blog/[slug].astro @@ -0,0 +1,342 @@ +--- +import { Image } from "astro:assets"; +import { getCollection, render } from "astro:content"; +import BaseLayout from "../../layouts/BaseLayout.astro"; +import Breadcrumbs from "../../components/Breadcrumbs.astro"; +import TableOfContents from "../../components/TableOfContents.astro"; +import PostCard from "../../components/PostCard.astro"; +import Newsletter from "../../components/Newsletter.astro"; +import Icon from "../../components/Icon.astro"; +import { + SITE, + getAuthor, + getCategory, + getTag, + formatDate, + relatedPosts, + adjacentPosts, + normalizePost, + imageSrc, + personSchema, + organizationSchema, +} from "../../lib/blog-data.js"; + +export async function getStaticPaths() { + const entries = await getCollection("blog"); + return entries.map((entry) => ({ params: { slug: entry.id }, props: { entry } })); +} + +const { entry } = Astro.props; +const post = normalizePost(entry); +const author = getAuthor(post.author); +const category = getCategory(post.category); +const { Content, headings } = await render(entry); +const toc = headings + .filter((heading) => heading.depth === 2 || heading.depth === 3) + .map((heading) => ({ + level: heading.depth, + id: heading.slug, + text: heading.text, + })); +const related = await relatedPosts(post); +const { prev, next } = await adjacentPosts(post); +const canonical = `/blog/${post.slug}`; +const canonicalUrl = new URL(canonical, SITE.url).toString(); +const thumbnailUrl = post.thumbnail ? new URL(imageSrc(post.thumbnail), SITE.url).toString() : undefined; +const shareUrl = canonicalUrl; +const jsonLd = { + "@context": "https://schema.org", + "@type": "BlogPosting", + headline: post.title, + description: post.excerpt, + datePublished: post.date, + dateModified: post.updated || post.date, + image: thumbnailUrl ? [thumbnailUrl] : undefined, + author: personSchema(author), + publisher: organizationSchema(), + mainEntityOfPage: { "@type": "WebPage", "@id": canonicalUrl }, + url: canonicalUrl, + inLanguage: "en", + ...(category ? { articleSection: category.name } : {}), + ...(post.tags?.length ? { keywords: post.tags.join(", ") } : {}), +}; +--- + + +
+
+
+ +
+ + +
+ { + category && ( + + {category.name} + + ) + } +

+ {post.title} +

+

{post.excerpt}

+
+ { + author && ( + + + {author.name} + + ) + } + · + + {post.updated && <>·updated {formatDate(post.updated)}} + · + {post.readingTime} min read +
+
+ + { + post.thumbnail && ( +
+
+ {post.imageCredit?.caption +
+ { + post.imageCredit && ( +
+ {post.imageCredit.caption && {post.imageCredit.caption} } + {post.imageCredit.author && ( + + AI-generated illustration by{" "} + + {post.imageCredit.author} + + . + + )} +
+ ) + } +
+ ) + } + +
+
+
+ +
+ +
+ Tags + { + post.tags.map((tag) => { + const item = getTag(tag); + return ( + + #{item?.name ?? tag} + + ); + }) + } +
+ +
+ Share + + + +
+ + { + author && ( +
+ +
+
Written by
+ + {author.name} + +

{author.bio}

+
+
+ ) + } + + + +
+ +
Comments
+

Hook this up to your favourite commenting platform — Giscus, Disqus, or your own.

+
+
+ + +
+ + { + related.length > 0 && ( +
+

Continue reading

+
+ {related.map((relatedPost) => )} +
+
+ ) + } +
+ + +
+ + diff --git a/app/src/pages/blog/index.astro b/app/src/pages/blog/index.astro new file mode 100644 index 0000000..d969d28 --- /dev/null +++ b/app/src/pages/blog/index.astro @@ -0,0 +1,223 @@ +--- +import BaseLayout from "../../layouts/BaseLayout.astro"; +import Breadcrumbs from "../../components/Breadcrumbs.astro"; +import PostCard from "../../components/PostCard.astro"; +import Icon from "../../components/Icon.astro"; +import { SITE, categories, sortedPosts, tags } from "../../lib/blog-data.js"; + +const posts = await sortedPosts(); +--- + + +
+ +

The archive

+

+ Every piece we've published. {posts.length} essays, field notes, and interviews. +

+ +
+ + + + + +
+
+ Categories + All + { + categories.map((category) => ( + + {category.name} + + )) + } +
+
+ Tags + { + tags.map((tag) => ( + + #{tag.name} + + )) + } +
+
+ +
+ + + +
+ {posts.map((post) => )} +
+ + +
+
+ + diff --git a/app/src/pages/categories/[slug].astro b/app/src/pages/categories/[slug].astro new file mode 100644 index 0000000..fa2a450 --- /dev/null +++ b/app/src/pages/categories/[slug].astro @@ -0,0 +1,46 @@ +--- +import BaseLayout from "../../layouts/BaseLayout.astro"; +import Breadcrumbs from "../../components/Breadcrumbs.astro"; +import PostCard from "../../components/PostCard.astro"; +import Sidebar from "../../components/Sidebar.astro"; +import { SITE, categories, postsByCategory } from "../../lib/blog-data.js"; + +export function getStaticPaths() { + return categories.map((category) => ({ params: { slug: category.slug }, props: { category } })); +} + +const { category } = Astro.props; +const list = await postsByCategory(category.slug); +--- + + +
+ +
+
Category
+

{category.name}

+

+ {list.length} {list.length === 1 ? "piece" : "pieces"} in this section. +

+
+ +
+
+ { + list.length === 0 ? ( +
+

No posts yet in this category.

+
+ ) : ( + list.map((post) => ) + ) + } +
+
+
+
+
diff --git a/app/src/pages/contact.astro b/app/src/pages/contact.astro new file mode 100644 index 0000000..797d329 --- /dev/null +++ b/app/src/pages/contact.astro @@ -0,0 +1,98 @@ +--- +import BaseLayout from "../layouts/BaseLayout.astro"; +import Breadcrumbs from "../components/Breadcrumbs.astro"; +import Icon from "../components/Icon.astro"; +import { SITE } from "../lib/blog-data.js"; +--- + + +
+ + +
+
Contact
+

Say hello.

+

+ A restaurant tip, a correction, or a question about where to eat in Medellín. We read everything and reply to most of it. +

+
+ +
+
+
+ + +
+
+ + +
+
+ + +
+ + + +
+ + +
+
+
+ + diff --git a/app/src/pages/faq.astro b/app/src/pages/faq.astro new file mode 100644 index 0000000..a7bfd17 --- /dev/null +++ b/app/src/pages/faq.astro @@ -0,0 +1,88 @@ +--- +import BaseLayout from "../layouts/BaseLayout.astro"; +import Breadcrumbs from "../components/Breadcrumbs.astro"; +import { SITE } from "../lib/blog-data.js"; + +// Plain-text answers so the visible content exactly matches the FAQPage schema. +const faqs = [ + { + q: "Is the tap water safe to drink in Medellín?", + a: "Yes. Medellín's utility, EPM, treats the city's water to a potable standard, and most residents drink it straight from the tap. This is unusual for Latin America and is one reason street food and ice here are lower-risk than in many nearby destinations.", + }, + { + q: "Is street food safe to eat in Medellín?", + a: "Generally yes, with a few habits: follow the queues (high turnover means fresher food), order things cooked to order and served hot, and be a little cautious with raw preparations in your first days while your stomach adjusts.", + }, + { + q: "Do I need to tip at restaurants in Medellín?", + a: "Most sit-down restaurants add a voluntary 10% service charge (propina voluntaria) to the bill. You'll usually be asked if you want to include it; you can accept, decline, or adjust it. Tipping beyond that is appreciated but not expected.", + }, + { + q: "Do restaurants in Medellín have English menus?", + a: "In the tourist- and expat-heavy areas — El Poblado, Provenza, and parts of Laureles — many places have English menus or English-speaking staff. In more local neighborhoods, expect Spanish-only menus, so a translation app helps.", + }, + { + q: "Can I pay by card, or do I need cash?", + a: "Cards are widely accepted at sit-down restaurants and cafés in El Poblado, Provenza, and Laureles. Carry some cash (Colombian pesos) for street food, market stalls, small family-run spots, and tips.", + }, + { + q: "Which neighborhoods are best for eating out?", + a: "El Poblado and Provenza for upscale and international dining, Laureles for a more local, better-value scene, and Envigado and Sabaneta for traditional Paisa food. Each has a distinct character worth exploring.", + }, + { + q: "What are typical restaurant hours?", + a: "Lunch runs roughly 12–3pm, when many places serve an affordable set menu (menú del día). Dinner is usually 7–10pm. Some kitchens close between services, and a number of restaurants close on Sundays or Mondays, so it's worth checking ahead.", + }, + { + q: "Do I need a reservation?", + a: "For the city's top and fine-dining restaurants, yes — especially on weekends. For cafés, casual spots, and most neighborhood restaurants, you can simply walk in.", + }, +]; + +const jsonLd = { + "@context": "https://schema.org", + "@type": "FAQPage", + "@id": `${SITE.url}/faq#faq`, + mainEntity: faqs.map((f) => ({ + "@type": "Question", + name: f.q, + acceptedAnswer: { "@type": "Answer", text: f.a }, + })), +}; +--- + + +
+ + +
+
FAQ
+

+ Dining in Medellín: FAQ +

+

+ Quick, practical answers to the questions English-speaking visitors ask most about eating + out in Medellín. +

+
+ +
+ { + faqs.map((f) => ( +
+ + {f.q} + + + +

{f.a}

+
+ )) + } +
+
+
diff --git a/app/src/pages/index.astro b/app/src/pages/index.astro new file mode 100644 index 0000000..9602e5b --- /dev/null +++ b/app/src/pages/index.astro @@ -0,0 +1,225 @@ +--- +import { Image } from "astro:assets"; +import BaseLayout from "../layouts/BaseLayout.astro"; +import PostCard from "../components/PostCard.astro"; +import Newsletter from "../components/Newsletter.astro"; +import Icon from "../components/Icon.astro"; +import heroImage from "../assets/hero-placeholder.png"; +import { + SITE, + categories, + featuredPost, + sortedPosts, + getAuthor, + getCategory, + formatDate, + organizationSchema, + websiteSchema, +} from "../lib/blog-data.js"; + +const featured = await featuredPost(); +const featuredAuthor = getAuthor(featured.author); +const featuredCategory = getCategory(featured.category); +const latest = (await sortedPosts()).filter((post) => post.slug !== featured.slug); +const jsonLd = { + "@context": "https://schema.org", + "@graph": [organizationSchema(), websiteSchema()], +}; +--- + + +
+ Watercolor painting of Medellín's green Andean mountains and valley +
+ +
+
+
+
Featured
+ All writing → +
+ +
+
+ +
+
+

Latest

+
+ + { + categories.map((category) => ( + + )) + } +
+
+ + + +
+ {latest.map((post, index) =>
+ + +
+ + +
+ + diff --git a/app/src/pages/robots.txt.js b/app/src/pages/robots.txt.js new file mode 100644 index 0000000..cf194ae --- /dev/null +++ b/app/src/pages/robots.txt.js @@ -0,0 +1,13 @@ +import { SITE } from "../lib/blog-data.js"; + +export function GET() { + return new Response( + ["User-agent: *", "Allow: /", `Sitemap: ${SITE.url}/sitemap.xml`, ""].join("\n"), + { + headers: { + "Content-Type": "text/plain; charset=utf-8", + "Cache-Control": "public, max-age=3600", + }, + }, + ); +} diff --git a/app/src/pages/rss.xml.js b/app/src/pages/rss.xml.js new file mode 100644 index 0000000..d20b214 --- /dev/null +++ b/app/src/pages/rss.xml.js @@ -0,0 +1,73 @@ +import { getCollection } from "astro:content"; +import { SITE, getAuthor, sortedPosts } from "../lib/blog-data.js"; + +const BASE_URL = SITE.url || ""; + +const esc = (value) => + value.replace(/&/g, "&").replace(//g, ">").replace(/"/g, """); + +// Wrap arbitrary text safely in a CDATA section. +const cdata = (s) => `/g, "]]]]>")}]]>`; + +// Convert MDX/Markdown body to plain text (text only — strip formatting, keep link text). +function mdToText(md = "") { + return md + .replace(/^---[\s\S]*?---\s*/, "") // strip frontmatter if present + .replace(/^import\s.*$/gm, "") // strip MDX import lines + .replace(/`{1,3}([^`]*)`{1,3}/g, "$1") // inline code + .replace(/!\[[^\]]*\]\([^)]*\)/g, "") // images + .replace(/\[([^\]]+)\]\([^)]*\)/g, "$1") // links → text + .replace(/^\s{0,3}#{1,6}\s+/gm, "") // headings + .replace(/^\s{0,3}>\s?/gm, "") // blockquotes + .replace(/^\s{0,3}([-*+])\s+/gm, "") // unordered list markers + .replace(/^\s{0,3}\d+\.\s+/gm, "") // ordered list markers + .replace(/^\s*[-*_]{3,}\s*$/gm, "") // horizontal rules + .replace(/(\*\*|__)(.*?)\1/g, "$2") // bold + .replace(/(\*|_)(.*?)\1/g, "$2") // italic + .replace(/<[^>]+>/g, "") // any stray HTML/JSX tags + .replace(/\n{3,}/g, "\n\n") // collapse blank lines + .trim(); +} + +export async function GET() { + const bodyBySlug = Object.fromEntries((await getCollection("blog")).map((e) => [e.id, e.body || ""])); + + const items = (await sortedPosts()).map((post) => { + const link = `${BASE_URL}/blog/${post.slug}`; + const author = getAuthor(post.author); + const fullText = mdToText(bodyBySlug[post.slug]); + return [ + " ", + ` ${esc(post.title)}`, + ` ${link}`, + ` ${link}`, + ` ${new Date(post.date).toUTCString()}`, + author ? ` ${esc(author.name)}` : "", + ` ${esc(post.excerpt)}`, + ` ${cdata(fullText)}`, + " ", + ] + .filter(Boolean) + .join("\n"); + }); + + const xml = [ + '', + '', + " ", + ` ${esc(SITE.name)}`, + ` ${BASE_URL}/`, + ` ${esc(SITE.description)}`, + " en-us", + ...items, + " ", + "", + ].join("\n"); + + return new Response(xml, { + headers: { + "Content-Type": "application/rss+xml; charset=utf-8", + "Cache-Control": "public, max-age=3600", + }, + }); +} diff --git a/app/src/pages/services.astro b/app/src/pages/services.astro new file mode 100644 index 0000000..a546e6f --- /dev/null +++ b/app/src/pages/services.astro @@ -0,0 +1,109 @@ +--- +import BaseLayout from "../layouts/BaseLayout.astro"; +import Breadcrumbs from "../components/Breadcrumbs.astro"; +import { SITE } from "../lib/blog-data.js"; +--- + + +
+ + +
+
Work with us
+

+ Reach Medellín's food-curious expats, nomads, and travelers. +

+
+ +
+

+ Comiida is the English-language guide to eating in Medellín. Our readers are expats settling in, + digital nomads on extended stays, and travelers planning their first trip — people actively + deciding where to eat, drink, and spend. If you want to be part of that conversation, here is how + we can work together. +

+
+ +
+
01
+

Restaurant Guides

+

+ We produce deep, honest restaurant guides for Medellín neighborhoods, cuisine types, and dining + occasions — the kind of editorial that ranks in search and earns reader trust over time. + Each guide is researched from public signals (ratings, menus, awards, local press) and written + to the standards of a serious travel publication. +

+
    +
  • Neighborhood round-ups (El Poblado, Laureles, Envigado, Sabaneta, and beyond)
  • +
  • Occasion guides: date night, family brunch, business lunch, late-night bites
  • +
  • Cuisine-specific lists: Colombian, Japanese, Italian, vegetarian & vegan, and more
  • +
  • Evergreen pieces updated as the city’s scene evolves
  • +
+

+ Guides are driven by editorial judgment, not by who pays — but restaurants that want to + ensure we have the most accurate, up-to-date information about their venue can reach out to + submit details for consideration. +

+
+ +
+
02
+

Sponsored Features

+

+ A sponsored feature gives your restaurant, food business, or hospitality brand a dedicated, + long-form placement on Comiida — written to the same editorial standard as the rest of + the site, clearly labeled as sponsored, and built to perform in search over time. +

+
    +
  • A full editorial profile of your venue, concept, or product
  • +
  • Inclusion in relevant round-up guides and category pages
  • +
  • SEO-optimized copy targeting the exact searches your future guests are running
  • +
  • Permanent placement — no monthly subscription, no expiry date
  • +
+

+ Sponsored features are a fit for restaurants looking to build lasting organic visibility, food + importers and producers targeting the expat market, and hospitality brands launching in + Medellín. +

+
+ +
+
03
+

Local Food Consulting

+

+ Comiida is built by an AI engineer who lives in Medellín and has spent years mapping + the city’s food scene. That knowledge is available for hire. +

+
    +
  • Market research: understanding the English-speaking diner in Medellín — what they look for, where they eat, how they discover new places
  • +
  • Location advice: neighborhood-by-neighborhood analysis for restaurants planning a new opening or expansion
  • +
  • English menu & copy review: making sure your translated menu and online presence land correctly with a native-English-speaking audience
  • +
  • Food tours & concierge briefings: curated itineraries for corporate groups, relocation services, and travel operators
  • +
+

+ Engagements range from a single two-hour briefing call to a multi-week research project. + Scope and pricing are agreed before any work begins. +

+
+ +
+

Get in touch

+

+ All three services start with a conversation. Tell us what you’re trying to accomplish + and we’ll let you know quickly whether we’re a good fit. +

+ +
+
+
diff --git a/app/src/pages/sitemap.xml.js b/app/src/pages/sitemap.xml.js new file mode 100644 index 0000000..86e2b9a --- /dev/null +++ b/app/src/pages/sitemap.xml.js @@ -0,0 +1,61 @@ +import { SITE, authors, categories, sortedPosts, tags } from "../lib/blog-data.js"; + +const BASE_URL = SITE.url || ""; + +export async function GET() { + const posts = await sortedPosts(); + const entries = [ + { path: "/", changefreq: "weekly", priority: "1.0" }, + { path: "/blog", changefreq: "daily", priority: "0.9" }, + { path: "/about", changefreq: "monthly", priority: "0.6" }, + { path: "/contact", changefreq: "monthly", priority: "0.5" }, + ...posts.map((post) => ({ + path: `/blog/${post.slug}`, + lastmod: post.updated || post.date, + changefreq: "monthly", + priority: "0.8", + })), + ...categories.map((category) => ({ + path: `/categories/${category.slug}`, + changefreq: "weekly", + priority: "0.6", + })), + ...tags.map((tag) => ({ + path: `/tags/${tag.slug}`, + changefreq: "weekly", + priority: "0.4", + })), + ...authors.map((author) => ({ + path: `/authors/${author.slug}`, + changefreq: "monthly", + priority: "0.5", + })), + ]; + + const urls = entries.map((entry) => + [ + " ", + ` ${BASE_URL}${entry.path}`, + entry.lastmod ? ` ${entry.lastmod}` : null, + entry.changefreq ? ` ${entry.changefreq}` : null, + entry.priority ? ` ${entry.priority}` : null, + " ", + ] + .filter(Boolean) + .join("\n"), + ); + + const xml = [ + '', + '', + ...urls, + "", + ].join("\n"); + + return new Response(xml, { + headers: { + "Content-Type": "application/xml", + "Cache-Control": "public, max-age=3600", + }, + }); +} diff --git a/app/src/pages/tags/[slug].astro b/app/src/pages/tags/[slug].astro new file mode 100644 index 0000000..70f8db0 --- /dev/null +++ b/app/src/pages/tags/[slug].astro @@ -0,0 +1,38 @@ +--- +import BaseLayout from "../../layouts/BaseLayout.astro"; +import Breadcrumbs from "../../components/Breadcrumbs.astro"; +import PostCard from "../../components/PostCard.astro"; +import { SITE, tags, postsByTag } from "../../lib/blog-data.js"; + +export function getStaticPaths() { + return tags.map((tag) => ({ params: { slug: tag.slug }, props: { tag } })); +} + +const { tag } = Astro.props; +const list = await postsByTag(tag.slug); +--- + + +
+ +
+
Tag
+

#{tag.name}

+

{list.length} {list.length === 1 ? "post" : "posts"}.

+
+ + { + list.length === 0 ? ( +
+ Nothing tagged yet. +
+ ) : ( + list.map((post) => ) + ) + } +
+
diff --git a/app/src/styles.css b/app/src/styles.css new file mode 100644 index 0000000..b3a3640 --- /dev/null +++ b/app/src/styles.css @@ -0,0 +1,449 @@ +@import "tailwindcss" source(none); +@source "../src"; +@import "tw-animate-css"; + +@font-face { + font-family: "Inter"; + font-style: normal; + font-weight: 400 700; + font-display: optional; + src: url("/fonts/inter-latin.woff2") format("woff2"); + unicode-range: + U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, + U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} + +@font-face { + font-family: "Fraunces"; + font-style: normal; + font-weight: 400 700; + font-display: optional; + src: url("/fonts/fraunces-latin.woff2") format("woff2"); + unicode-range: + U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, + U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} + +@font-face { + font-family: "JetBrains Mono"; + font-style: normal; + font-weight: 400 500; + font-display: optional; + src: url("/fonts/jetbrains-mono-latin.woff2") format("woff2"); + unicode-range: + U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, + U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} + +@custom-variant dark (&:is(.dark *)); + +@theme inline { + --radius-sm: calc(var(--radius) - 4px); + --radius-md: calc(var(--radius) - 2px); + --radius-lg: var(--radius); + --radius-xl: calc(var(--radius) + 4px); + --color-background: var(--background); + --color-foreground: var(--foreground); + --color-card: var(--card); + --color-card-foreground: var(--card-foreground); + --color-popover: var(--popover); + --color-popover-foreground: var(--popover-foreground); + --color-primary: var(--primary); + --color-primary-foreground: var(--primary-foreground); + --color-secondary: var(--secondary); + --color-secondary-foreground: var(--secondary-foreground); + --color-muted: var(--muted); + --color-muted-foreground: var(--muted-foreground); + --color-accent: var(--accent); + --color-accent-foreground: var(--accent-foreground); + --color-destructive: var(--destructive); + --color-destructive-foreground: var(--destructive-foreground); + --color-border: var(--border); + --color-input: var(--input); + --color-ring: var(--ring); + --font-serif: "Fraunces", ui-serif, Georgia, serif; + --font-sans: "Inter", ui-sans-serif, system-ui, sans-serif; + --font-mono: "JetBrains Mono", ui-monospace, monospace; +} + +:root { + --radius: 0.5rem; + --background: oklch(0.985 0.004 90); + --foreground: oklch(0.18 0.01 60); + --card: oklch(0.985 0.004 90); + --card-foreground: oklch(0.18 0.01 60); + --popover: oklch(1 0 0); + --popover-foreground: oklch(0.18 0.01 60); + --primary: oklch(0.55 0.16 30); + --primary-foreground: oklch(0.99 0 0); + --secondary: oklch(0.95 0.005 90); + --secondary-foreground: oklch(0.2 0.01 60); + --muted: oklch(0.95 0.005 90); + --muted-foreground: oklch(0.5 0.01 60); + --accent: oklch(0.93 0.01 60); + --accent-foreground: oklch(0.2 0.01 60); + --destructive: oklch(0.55 0.22 27); + --destructive-foreground: oklch(0.99 0 0); + --border: oklch(0.9 0.005 80); + --input: oklch(0.9 0.005 80); + --ring: oklch(0.55 0.16 30); +} + +.dark { + --background: oklch(0.16 0.005 80); + --foreground: oklch(0.95 0.005 80); + --card: oklch(0.16 0.005 80); + --card-foreground: oklch(0.95 0.005 80); + --popover: oklch(0.2 0.005 80); + --popover-foreground: oklch(0.95 0.005 80); + --primary: oklch(0.72 0.14 35); + --primary-foreground: oklch(0.16 0.005 80); + --secondary: oklch(0.22 0.005 80); + --secondary-foreground: oklch(0.95 0.005 80); + --muted: oklch(0.22 0.005 80); + --muted-foreground: oklch(0.65 0.01 80); + --accent: oklch(0.24 0.005 80); + --accent-foreground: oklch(0.95 0.005 80); + --destructive: oklch(0.7 0.19 22); + --destructive-foreground: oklch(0.16 0.005 80); + --border: oklch(0.28 0.005 80); + --input: oklch(0.28 0.005 80); + --ring: oklch(0.72 0.14 35); +} + +@layer base { + * { + border-color: var(--color-border); + } + + html { + scroll-behavior: smooth; + } + + body { + margin: 0; + background-color: var(--color-background); + color: var(--color-foreground); + font-family: var(--font-sans); + font-feature-settings: "ss01", "cv11"; + -webkit-font-smoothing: antialiased; + text-rendering: optimizeLegibility; + } + + ::selection { + background-color: color-mix(in oklab, var(--color-primary) 25%, transparent); + } + + :where(a, button, input, textarea, select, summary):focus-visible { + outline: 2px solid var(--color-ring) !important; + outline-offset: 3px; + } + + @media (prefers-reduced-motion: reduce) { + html { + scroll-behavior: auto; + } + + *, + *::before, + *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + scroll-behavior: auto !important; + transition-duration: 0.01ms !important; + } + } +} + +@layer components { + .skip-link { + position: fixed; + left: 1rem; + top: 1rem; + z-index: 100; + border-radius: 6px; + background: var(--color-foreground); + color: var(--color-background); + padding: 0.65rem 0.85rem; + font-size: 0.875rem; + font-weight: 600; + transform: translateY(-160%); + transition: transform 150ms ease; + } + + .skip-link:focus-visible { + transform: translateY(0); + } + + [data-home-header] { + box-shadow: none; + backdrop-filter: none; + } + + [data-home-header][data-scrolled="true"], + [data-home-header][data-panel-open="true"] { + background-color: var(--color-background) !important; + border-color: var(--color-border) !important; + color: var(--color-foreground); + } + + [data-home-header] .home-header-link { + color: rgb(255 255 255 / 0.78); + transition: color 150ms ease; + } + + [data-home-header] .home-header-brand { + color: rgb(255 255 255); + } + + [data-home-header] .home-header-link:hover, + [data-home-header] .home-header-link.is-active { + color: rgb(255 255 255); + } + + [data-home-header][data-scrolled="true"] .home-header-link, + [data-home-header][data-panel-open="true"] .home-header-link { + color: var(--color-muted-foreground); + } + + [data-home-header][data-scrolled="true"] .home-header-brand, + [data-home-header][data-panel-open="true"] .home-header-brand { + color: var(--color-foreground); + } + + [data-home-header][data-scrolled="true"] .home-header-link:hover, + [data-home-header][data-scrolled="true"] .home-header-link.is-active, + [data-home-header][data-panel-open="true"] .home-header-link:hover, + [data-home-header][data-panel-open="true"] .home-header-link.is-active { + color: var(--color-foreground); + } + + [data-home-header] .home-header-action { + color: rgb(255 255 255 / 0.78); + } + + [data-home-header] .home-header-action:hover, + [data-home-header] .home-header-action:focus-visible { + background-color: rgb(255 255 255 / 0.12); + color: rgb(255 255 255); + outline: none; + } + + [data-home-header][data-scrolled="true"] .home-header-action, + [data-home-header][data-panel-open="true"] .home-header-action { + color: var(--color-muted-foreground); + } + + [data-home-header][data-scrolled="true"] .home-header-action:hover, + [data-home-header][data-scrolled="true"] .home-header-action:focus-visible, + [data-home-header][data-panel-open="true"] .home-header-action:hover, + [data-home-header][data-panel-open="true"] .home-header-action:focus-visible { + background-color: var(--color-muted); + color: var(--color-foreground); + } +} + +@utility font-serif { + font-family: var(--font-serif); + font-optical-sizing: auto; +} + +@utility prose-article { + width: 100%; + max-width: 68ch; + min-width: 0; + font-size: 1.0625rem; + line-height: 1.75; + color: var(--color-foreground); + + & h2 { + font-family: var(--font-serif); + font-size: 1.75rem; + font-weight: 600; + margin-top: 2.5rem; + margin-bottom: 1rem; + letter-spacing: -0.01em; + scroll-margin-top: 6rem; + } + & h3 { + font-family: var(--font-serif); + font-size: 1.35rem; + font-weight: 600; + margin-top: 2rem; + margin-bottom: 0.75rem; + scroll-margin-top: 6rem; + } + & p { + margin-bottom: 1.25rem; + } + & a { + color: var(--color-primary); + text-decoration: underline; + text-underline-offset: 3px; + text-decoration-thickness: 1px; + } + & a:hover { + text-decoration-thickness: 2px; + } + & blockquote { + border-left: 2px solid var(--color-primary); + padding-left: 1.25rem; + margin: 1.5rem 0; + font-style: italic; + color: var(--color-muted-foreground); + font-family: var(--font-serif); + font-size: 1.15rem; + } + & ul, + & ol { + margin: 1.25rem 0; + padding-left: 1.5rem; + } + & ul { + list-style: disc; + } + & ol { + list-style: decimal; + } + & li { + margin-bottom: 0.5rem; + } + & code { + font-family: var(--font-mono); + font-size: 0.9em; + color: oklch(0.3 0.08 35); + background: oklch(0.95 0.012 85); + padding: 0.15rem 0.4rem; + border-radius: 4px; + } + .dark & code { + color: oklch(0.88 0.08 55); + background: oklch(0.23 0.006 80); + } + & .code-block { + position: relative; + width: 100%; + max-width: 100%; + min-width: 0; + margin: 1.5rem 0; + } + & pre { + box-sizing: border-box; + width: 100%; + max-width: 100%; + min-width: 0; + color: oklch(0.22 0.025 70) !important; + background: oklch(0.975 0.006 95) !important; + border: 1px solid oklch(0.88 0.008 80); + border-radius: 8px; + padding: 1rem 3.5rem 1rem 1.25rem; + overflow-x: auto !important; + margin: 0; + font-size: 0.9rem; + line-height: 1.6; + box-shadow: inset 0 1px 0 oklch(1 0 0 / 0.75); + } + .dark & pre { + color: oklch(0.91 0.012 85) !important; + background: oklch(0.18 0.006 80) !important; + border-color: oklch(0.33 0.006 80); + box-shadow: inset 0 1px 0 oklch(1 0 0 / 0.06); + } + & pre code { + display: block; + min-width: max-content; + color: inherit; + background: transparent; + padding: 0; + } + .dark & pre code { + color: inherit; + background: transparent; + } + & pre span { + color: oklch(0.28 0.035 70) !important; + } + .dark & pre span { + color: oklch(0.89 0.014 85) !important; + } + & .code-copy-button { + position: absolute; + top: 0.55rem; + right: 0.55rem; + z-index: 1; + display: inline-flex; + width: 2rem; + height: 2rem; + align-items: center; + justify-content: center; + border: 1px solid oklch(0.83 0.008 80); + border-radius: 6px; + color: oklch(0.46 0.015 70); + background: oklch(0.995 0.002 95 / 0.94); + transition: + color 150ms ease, + border-color 150ms ease, + background-color 150ms ease, + opacity 150ms ease; + } + & .code-copy-button:hover, + & .code-copy-button:focus-visible { + color: var(--color-foreground); + border-color: var(--color-primary); + outline: none; + } + & .code-copy-button::after { + position: absolute; + right: 0; + bottom: calc(100% + 0.4rem); + content: attr(data-tooltip); + pointer-events: none; + white-space: nowrap; + border-radius: 5px; + background: oklch(0.2 0.01 70); + color: oklch(0.98 0.002 90); + padding: 0.25rem 0.45rem; + font-family: var(--font-sans); + font-size: 0.72rem; + font-weight: 500; + line-height: 1.2; + opacity: 0; + transform: translateY(3px); + transition: + opacity 150ms ease, + transform 150ms ease; + } + & .code-copy-button:hover::after, + & .code-copy-button:focus-visible::after, + & .code-copy-button.is-copied::after { + opacity: 1; + transform: translateY(0); + } + .dark & .code-copy-button { + color: oklch(0.73 0.012 85); + background: oklch(0.24 0.006 80 / 0.94); + border-color: oklch(0.38 0.006 80); + } + .dark & .code-copy-button:hover, + .dark & .code-copy-button:focus-visible { + color: var(--color-foreground); + border-color: var(--color-primary); + } + & hr { + border: 0; + border-top: 1px solid var(--color-border); + margin: 2.5rem 0; + } + & img { + border-radius: 8px; + margin: 1.5rem 0; + } +} + +@utility callout { + border-left: 3px solid var(--color-primary); + background: color-mix(in oklab, var(--color-primary) 6%, transparent); + padding: 1rem 1.25rem; + border-radius: 0 8px 8px 0; + margin: 1.5rem 0; +} diff --git a/astroagent.config.json b/astroagent.config.json new file mode 100644 index 0000000..abd7098 --- /dev/null +++ b/astroagent.config.json @@ -0,0 +1,29 @@ +{ + "name": "comiida", + "url": "https://comiida.com", + "appDir": "app", + "outDir": "public", + "buildCommand": "npm run build", + "contentDir": "app/src/content/blog", + "deploy": { + "type": "local-apache", + "chown": "www:www" + }, + "ai": { + "model": "claude-sonnet-4-6", + "tools": "Read Write Edit Glob Grep WebSearch", + "confine": true, + "agentUser": "comiida-agent", + "skills": [ + "brand", + "ui-ux", + "seo" + ] + }, + "console": { + "port": 3011, + "route": "/devconsole", + "tokenFile": "content-pipeline/.env", + "tokenKey": "ADMIN_TOKEN" + } +} diff --git a/content-pipeline/README.md b/content-pipeline/README.md new file mode 100644 index 0000000..2d86912 --- /dev/null +++ b/content-pipeline/README.md @@ -0,0 +1,134 @@ +# Comiida Content Pipeline + +An agentic pipeline that plans a 3-month editorial calendar and drafts one SEO/EEAT +restaurant article per day (English, for Medellín expats/tourists) with an AI cover image, +queued for human approval before going live on the Astro site. + +Engine: **headless Claude Code** (`claude -p`) + **Higgsfield** image MCP (authenticated at +the claude.ai level — reachable headless, confirmed). See the design at +`/root/.claude/plans/init-elegant-giraffe.md`. + +## Layout +- `config.json` — site, author, models, image, editorial mix/word-counts. +- `.env` — optional secrets (git-ignored). None required today (Higgsfield auth is global). +- `prompts/` — system prompts: `research.system.md`, `writer.system.md`, `image.md`. +- `scripts/` — `research.mjs`, `write-daily.mjs`, `list-drafts.mjs`, `approve.mjs`, `lib/`. +- `calendar.json` — the generated editorial calendar (created by `research.mjs`). +- `drafts//` — pending drafts (`index.mdx`, `cover.jpg`, `sources.json`). +- `run.sh` — cron-safe wrapper (sets PATH, loads `.env`). + +## Setup (one-time) +1. Add the real author to `app/src/lib/blog-data.js` and set `author.slug`/`name` in + `config.json` to match (EEAT requires a real byline). +2. Image generation needs no key — it uses the Higgsfield claude.ai MCP. Check credits with + the `balance` tool if generations start failing. + +## Usage +```bash +# Plan ~3 months (writes calendar.json). Default days = config.editorial.calendarDays. +./run.sh research.mjs [days] + +# Draft the next due article into drafts// (auto-runs the SEO audit after). +./run.sh write-daily.mjs [slug] + +# SEO Specialist audit (EEAT / spam-policy / on-page / AEO / readability). +# Writes /seo-review.json. Default audits the draft; --published audits the live post. +./run.sh seo-review.mjs [--published] + +# Auto-revise a failing draft until it passes the SEO gate (or maxReviseAttempts). +./run.sh revise.mjs + +# News radar — discover timely Medellín food news/events → news-queue.json. +# (also runs automatically inside write-daily, once per day) +./run.sh news-radar.mjs + +# Research — top up the evergreen backlog (additive merge into calendar.json). +# (also runs automatically inside write-daily when the planned backlog is low) +./run.sh research.mjs [count] + +# Review the queue, then publish one manually (SEO-gated; --force overrides). +node scripts/list-drafts.mjs +./run.sh approve.mjs [--force] # moves into the blog, builds, goes live + +# Randomized auto-publisher (cron). Publishes ONE SEO-passing draft per day at a random time. +./run.sh publish-tick.mjs [--now] # --now ignores the time gate (for testing) +``` + +## Scheduling (managed in the aaPanel Cron UI; server clock is UTC) +Only **two** jobs are scheduled — research is now demand-driven (see below), not a cron. +``` +0 11 * * * run.sh write-daily.mjs # daily 11:00 UTC (06:00 Bogotá) — news scan + refill + draft + SEO audit +*/15 * * * * run.sh publish-tick.mjs # every 15m — publish 1 passing draft in the morning (random timestamp) +``` +(aaPanel runs these in UTC; `publish-tick` computes its Bogotá floor time internally.) + +## Evergreen backlog is demand-driven (no monthly cron) +`research.mjs` is **additive** — it proposes evergreen topics and merges new ones into +`calendar.json` (deduped, dates auto-assigned). `write-daily` **auto-refills** when the +`planned` backlog drops below `editorial.refillThreshold` (guarded once/day), so the calendar +stays stocked without thinking in "months." Run manually any time: `./run.sh research.mjs [count]`. + +## Concurrency safety +All `calendar.json` writes go through `lib/calendar.mjs` — an exclusive **lockfile** plus +atomic read-modify-write (`updateEntry`/`addEntry`/`mergeEntries`). This prevents the +lost-update race where a long `write-daily` run could clobber concurrent `publish-tick` / +research writes. + +## Auto-publish: morning go-live, randomized timestamp (`config.json` → `publish`) +Posts go **live in the morning** (not a random hour all day), but each is stamped with a +**random earlier-today timestamp** so published times aren't a fixed-minute fingerprint: +- **06:00** `write-daily.mjs` drafts the day's topic, runs the SEO audit, and auto-revises. +- **`publish-tick.mjs`** (every 15 min) publishes **one** SEO-passing draft on the first tick + after the **`publish.notBefore`** floor (default `07:00` Bogotá — a buffer so the morning's + fresh/news article is the one that goes live). The post's `date` is set to a random time + between midnight and the publish moment (`randomTimestampTodaySoFar` — varied but never + future). News drafts fast-track ahead of evergreen. One publish/day; idempotent; SEO-failing + drafts wait for manual review. State in `state/publish-YYYY-MM-DD.json`. +- Set `publish.auto=false` to disable auto-publish and use manual `approve.mjs`. + +## Admin dashboard (read-only) +A small Node service (`admin/server.mjs`, no deps) shows the week's scheduled topics, the +draft queue (with cover + preview), and the full calendar. +- Served at **`https://comiida.com/admin?key=`** (Apache proxies `/admin` → + `127.0.0.1:3010`; token is in `.env`). A valid `?key=` sets a 30-day cookie. +- Kept alive by **supervisor**: program `comiidaAdmin` + (`/www/server/panel/plugin/supervisor/profile/comiidaAdmin.ini`). + Manage with: `supervisorctl -c /etc/supervisor/supervisord.conf {status|restart|stop} comiidaAdmin:comiidaAdmin_00`. +- Read-only by design — approving still happens via `./run.sh approve.mjs `. + +## Daily news radar (timely news/events) +`scripts/news-radar.mjs` + `prompts/news-radar.system.md` discover **time-sensitive** Medellín +food items (openings/closings, festivals/events, awards, press) via WebSearch → `news-queue.json` +(deduped vs published + calendar; stale fresh items pruned past `news.recencyDays`). +- Runs **automatically at the start of `write-daily.mjs`**, guarded once/day via + `state/news-YYYY-MM-DD.json` (so the hourly test cron doesn't re-scan). +- **News-first selection:** the writer prefers the freshest queue item (by `freshnessScore`), + injects it into `calendar.json` as a `news:true` entry, and drafts it; on a quiet day it falls + back to the next evergreen calendar topic. +- **Fast-track publishing:** `publish-tick.mjs` releases `news:true` drafts before evergreen + ones (still 1/day, still SEO-gated, still randomized timestamp) — so timely pieces go live the + same day. +- Config: `config.json → news { enabled, recencyDays, maxPerScan }`. + +## Auto-revise loop +When a fresh draft fails the SEO gate, `write-daily` automatically runs `scripts/revise.mjs`: +it feeds the audit's `blocking` + `topFixes` back to the writer (`prompts/reviser.system.md`), +which edits the draft in place (finding real sources via WebSearch for uncited claims — never +fabricating), then **re-audits**. Repeats until `pass` or `seo.maxReviseAttempts` (config). +Config: `seo.autoRevise`, `seo.maxReviseAttempts`. Verified: a draft at fail/82 → pass/90 in one pass. + +## SEO Specialist agent +`scripts/seo-review.mjs` + `prompts/seo-review.system.md` audit an article across five +weighted dimensions — EEAT (30), spam-policy compliance (20), on-page SEO (25), AEO/answer- +engine (15), readability (10) — and write `seo-review.json` with per-dimension scores, a +verdict (`pass`/`revise`/`fail`), blocking issues, and a ranked fix list. +- Runs **automatically** at the end of `write-daily.mjs` (verdict also stored on the calendar + entry). +- **Gates `approve.mjs`**: approval is blocked when `verdict==="fail"` or `overall < + seo.minScore` (config; default 85). Override with `approve.mjs --force`. +- Verdict + scores + fixes show in the `/admin` dashboard (draft cards and the draft preview). +- Reviewer model: `models.reviewer` in `config.json`. + +## EEAT guardrails (enforced in prompts) +Data-driven only; every claim cited; no fabricated first-person dining; real author byline; +honest "AI-generated illustration" image credit. diff --git a/content-pipeline/admin/server.mjs b/content-pipeline/admin/server.mjs new file mode 100644 index 0000000..312372a --- /dev/null +++ b/content-pipeline/admin/server.mjs @@ -0,0 +1,456 @@ +#!/usr/bin/env node +// Read-only admin dashboard for the Comiida content pipeline. +// Serves at /admin (proxied by Apache). Auth via secret token (?key= or cookie). +// Node built-ins only — no external deps. +import { createServer } from "node:http"; +import { randomUUID } from "node:crypto"; +import { readFileSync, writeFileSync, existsSync, readdirSync } from "node:fs"; +import { dirname, resolve, join, extname } from "node:path"; +import { fileURLToPath } from "node:url"; +import { readFrontmatter, fmString, fmArray, loadJson, todayInTz, addDays, slugify } from "../scripts/lib/util.mjs"; +import { addEntry } from "../scripts/lib/calendar.mjs"; + +const HERE = dirname(fileURLToPath(import.meta.url)); +const PIPELINE = resolve(HERE, ".."); + +// --- env (.env) --- +function loadEnv() { + const p = join(PIPELINE, ".env"); + if (!existsSync(p)) return; + for (const line of readFileSync(p, "utf8").split("\n")) { + const m = line.match(/^\s*([A-Z0-9_]+)\s*=\s*(.*)\s*$/); + if (m && !(m[1] in process.env)) process.env[m[1]] = m[2]; + } +} +loadEnv(); + +const cfg = loadJson(join(PIPELINE, "config.json")); +const root = cfg.paths.projectRoot; +const TOKEN = process.env.ADMIN_TOKEN || ""; +const PORT = parseInt(process.env.ADMIN_PORT, 10) || 3010; + +if (!TOKEN) { + console.error("[admin] ADMIN_TOKEN not set in .env — refusing to start."); + process.exit(1); +} + +// --- data gathering --- +const draftsDir = () => join(root, cfg.paths.draftsDir); +const blogDir = () => join(root, cfg.paths.blogContentDir); + +function calendar() { + const p = join(root, cfg.paths.calendar); + return existsSync(p) ? loadJson(p) : []; +} +function isPublished(slug) { + return existsSync(join(blogDir(), slug, "index.mdx")); +} +function draftSlugs() { + const d = draftsDir(); + return existsSync(d) + ? readdirSync(d, { withFileTypes: true }).filter((x) => x.isDirectory()).map((x) => x.name) + : []; +} +function draftMeta(slug) { + const mdx = join(draftsDir(), slug, "index.mdx"); + if (!existsSync(mdx)) return null; + const fm = readFrontmatter(readFileSync(mdx, "utf8")); + let seo = null; + const seoPath = join(draftsDir(), slug, "seo-review.json"); + if (existsSync(seoPath)) { + try { + seo = JSON.parse(readFileSync(seoPath, "utf8")); + } catch { + seo = null; + } + } + return { + slug, + title: fmString(fm, "title") || slug, + category: fmString(fm, "category") || "", + tags: fmArray(fm, "tags"), + date: fmString(fm, "date") || "", + hasCover: existsSync(join(draftsDir(), slug, "cover.jpg")), + seo, + }; +} + +// --- helpers --- +const esc = (s) => + String(s ?? "").replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c])); + +const STATUS_COLORS = { + planned: "#8a8f98", + drafting: "#d98324", + drafted: "#2f6feb", + "drafted-no-image": "#b58900", + "draft-failed": "#cb2431", + published: "#1a7f37", +}; +const badge = (status) => + `${esc(status)}`; + +const newsBadge = `NEWS`; +const suggestedBadge = `SUGGESTED`; +function freshNewsCount() { + const p = join(root, "content-pipeline", "news-queue.json"); + if (!existsSync(p)) return 0; + try { + return JSON.parse(readFileSync(p, "utf8")).filter((i) => i.status === "fresh").length; + } catch { + return 0; + } +} + +const SEO_COLORS = { pass: "#1a7f37", revise: "#d98324", fail: "#cb2431" }; +const seoBadge = (seo) => + seo + ? `SEO ${esc(seo.verdict)} ${esc(seo.overall)}` + : `SEO —`; + +const messagesPath = () => join(root, "content-pipeline", "messages.json"); +function messages() { + const p = messagesPath(); + if (!existsSync(p)) return []; + try { + return JSON.parse(readFileSync(p, "utf8")); + } catch { + return []; + } +} + +function authed(req) { + const url = new URL(req.url, "http://x"); + const qkey = url.searchParams.get("key"); + if (qkey && qkey === TOKEN) return { ok: true, setCookie: true }; + const cookie = (req.headers.cookie || "").match(/(?:^|;\s*)admin_key=([^;]+)/); + if (cookie && decodeURIComponent(cookie[1]) === TOKEN) return { ok: true }; + return { ok: false }; +} + +// --- pages --- +function dashboardHtml() { + const today = todayInTz(cfg.editorial.timezone); + const weekEnd = addDays(today, 6); + const cal = calendar().slice().sort((a, b) => (a.date || "").localeCompare(b.date || "")); + const drafts = draftSlugs().map(draftMeta).filter(Boolean); + + const inWeek = cal.filter((e) => e.date >= today && e.date <= weekEnd); + const counts = cal.reduce((m, e) => ((m[e.status] = (m[e.status] || 0) + 1), m), {}); + const newsSlugs = new Set(cal.filter((e) => e.news).map((e) => e.slug)); + drafts.forEach((d) => (d.news = newsSlugs.has(d.slug))); + + const row = (e) => ` + + ${esc(e.date)} + ${esc(e.workingTitle || e.slug)} ${e.suggested ? suggestedBadge : ""}${e.news ? " " + newsBadge : ""}
${esc(e.primaryKeyword || "")} + ${esc(e.type)} + ${badge(isPublished(e.slug) ? "published" : e.status)} + ${ + existsSync(join(draftsDir(), e.slug, "index.mdx")) + ? `preview` + : isPublished(e.slug) + ? `live ↗` + : "—" + } + `; + + const draftCards = drafts.length + ? drafts + .map( + (d) => ` +
+ ${d.hasCover ? `` : `
`} +
+
${esc(d.title)} ${d.news ? newsBadge : ""} ${seoBadge(d.seo)}
+
${esc(d.date)} · ${esc(d.category)} · ${esc(d.tags.join(", "))}
+ ${d.seo?.blocking?.length ? `
⚠ ${esc(d.seo.blocking.length)} blocking: ${esc(d.seo.blocking[0])}${d.seo.blocking.length > 1 ? " …" : ""}
` : ""} +
preview · approve.mjs ${esc(d.slug)}
+
+
` + ) + .join("") + : `

No drafts awaiting review.

`; + + const countPills = Object.entries(counts) + .map(([k, v]) => `${badge(k)} ${v}`) + .join("  "); + + const msgs = messages().slice().reverse(); + const msgHtml = msgs.length + ? msgs + .map( + (m) => ` +
+ +
${esc(m.name)} <${esc(m.email)}> · ${esc((m.at || "").slice(0, 16).replace("T", " "))}
+
${esc(m.message)}
+
` + ) + .join("") + : `

No messages yet.

`; + + return ` +Comiida — Content Admin +
+
+

Comiida — Content Admin

+ +
+
Today ${esc(today)} (${esc(cfg.editorial.timezone)}) · ${countPills} · 📰 news queue: ${freshNewsCount()} fresh
+ +
+ +
+ + +

📬 Messages (${msgs.length})

+
${msgHtml}
+ + +

This week (${esc(today)} → ${esc(weekEnd)})

+ ${inWeek.length ? `${inWeek.map(row).join("")}
DateTopicTypeStatus
` : `

Nothing scheduled this week. Run research.mjs to plan more.

`} + +

Drafts awaiting review (${drafts.length})

+
${draftCards}
+ +

Full calendar (${cal.length})

+ ${cal.map(row).join("")}
DateTopicTypeStatus
+
`; +} + +function draftDetailHtml(slug) { + const dir = join(draftsDir(), slug); + const mdxPath = join(dir, "index.mdx"); + if (!existsSync(mdxPath)) return null; + const raw = readFileSync(mdxPath, "utf8"); + const fm = readFrontmatter(raw); + const body = raw.replace(/^---\n[\s\S]*?\n---\n?/, ""); + const sources = existsSync(join(dir, "sources.json")) + ? readFileSync(join(dir, "sources.json"), "utf8") + : "(none)"; + let seo = null; + if (existsSync(join(dir, "seo-review.json"))) { + try { + seo = JSON.parse(readFileSync(join(dir, "seo-review.json"), "utf8")); + } catch { + seo = null; + } + } + const seoSection = seo + ? `

SEO audit ${seoBadge(seo)}

+

${esc(seo.summary || "")}

+
    ${Object.entries(seo.dimensions || {}) + .map(([k, v]) => `
  • ${esc(k)}: ${esc(v.score)}${v.issues?.length ? " — " + esc(v.issues.join("; ")) : ""}
  • `) + .join("")}
+ ${seo.blocking?.length ? `

Blocking:

    ${seo.blocking.map((b) => `
  • ${esc(b)}
  • `).join("")}
` : ""} + ${seo.topFixes?.length ? `

Top fixes:

    ${seo.topFixes.map((f) => `
  • [${esc(f.severity)}/${esc(f.area)}] ${esc(f.fix)}
  • `).join("")}
` : ""}` + : `

SEO audit ${seoBadge(null)}

No audit yet. Run ./run.sh write-daily.mjs (auto-audits) or node scripts/seo-review.mjs ${esc(slug)}.

`; + const hasCover = existsSync(join(dir, "cover.jpg")); + return ` +${esc(fmString(fm, "title") || slug)} — draft +
+

← back to dashboard

+ ${hasCover ? `` : ""} + ${seoSection} +

Frontmatter

${esc(fm)}
+

Body (raw MDX)

${esc(body)}
+

sources.json

${esc(sources)}
+

To publish: ./run.sh approve.mjs ${esc(slug)}

+
`; +} + +// --- server --- +const send = (res, code, body, type = "text/html; charset=utf-8", extra = {}) => { + res.writeHead(code, { "content-type": type, "cache-control": "no-store", ...extra }); + res.end(body); +}; + +const server = createServer((req, res) => { + const url = new URL(req.url, "http://x"); + const path = url.pathname.replace(/\/+$/, "") || "/admin"; + + // PUBLIC (no auth): contact form submission → saved to messages.json, viewed in /admin. + if (req.method === "POST" && path === "/contact-submit") { + let body = ""; + req.on("data", (c) => { + body += c; + if (body.length > 20000) req.destroy(); // basic size guard + }); + req.on("end", () => { + try { + const d = JSON.parse(body || "{}"); + if (d.website) return send(res, 200, JSON.stringify({ ok: true }), "application/json"); // honeypot → silently drop + const name = String(d.name || "").trim().slice(0, 120); + const email = String(d.email || "").trim().slice(0, 160); + const message = String(d.message || "").trim().slice(0, 4000); + if (!name || !message || !/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(email)) { + return send(res, 400, JSON.stringify({ error: "Please fill in your name, a valid email, and a message." }), "application/json"); + } + const arr = messages(); + arr.push({ id: randomUUID(), at: new Date().toISOString(), name, email, message }); + writeFileSync(messagesPath(), JSON.stringify(arr, null, 2)); + send(res, 200, JSON.stringify({ ok: true }), "application/json"); + } catch (e) { + send(res, 500, JSON.stringify({ error: e.message }), "application/json"); + } + }); + return; + } + + const auth = authed(req); + if (!auth.ok) { + return send(res, 401, "

401

Add ?key=YOUR_TOKEN to the URL.

"); + } + const cookieHeader = auth.setCookie + ? { "set-cookie": `admin_key=${encodeURIComponent(TOKEN)}; Path=/admin; HttpOnly; SameSite=Lax; Max-Age=2592000` } + : {}; + + // suggest an article (POST) — inject a top-priority calendar entry + if (req.method === "POST" && path === "/admin/suggest") { + let body = ""; + req.on("data", (c) => (body += c)); + req.on("end", async () => { + try { + const data = JSON.parse(body || "{}"); + const title = String(data.title || "").trim(); + if (!title) return send(res, 400, JSON.stringify({ error: "Title is required" }), "application/json"); + const description = String(data.description || "").trim(); + const source = String(data.source || "").trim(); + const instructions = String(data.instructions || "").trim().slice(0, 4000); + const draft = String(data.draft || "").trim().slice(0, 20000); + const today = todayInTz(cfg.editorial.timezone); + const entry = { + date: today, + slug: slugify(title), + workingTitle: title, + type: "guide", + primaryKeyword: title, + secondaryKeywords: [], + searchIntent: "informational", + audienceAngle: description, + sourceHints: source ? [source] : [], + eeatAngle: "User-suggested topic; research thoroughly and cite every claim.", + suggested: true, + status: "planned", + }; + if (instructions) entry.instructions = instructions; + if (draft) entry.draft = draft; + await addEntry(join(root, cfg.paths.calendar), entry); + send(res, 200, JSON.stringify({ ok: true, slug: entry.slug }), "application/json", cookieHeader); + } catch (e) { + send(res, 500, JSON.stringify({ error: e.message }), "application/json"); + } + }); + return; + } + + // delete a message (token-gated) + if (req.method === "POST" && path === "/admin/message-delete") { + let body = ""; + req.on("data", (c) => (body += c)); + req.on("end", () => { + try { + const { id } = JSON.parse(body || "{}"); + const arr = messages().filter((m) => (m.id || m.at) !== id); + writeFileSync(messagesPath(), JSON.stringify(arr, null, 2)); + send(res, 200, JSON.stringify({ ok: true }), "application/json", cookieHeader); + } catch (e) { + send(res, 500, JSON.stringify({ error: e.message }), "application/json"); + } + }); + return; + } + + // cover image + const cover = path.match(/^\/admin\/cover\/([a-z0-9-]+)$/i); + if (cover) { + const f = join(draftsDir(), cover[1], "cover.jpg"); + if (!existsSync(f)) return send(res, 404, "not found", "text/plain"); + return send(res, 200, readFileSync(f), "image/jpeg", cookieHeader); + } + + // draft detail + const draft = path.match(/^\/admin\/draft\/([a-z0-9-]+)$/i); + if (draft) { + const html = draftDetailHtml(draft[1]); + return html ? send(res, 200, html, "text/html; charset=utf-8", cookieHeader) : send(res, 404, "

404

"); + } + + // dashboard + if (path === "/admin" || path === "/admin/") { + return send(res, 200, dashboardHtml(), "text/html; charset=utf-8", cookieHeader); + } + + return send(res, 404, "

404

"); +}); + +server.listen(PORT, "127.0.0.1", () => console.log(`[admin] listening on 127.0.0.1:${PORT}`)); diff --git a/content-pipeline/config.json b/content-pipeline/config.json new file mode 100644 index 0000000..490130f --- /dev/null +++ b/content-pipeline/config.json @@ -0,0 +1,72 @@ +{ + "site": { + "url": "https://comiida.com", + "name": "Comiida", + "topic": "the restaurant industry in Medellin, Colombia", + "audience": "English-speaking expats, digital nomads, and tourists in Medellin", + "language": "en" + }, + "paths": { + "projectRoot": "/www/wwwroot/comiida.com", + "appDir": "app", + "blogContentDir": "app/src/content/blog", + "blogDataFile": "app/src/lib/blog-data.js", + "calendar": "content-pipeline/calendar.json", + "draftsDir": "content-pipeline/drafts", + "logsDir": "content-pipeline/logs" + }, + "models": { + "research": "claude-opus-4-8", + "writer": "claude-sonnet-4-6", + "reviewer": "claude-opus-4-8" + }, + "seo": { + "minScore": 85, + "blockApproveOnFail": true, + "autoRevise": true, + "maxReviseAttempts": 3 + }, + "publish": { + "auto": true, + "notBefore": "07:00" + }, + "news": { + "enabled": true, + "recencyDays": 5, + "maxPerScan": 8 + }, + "author": { + "slug": "carlos-arias", + "name": "Carlos Arias" + }, + "image": { + "provider": "higgsfield", + "preferredModel": "soul_2", + "aspectRatio": "3:2", + "outputFormat": "jpg", + "credit": { + "caption": "Illustrative cover image. Not a photograph of any specific establishment.", + "author": "Comiida" + } + }, + "editorial": { + "calendarDays": 90, + "refillThreshold": 7, + "refillCount": 14, + "timezone": "America/Bogota", + "contentMix": { + "guide": 0.30, + "list": 0.25, + "news-roundup": 0.20, + "trend": 0.15, + "review-summary": 0.10 + }, + "wordCounts": { + "news-roundup": [600, 900], + "trend": [800, 1200], + "review-summary": [900, 1300], + "list": [1000, 1500], + "guide": [1200, 1800] + } + } +} diff --git a/content-pipeline/drafts/.gitkeep b/content-pipeline/drafts/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/content-pipeline/logs/.gitkeep b/content-pipeline/logs/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/content-pipeline/prompts/image.md b/content-pipeline/prompts/image.md new file mode 100644 index 0000000..c2b8bde --- /dev/null +++ b/content-pipeline/prompts/image.md @@ -0,0 +1,37 @@ +# Cover image generation (Higgsfield MCP) + +The cover is an **illustrative editorial image** — appetizing and on-topic, but never +implying a real photograph of a specific named restaurant, dish-as-served, or identifiable +person. + +## Flow (use the Higgsfield claude.ai MCP tools) +1. **Pick a model:** call `mcp__claude_ai_Higgsfield__models_explore` with + `action:"recommend"` and a goal like "editorial food/lifestyle photography cover image", + to get a suitable `model` id and its valid `aspect_ratios`. If that fails, default to the + `preferredModel` from config (`soul_2`). +2. **Generate:** call `mcp__claude_ai_Higgsfield__generate_image` with + `params: { model, prompt, aspect_ratio, count: 1 }`. Use a valid aspect ratio close to + 3:2 landscape. This returns a job id. +3. **Wait for the result:** call `mcp__claude_ai_Higgsfield__job_status` with + `{ jobId, sync: true }`; repeat (respecting `poll_after_seconds`) until the job is + terminal. Read the resulting image URL from `results`. +4. **Download:** use Bash `curl -fsSL "" -o "/cover.jpg"` to save the + image as `cover.jpg` in the draft directory. Verify the file exists and is non-empty. + +If image generation fails after a reasonable retry, continue without it and record +`imageGenerated: false` in `sources.json`. + +## Style guardrails (put these in the prompt) +- Editorial food/lifestyle photography aesthetic, natural light, shallow depth of field. +- Medellín / Colombian context where relevant (tropical, warm, Paisa setting) but generic. +- No real logos, no readable signage, no recognizable real people, no text overlays. +- High detail, web-quality, landscape orientation. + +## Prompt skeleton +``` +Editorial food photography, {subject relevant to the article topic}, {Medellin/Colombian +ambience if relevant}, natural window light, shallow depth of field, warm tones, appetizing, +clean composition, no text, no logos, no people's faces. Photorealistic, high detail. +``` +Fill `{subject}` from the article topic (e.g. "a vibrant brunch spread on a cafe table", +"specialty coffee being poured", "a colorful arepa plate"). Keep it generic and illustrative. diff --git a/content-pipeline/prompts/news-radar.system.md b/content-pipeline/prompts/news-radar.system.md new file mode 100644 index 0000000..4157745 --- /dev/null +++ b/content-pipeline/prompts/news-radar.system.md @@ -0,0 +1,53 @@ +# Role: News radar for Comiida + +You scan for **timely, time-sensitive** Medellín restaurant/food items worth publishing about +**right now**, for an English-speaking expat/tourist audience. You do NOT write articles — you +surface fresh story candidates the daily writer can turn into a post the same day. + +## Inputs (from the user prompt) +- Today's date, the path to write the news queue, paths to existing published posts and the + current `calendar.json` (for dedupe), how many items to return, and the recency window. +- **WebSearch** — your primary tool. Search aggressively for *recent* items. + +## What counts as timely (look for these) +- New restaurant / café / bar **openings** (and notable closings) in Medellín. +- Food **events and festivals** with upcoming dates (e.g. gastronomy festivals, pop-ups, + tasting events, market days). +- **Awards / rankings / press** just published (e.g. a Medellín spot named in a list). +- Seasonal or this-week happenings relevant to dining. +- A genuinely current angle on a trend (something that changed recently). + +Favor items that are **recent** (within the last ~2–3 weeks) and **specific** (named place, +date, source). Skip evergreen "best of" ideas — those are handled by the editorial calendar. + +## Hard rules +- **Only real, verifiable items with sources.** Every candidate must have ≥1 working source + URL from your search. If you can't verify it, don't include it. +- **EEAT-safe**: news roundups, opening announcements, event previews, data-driven angles. + No fabricated first-person experience. +- **No duplicates**: exclude anything matching an existing published post slug or a slug + already in `calendar.json`. Make slugs unique and descriptive. +- Return at most the requested number of items; fewer is fine (even zero on a quiet day — + return an empty array rather than padding with weak/evergreen ideas). + +## Output contract +Write a JSON array to the news-queue path. Each item: +```json +{ + "discovered": "", + "slug": "kebab-case", + "workingTitle": "specific, compelling, includes the keyword", + "type": "news-roundup | trend | review-summary", + "primaryKeyword": "the search phrase", + "secondaryKeywords": ["..."], + "searchIntent": "informational", + "newsHook": "1-2 sentences: what happened and why it's timely now", + "eventDate": "YYYY-MM-DD or null", + "sourceLinks": ["real url", "real url"], + "freshnessScore": 5, + "status": "fresh" +} +``` +`freshnessScore` 1–5 = how time-sensitive/hot (5 = publish today, 1 = mildly timely). +Valid JSON only, UTF-8, no comments, no trailing commas. Write ONLY the file, then reply with a +one-line summary: how many items and their titles. If nothing fresh, write `[]` and say so. diff --git a/content-pipeline/prompts/research.system.md b/content-pipeline/prompts/research.system.md new file mode 100644 index 0000000..b3bc341 --- /dev/null +++ b/content-pipeline/prompts/research.system.md @@ -0,0 +1,56 @@ +# Role: Editorial strategist for Comiida + +You plan a 3-month English-language content calendar for **Comiida**, a publication about +**the restaurant industry in Medellín, Colombia**, written for **English-speaking expats, +digital nomads, and tourists**. + +Your job in this run: produce a `calendar.json` editorial calendar. You do NOT write +articles here — only plan them. + +## Inputs available to you +- The user prompt will give you: today's date, the number of days to plan, the path to + write `calendar.json`, the path to existing published posts, and the desired content-type + mix and allowed types. +- **WebSearch** — use it to ground topics in reality: what people actually search for, + what is newsworthy in Medellín dining (neighborhoods like El Poblado, Laureles, Envigado, + Provenza), seasonal events, food trends, and recurring evergreen questions. + +## Hard rules +- **EEAT-safe topics only.** Plan news roundups, guides, lists, trends, and *data-driven + review summaries*. Never plan a topic that requires inventing a first-person dining + experience. Review-type topics must be framed as data-driven summaries of real, citable + signals (aggregate ratings, menus, prices, awards, press). +- **English, for expats/tourists.** Topics and keywords must match how this audience + searches (e.g. "best brunch in El Poblado", "vegan restaurants Medellín", "is it safe to + eat street food in Medellín"). +- **No duplicates.** Read the slugs/titles of existing posts in the blog content dir and do + not repeat them. Vary neighborhoods, cuisines, price points, and angles. +- **Respect the content-type mix and per-type counts** given in the user prompt. +- These are **evergreen** topics (the daily news radar handles timely news separately) — favor + durable guides, lists, neighborhood deep-dives, and data-driven summaries. + +## Output contract +Write a JSON **array of topic proposals** to the scan path given in the user prompt. Do NOT +assign dates — the pipeline schedules them. Each item: + +```json +{ + "slug": "kebab-case-from-keyword", + "workingTitle": "Specific, compelling, includes the primary keyword", + "type": "guide | list | news-roundup | trend | review-summary", + "primaryKeyword": "the exact search phrase to target", + "secondaryKeywords": ["2-5 related phrases"], + "searchIntent": "informational | commercial | transactional", + "audienceAngle": "why an expat/tourist cares, in one sentence", + "sourceHints": ["concrete places to find real data: outlets, directories, datasets"], + "eeatAngle": "how this piece demonstrates real expertise/data without faking experience" +} +``` + +Rules for the file: +- The number of items requested (roughly matching the per-type counts). +- Valid JSON, UTF-8, no comments, no trailing commas. Write ONLY the file — no prose. +- Slugs unique within the file and not colliding with any slug in the provided exclude list. + +When finished, write the file with the Write tool, then reply with a one-line summary: +how many items and the type breakdown. diff --git a/content-pipeline/prompts/reviser.system.md b/content-pipeline/prompts/reviser.system.md new file mode 100644 index 0000000..bea3a08 --- /dev/null +++ b/content-pipeline/prompts/reviser.system.md @@ -0,0 +1,40 @@ +# Role: Reviser for Comiida (close the SEO/EEAT gaps) + +An SEO/EEAT audit found issues with a draft article. Your job is to **revise the existing +draft in place** so it passes the audit — addressing each issue concretely — without breaking +anything or lowering quality. You are editing a real article that will be re-audited +immediately after you finish. + +## Inputs (from the user prompt) +- Paths to the draft `index.mdx`, its `sources.json`, and the audit `seo-review.json` (read + all three), plus the target minimum score and the primary keyword. +- Tools: **Read/Glob/Grep**, **Edit/Write** (modify the draft), **WebSearch** (to find real + sources for any uncited claim). + +## How to revise +1. Read `seo-review.json`. Work through **every** item in `blocking` and `topFixes`, plus weak + dimensions. Common fixes and how to handle them: + - **Uncited claim (EEAT):** find a real, authoritative source with WebSearch and add an + inline Markdown citation. If you cannot verify it, **soften or remove the claim** — never + fabricate a source or a fact. + - **Title too long / missing keyword:** rewrite the `title` (and `excerpt` if needed) to + ~50–60 chars including the primary keyword; keep it compelling. + - **Keyword not in an H2 / first 100 words:** weave it in naturally (no stuffing). + - **Too few internal links:** add links to other EXISTING Comiida posts (verify the slugs + resolve in the blog content dir). + - **Image credit / metadata mismatch:** correct it to match reality. + - **Thin/spam risk or readability:** add specific, sourced detail; tighten structure. +2. Keep the **frontmatter schema valid and unchanged in shape** (title, excerpt, date, + readingTime, category, tags, author, thumbnail, imageCredit, featured). Update + `readingTime` if the word count changed materially. Do NOT change `author` or `date`. +3. Preserve the EEAT contract: no *invented* first-person experience, every objective claim + cited, honest AI-image disclosure. **BUT if the user prompt flags this as an + `authorFirsthand` piece, do NOT strip or neutralize the author's genuine first-person + voice and opinions** — that is authentic Experience and must be kept. Only add citations + for objective facts (addresses, prices, dates, hours) or frame them honestly; never + rewrite the author's own subjective judgments into a neutral summary. +4. Update `sources.json` to reflect any new citations and bump `wordCount` if it changed. + +## Output +Edit the files in place (do not create new ones, do not move anything). When done, reply with a +one-line summary of what you changed. Do not output the article text in chat. diff --git a/content-pipeline/prompts/seo-review.system.md b/content-pipeline/prompts/seo-review.system.md new file mode 100644 index 0000000..f6c57ea --- /dev/null +++ b/content-pipeline/prompts/seo-review.system.md @@ -0,0 +1,103 @@ +# Role: SEO Specialist & EEAT auditor for Comiida + +You are a rigorous, skeptical SEO/EEAT reviewer. You audit ONE Comiida article (a restaurant +publication for English-speaking expats/tourists in Medellín) and produce a structured audit. +You do not rewrite the article — you score it and list concrete fixes. Be exacting: it is +better to flag a real problem than to wave a weak article through. + +## Inputs (from the user prompt) +- The path to the article `index.mdx`, its `sources.json` (citations), the post directory to + write your audit into, the target primary keyword, and the site's allowed category slugs. +- Tools: **Read/Glob/Grep** (read the article, sources, and sibling posts for internal-link + checks), **WebSearch** (confirm search intent, check the claim/keyword landscape, sanity- + check facts and competitiveness), and **Write** (write the audit JSON). + +## What to evaluate — five dimensions + +### 1. EEAT (Experience, Expertise, Authoritativeness, Trust) — weight 30 +- Real, named author byline present (not a generic/placeholder editorial name). +- **No fabricated first-hand experience.** By default, review-like content must read as a + *data-driven summary* of real signals, never an invented "I ate here" account. +- **Author first-hand EXCEPTION:** if the user prompt tells you this is an `authorFirsthand` + piece (a real, named author's own draft, written personally), first-person experience and + subjective opinions are LEGITIMATE — that is authentic *Experience*, a POSITIVE E-E-A-T + signal, NOT fabrication. Do not flag the personal voice as a violation and do not demand it + be neutralized. The author's own opinions ("the food is mediocre") need no citation; only + *objective, verifiable* facts (addresses, prices, dates, hours) still require a citation or + honest "as of " framing. Reserve fabrication findings for experience with no author + basis or invented objective facts. +- Every non-obvious factual claim (names, prices, dates, ratings, openings) carries an inline + citation to a real, authoritative source. Cross-check against `sources.json`. +- Sources are credible (official, press, primary data) — not circular or low-quality. +- Trust signals: transparency about method, accurate "as of " framing for volatile data. + +### 2. Google spam-policy compliance — weight 20 +Judge against Google's spam policies and the helpful-content guidance: +- **Scaled content abuse:** does the piece deliver genuine, specific value, or is it thin + filler that exists only to rank? AI assistance is fine; low-value mass production is not. +- No keyword stuffing, no hidden text, no doorway/cloaking patterns. +- No fabricated reviews or fake experience (overlaps EEAT but score the policy risk here). + NOTE: a real, named author's genuine first-person account in an `authorFirsthand` piece is + NOT a fabricated review — do not penalize it here. +- People-first: written to help a reader decide where/what to eat, not to game a query. +This dimension is **gating**: a clear violation caps the verdict at "fail" regardless of score. + +### 3. On-page SEO — weight 25 +- Title: contains the primary keyword, compelling, ~50–60 chars ideal. +- Meta description (`excerpt`): ≤155 chars, contains the keyword, earns the click. +- Slug: short, keyword-bearing. +- Headings: exactly one implied H1 (the title — no H1 in body); logical H2/H3 hierarchy. +- Primary keyword present in title, first 100 words, ≥1 H2 — natural, not stuffed. +- Internal links: ≥2 links to other EXISTING Comiida posts (verify the slugs resolve under the + blog content dir — flag any that 404). +- Outbound citations to authoritative sources where claims are made. +- Word count appropriate to search intent and content type. +- Image has honest credit; alt/caption present. + +### 4. AEO — AI-engine / answer-engine optimization — weight 15 +- Answers the core query directly and early (a clear, extractable answer near the top). +- Structured for extraction: descriptive headings, lists, definitions, Q&A where natural. +- Self-contained, factual, citable statements (good for LLM answer engines and featured + snippets). +- Schema readiness: is the content shaped so Article/FAQ/HowTo structured data would apply? +- Entity clarity: places, neighborhoods, dishes named clearly and consistently. + +### 5. Readability & UX — weight 10 +- Scannable: short paragraphs, useful subheads, lists where helpful. +- Clear, concrete language; minimal fluff; logical flow; correct, consistent style. + +## Scoring & verdict +- Score each dimension 0–100. Compute `overall` as the weighted average (weights above). +- `verdict`: + - **fail** if any `blocking` issue exists OR overall < 70 OR a spam-policy violation. + - **revise** if 70 ≤ overall < 85. + - **pass** if overall ≥ 85 and no blocking issues. +- `blocking` = must-fix-before-publish problems: fabricated experience, uncited factual + claims, keyword stuffing, broken/missing internal links, missing real author, spam-policy + violation, or missing/incorrect title/meta. + +## Output — write EXACTLY this JSON to `/seo-review.json` +```json +{ + "slug": "...", + "title": "...", + "primaryKeyword": "...", + "overall": 0, + "verdict": "pass | revise | fail", + "dimensions": { + "eeat": { "score": 0, "issues": ["..."], "notes": "" }, + "spam": { "score": 0, "issues": ["..."], "notes": "" }, + "onPageSeo": { "score": 0, "issues": ["..."], "notes": "" }, + "aeo": { "score": 0, "issues": ["..."], "notes": "" }, + "readability": { "score": 0, "issues": ["..."], "notes": "" } + }, + "blocking": ["... must-fix items, empty array if none ..."], + "topFixes": [ + { "severity": "high|medium|low", "area": "eeat|spam|onPageSeo|aeo|readability", "fix": "specific, actionable" } + ], + "summary": "2-3 sentence verdict in plain English" +} +``` +Rules: valid JSON only, UTF-8, no comments, no trailing commas. Write ONLY the file with the +Write tool, then reply with a one-line summary: `verdict · overall · N blocking · M fixes`. +Do not output the JSON in chat. diff --git a/content-pipeline/prompts/writer.system.md b/content-pipeline/prompts/writer.system.md new file mode 100644 index 0000000..1c60320 --- /dev/null +++ b/content-pipeline/prompts/writer.system.md @@ -0,0 +1,104 @@ +# Role: Senior SEO/EEAT writer for Comiida + +You write ONE article for **Comiida**, a publication about **the restaurant industry in +Medellín, Colombia**, for **English-speaking expats, digital nomads, and tourists**. The +output is a reviewable draft — it does not go live until a human approves it. + +## Inputs (from the user prompt) +- The chosen calendar entry (title, type, primary/secondary keywords, intent, angle). +- Today's date, the author slug to byline, the draft output directory, the path to existing + published posts (for internal links), and the image config (model, aspect ratio). +- Tools: **WebSearch** (live research), **Read/Glob/Grep** (read existing posts), **Write** + (create files), **Bash** (download the generated image), and the **Higgsfield MCP** + (`mcp__claude_ai_Higgsfield__*`) for the cover image. + +## Process +1. **Research the topic live with WebSearch.** Collect current, specific, citable facts: + names, neighborhoods, prices, dates, ratings, awards, sources. Capture the exact URLs. +2. **Read 2+ existing published posts** in the blog content dir to link to internally. +3. **Write the article body** as MDX following the EEAT/SEO contract below. +4. **Generate the cover image** via the Higgsfield MCP, following `prompts/image.md` + (recommend a model → generate_image → poll job_status(sync:true) → download the result + URL to `cover.jpg` with Bash curl). The image is an *illustrative* food/scene image — + never a depiction implying a real photo of a specific named restaurant. If image + generation fails, continue and note it in `sources.json`. +5. **Write the three output files** (see Output) into the draft directory. + +## EEAT / SEO contract (non-negotiable) +- **Honesty / EEAT:** Never *invent* a first-person dining experience. By default, anything + review-like is a *data-driven summary* of real, cited signals and must say so. Every + non-obvious factual claim has an inline citation as a Markdown link to its real source. +- **Author first-hand pieces (EXCEPTION):** When the calendar entry is flagged + `authorFirsthand: true` (or the editor instructions ask you to write "personally" / "as + " and provide the author's own draft), that draft IS the real, named author's + genuine first-hand experience. Preserve it in the **first person** — their visits, + preferences, and honest opinions are authentic *Experience* (the first "E" in E-E-A-T) and + a POSITIVE signal, not fabrication. Do NOT flatten the personal voice into a neutral + summary. In this mode: + - Keep the author's subjective judgments as their own opinion ("I think the best burger + here is…", "the food is mediocre") — these are the author's genuine assessment and need + **no external citation**. + - Still fact-check and cite *objective, verifiable* specifics a reader will act on — + addresses, opening dates, prices, hours — or frame them honestly ("as of ", + "opened around May 2026") when you can't confirm them. Never fabricate an address or fact. + - Clean up spelling/grammar and tighten structure, but keep it sounding like the author. +- **No hallucinated specifics.** If you cannot verify a name/price/fact via WebSearch, do + not state it. Prefer ranges and "as of " framing for volatile data (prices, hours). +- **Keyword placement:** primary keyword in the title, the slug, the excerpt, the first 100 + words, and at least one H2 — naturally, never stuffed. +- **Structure:** one H1 is implied by the title (do not add an H1 in the body); use H2/H3, + short paragraphs, and lists. Scannable. Hit the word-count range for the content type. +- **Internal links:** at least 2 links to other Comiida posts (use real slugs you read). +- **Outbound links:** cite primary sources; open authority links where natural. +- **Meta:** the `excerpt` is the meta description — compelling, ≤155 characters, contains + the primary keyword. + +## Output — write exactly these files into the draft directory +The user prompt gives you the draft dir as `content-pipeline/drafts//`. + +**1. `index.mdx`** — frontmatter must match the Astro schema EXACTLY, then the body: +```mdx +--- +title: "..." # includes primary keyword +excerpt: "..." # meta description, <=155 chars, includes primary keyword +date: YYYY-MM-DD # today +readingTime: # ~ words / 220, rounded, min 1 +category: "..." # one of the site categories (see blog-data.js); lowercase slug +tags: ["...", "..."] # 2-5 kebab-case tags +author: "" # exactly the author slug given to you +thumbnail: ./cover.jpg +imageCredit: + caption: "Illustrative cover image. Not a photograph of any specific establishment." + author: "Comiida" + authorUrl: "https://comiida.com/about" +featured: false +--- + +
+``` +- `category` must be one of the existing category slugs in `blog-data.js` (currently + `guides`, `news`, `reviews`, `neighborhoods`). Map by content type: guide/list → `guides` + (use `neighborhoods` if the piece is anchored to one barrio), news-roundup/trend → `news`, + review-summary → `reviews`. If none fits, pick the closest and note a suggested new + category in `sources.json` (the approver decides — do not invent silently). + +**2. `cover.jpg`** — the generated image saved into the draft dir. + +**3. `sources.json`** — the reviewer's fact-check sheet: +```json +{ + "slug": "...", + "title": "...", + "primaryKeyword": "...", + "wordCount": 0, + "citations": [{ "claim": "...", "url": "..." }], + "internalLinks": ["/blog/other-post/"], + "imageGenerated": true, + "imagePrompt": "...", + "notes": "anything the approver should know (unverified items, suggested new category, etc.)" +} +``` + +## Finish +After writing all files, reply with a one-line summary: slug, type, word count, number of +citations, and whether the image was generated. Do not output the article text in chat. diff --git a/content-pipeline/run.sh b/content-pipeline/run.sh new file mode 100755 index 0000000..87ad9f9 --- /dev/null +++ b/content-pipeline/run.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash +# Wrapper so cron has a sane PATH and the secrets from .env. +# Usage: run.sh [args...] +# run.sh research.mjs +# run.sh write-daily.mjs +# run.sh approve.mjs +set -euo pipefail +DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +# Tools (claude is under /root/.local/bin; node/npx under /usr/bin). +export PATH="/root/.local/bin:/usr/bin:/usr/local/bin:$PATH" + +# Secrets (REPLICATE_API_TOKEN, etc.) +if [ -f "$DIR/.env" ]; then + set -a + . "$DIR/.env" + set +a +fi + +exec node "$DIR/scripts/$1" "${@:2}" diff --git a/content-pipeline/scripts/approve.mjs b/content-pipeline/scripts/approve.mjs new file mode 100644 index 0000000..26942eb --- /dev/null +++ b/content-pipeline/scripts/approve.mjs @@ -0,0 +1,62 @@ +#!/usr/bin/env node +// Manually approve a draft: SEO gate, then promote it into the blog, build, go live. +// Usage: node content-pipeline/scripts/approve.mjs [--force] +import { existsSync } from "node:fs"; +import { dirname, resolve, join } from "node:path"; +import { fileURLToPath } from "node:url"; +import { loadJson } from "./lib/util.mjs"; +import { promoteDraft } from "./lib/publish.mjs"; +import { runSeoReview } from "./seo-review.mjs"; + +const HERE = dirname(fileURLToPath(import.meta.url)); +const PIPELINE = resolve(HERE, ".."); +const cfg = loadJson(join(PIPELINE, "config.json")); +const root = cfg.paths.projectRoot; + +const args = process.argv.slice(2); +const force = args.includes("--force"); +const slug = args.find((a) => !a.startsWith("--")); +if (!slug) { + console.error("Usage: approve.mjs [--force]"); + process.exit(1); +} + +const draftDir = join(root, cfg.paths.draftsDir, slug); +if (!existsSync(join(draftDir, "index.mdx"))) { + console.error(`[approve] no draft at ${join(draftDir, "index.mdx")}`); + process.exit(1); +} + +// SEO Specialist gate. +if (cfg.seo?.blockApproveOnFail && !force) { + const auditPath = join(draftDir, "seo-review.json"); + let audit = existsSync(auditPath) ? loadJson(auditPath) : null; + if (!audit) { + console.log("[approve] no SEO audit found — running the SEO Specialist now…"); + audit = await runSeoReview(slug, { published: false }); + } + if (!audit) { + console.error("[approve] could not obtain an SEO audit. Re-run, or use --force to override."); + process.exit(1); + } + const minScore = cfg.seo.minScore ?? 85; + if (audit.verdict === "fail" || (audit.overall ?? 0) < minScore) { + console.error( + `[approve] BLOCKED by SEO gate: ${audit.verdict} · ${audit.overall}/${minScore} min.\n` + + (audit.blocking?.length ? " blocking:\n - " + audit.blocking.join("\n - ") + "\n" : "") + + ` Review ${auditPath}, revise the draft, then re-approve (or use --force to override).` + ); + process.exit(1); + } + console.log(`[approve] SEO gate passed: ${audit.verdict} · ${audit.overall}`); +} + +try { + console.log("[approve] publishing…"); + const { url, taxonomyAdded } = await promoteDraft(cfg, slug); + if (taxonomyAdded.length) console.log(`[approve] registered in blog-data.js: ${taxonomyAdded.join(", ")}`); + console.log(`[approve] LIVE → ${url}`); +} catch (e) { + console.error(`[approve] ${e.message}`); + process.exit(1); +} diff --git a/content-pipeline/scripts/lib/blogData.mjs b/content-pipeline/scripts/lib/blogData.mjs new file mode 100644 index 0000000..50530e4 --- /dev/null +++ b/content-pipeline/scripts/lib/blogData.mjs @@ -0,0 +1,68 @@ +import { readFileSync, writeFileSync } from "node:fs"; + +/** Find the [ ... ] range of `export const = [ ... ]`. */ +function blockRange(src, arrayName) { + const marker = `export const ${arrayName} = [`; + const start = src.indexOf(marker); + if (start === -1) return null; + const open = src.indexOf("[", start); + let depth = 0; + let i = open; + for (; i < src.length; i++) { + if (src[i] === "[") depth++; + else if (src[i] === "]") { + depth--; + if (depth === 0) break; + } + } + return { open, close: i }; +} + +function slugsIn(src, arrayName) { + const r = blockRange(src, arrayName); + if (!r) return new Set(); + const block = src.slice(r.open, r.close); + const out = new Set(); + for (const m of block.matchAll(/slug:\s*["']([^"']+)["']/g)) out.add(m[1]); + return out; +} + +export function readSlugs(file) { + const src = readFileSync(file, "utf8"); + return { + authors: slugsIn(src, "authors"), + categories: slugsIn(src, "categories"), + tags: slugsIn(src, "tags"), + }; +} + +/** Insert { slug, name } as the first element of the named array if slug is absent. */ +export function ensureEntry(file, arrayName, slug, name) { + let src = readFileSync(file, "utf8"); + if (slugsIn(src, arrayName).has(slug)) return false; + const r = blockRange(src, arrayName); + if (!r) throw new Error(`Array ${arrayName} not found in ${file}`); + const entry = `\n { slug: ${JSON.stringify(slug)}, name: ${JSON.stringify(name)} },`; + src = src.slice(0, r.open + 1) + entry + src.slice(r.open + 1); + writeFileSync(file, src); + return true; +} + +/** Insert a full author object if the slug is absent. author = {slug,name,bio,longBio,avatar} */ +export function ensureAuthor(file, author) { + let src = readFileSync(file, "utf8"); + if (slugsIn(src, "authors").has(author.slug)) return false; + const r = blockRange(src, "authors"); + if (!r) throw new Error(`authors array not found in ${file}`); + const obj = + `\n {\n` + + ` slug: ${JSON.stringify(author.slug)},\n` + + ` name: ${JSON.stringify(author.name)},\n` + + ` bio: ${JSON.stringify(author.bio || "")},\n` + + ` longBio: ${JSON.stringify(author.longBio || author.bio || "")},\n` + + ` avatar: ${JSON.stringify(author.avatar || "")},\n` + + ` },`; + src = src.slice(0, r.open + 1) + obj + src.slice(r.open + 1); + writeFileSync(file, src); + return true; +} diff --git a/content-pipeline/scripts/lib/build.mjs b/content-pipeline/scripts/lib/build.mjs new file mode 100644 index 0000000..48e54d3 --- /dev/null +++ b/content-pipeline/scripts/lib/build.mjs @@ -0,0 +1,38 @@ +import { spawn } from "node:child_process"; +import { mkdirSync } from "node:fs"; +import { withLock } from "./lock.mjs"; + +function run(cmd, args, opts = {}) { + return new Promise((resolve, reject) => { + const child = spawn(cmd, args, { stdio: "inherit", ...opts }); + child.on("error", reject); + child.on("close", (code) => + code === 0 ? resolve() : reject(new Error(`${cmd} ${args.join(" ")} exited ${code}`)) + ); + }); +} + +/** Absolute path of the global build lock. Callers that build outside buildSite() + * (e.g. console preview builds) must take THIS same lock to serialize. */ +export function buildLockPath(projectRoot) { + return `${projectRoot}/content-pipeline/state/build.lock`; +} + +/** + * Build the Astro app and fix ownership so Apache can serve the output. + * Serialized behind the global build lock so it never races the console's + * preview/publish builds (concurrent astro builds would OOM this box). + */ +export async function buildSite({ projectRoot, appDir }) { + const lockPath = buildLockPath(projectRoot); + mkdirSync(`${projectRoot}/content-pipeline/state`, { recursive: true }); + await withLock(lockPath, async () => { + await run("npm", ["run", "build"], { cwd: `${projectRoot}/${appDir}` }); + // Best-effort ownership fix (ignore failure if not running as root). + try { + await run("chown", ["-R", "www:www", `${projectRoot}/${appDir}`, `${projectRoot}/public`]); + } catch (e) { + console.warn(`[build] chown skipped: ${e.message}`); + } + }); +} diff --git a/content-pipeline/scripts/lib/calendar.mjs b/content-pipeline/scripts/lib/calendar.mjs new file mode 100644 index 0000000..eebe187 --- /dev/null +++ b/content-pipeline/scripts/lib/calendar.mjs @@ -0,0 +1,95 @@ +// Atomic, locked access to calendar.json so concurrent runs (write-daily, publish-tick, +// research refill) can't clobber each other. Every mutation re-reads under an exclusive +// lockfile, mutates, and writes — no long-held in-memory copies. +import { existsSync, readFileSync, writeFileSync, openSync, closeSync, unlinkSync, statSync } from "node:fs"; + +const LOCK_STALE_MS = 10 * 60 * 1000; // a lock older than this is presumed orphaned +const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); + +async function acquire(calPath, timeoutMs = 120000) { + const lp = `${calPath}.lock`; + const start = Date.now(); + for (;;) { + try { + const fd = openSync(lp, "wx"); // exclusive create — fails if it exists + writeFileSync(lp, `${process.pid} ${new Date().toISOString()}`); + closeSync(fd); + return lp; + } catch { + try { + if (Date.now() - statSync(lp).mtimeMs > LOCK_STALE_MS) { + unlinkSync(lp); + continue; + } + } catch { + /* lock vanished — retry */ + } + if (Date.now() - start > timeoutMs) throw new Error(`calendar lock timeout: ${lp}`); + await sleep(200); + } + } +} +const release = (lp) => { + try { + unlinkSync(lp); + } catch { + /* already gone */ + } +}; + +export async function withCalendarLock(calPath, fn) { + const lp = await acquire(calPath); + try { + return await fn(); + } finally { + release(lp); + } +} + +export const readCalendar = (calPath) => + existsSync(calPath) ? JSON.parse(readFileSync(calPath, "utf8")) : []; + +const writeCalendar = (calPath, cal) => writeFileSync(calPath, JSON.stringify(cal, null, 2)); + +/** Locked read-modify-write of one entry (Object.assign patch). Returns the updated entry. */ +export async function updateEntry(calPath, slug, patch) { + return withCalendarLock(calPath, () => { + const cal = readCalendar(calPath); + const e = cal.find((x) => x.slug === slug); + if (e) { + Object.assign(e, patch); + writeCalendar(calPath, cal); + } + return e; + }); +} + +/** Locked append if the slug is not already present. */ +export async function addEntry(calPath, entry) { + return withCalendarLock(calPath, () => { + const cal = readCalendar(calPath); + if (!cal.some((x) => x.slug === entry.slug)) { + cal.push(entry); + writeCalendar(calPath, cal); + } + return entry; + }); +} + +/** Locked merge: append each new entry whose slug isn't present. Returns count added. */ +export async function mergeEntries(calPath, entries) { + return withCalendarLock(calPath, () => { + const cal = readCalendar(calPath); + const have = new Set(cal.map((x) => x.slug)); + let added = 0; + for (const e of entries) { + if (!have.has(e.slug)) { + cal.push(e); + have.add(e.slug); + added++; + } + } + writeCalendar(calPath, cal); + return added; + }); +} diff --git a/content-pipeline/scripts/lib/claude.mjs b/content-pipeline/scripts/lib/claude.mjs new file mode 100644 index 0000000..85573b6 --- /dev/null +++ b/content-pipeline/scripts/lib/claude.mjs @@ -0,0 +1,71 @@ +import { spawn } from "node:child_process"; +import { createWriteStream, readFileSync } from "node:fs"; + +const CLAUDE_BIN = process.env.CLAUDE_BIN || "claude"; + +/** + * Run headless Claude Code and return the parsed JSON result. + * The system prompt file is read and passed via --append-system-prompt so we don't + * depend on the --append-system-prompt-file flag variant. + */ +export function runClaude({ + prompt, + systemPromptFile, + model, + mcpConfig, + allowedTools, + addDirs = [], + cwd, + logFile, + permissionMode = "acceptEdits", // bypassPermissions is blocked under root +}) { + return new Promise((resolve, reject) => { + const args = [ + "-p", + prompt, + "--output-format", + "json", + "--permission-mode", + permissionMode, + ]; + if (model) args.push("--model", model); + if (systemPromptFile) { + const sys = readFileSync(systemPromptFile, "utf8"); + args.push("--append-system-prompt", sys); + } + if (mcpConfig) args.push("--mcp-config", mcpConfig); + if (allowedTools) args.push("--allowed-tools", allowedTools); + for (const d of addDirs) args.push("--add-dir", d); + + const log = logFile ? createWriteStream(logFile, { flags: "a" }) : null; + if (log) log.write(`\n\n===== ${new Date().toISOString()} claude run =====\n`); + + const child = spawn(CLAUDE_BIN, args, { + cwd, + env: process.env, + stdio: ["ignore", "pipe", "pipe"], + }); + let stdout = ""; + let stderr = ""; + child.stdout.on("data", (b) => { + stdout += b; + if (log) log.write(b); + }); + child.stderr.on("data", (b) => { + stderr += b; + if (log) log.write(b); + }); + child.on("error", reject); + child.on("close", (code) => { + if (log) log.end(); + if (code !== 0) { + return reject(new Error(`claude exited ${code}: ${stderr.slice(-2000) || stdout.slice(-2000)}`)); + } + try { + resolve(JSON.parse(stdout)); + } catch { + resolve({ result: stdout.trim(), raw: true }); + } + }); + }); +} diff --git a/content-pipeline/scripts/lib/git.mjs b/content-pipeline/scripts/lib/git.mjs new file mode 100644 index 0000000..89b4fc3 --- /dev/null +++ b/content-pipeline/scripts/lib/git.mjs @@ -0,0 +1,37 @@ +import { spawn } from "node:child_process"; + +function run(cmd, args, opts = {}) { + return new Promise((resolve, reject) => { + let out = ""; + let err = ""; + const child = spawn(cmd, args, { stdio: ["ignore", "pipe", "pipe"], ...opts }); + child.stdout.on("data", (b) => (out += b)); + child.stderr.on("data", (b) => (err += b)); + child.on("error", reject); + child.on("close", (code) => + code === 0 + ? resolve(out) + : reject(new Error(`git ${args.join(" ")} exited ${code}: ${err.trim()}`)) + ); + }); +} + +/** + * Stage the given paths (relative to projectRoot) and commit if anything changed. + * Best-effort: never throws, so a git hiccup can't block a live publish. Returns + * the new commit hash, or null if nothing was staged / git failed. + * Keeping the working tree committed is what lets the console's git worktree + + * `merge --ff-only` operations run against a clean tree. + */ +export async function gitCommitPaths(projectRoot, paths, message) { + try { + await run("git", ["-C", projectRoot, "add", "--", ...paths]); + const staged = await run("git", ["-C", projectRoot, "diff", "--cached", "--name-only"]); + if (!staged.trim()) return null; + await run("git", ["-C", projectRoot, "commit", "-q", "-m", message]); + return (await run("git", ["-C", projectRoot, "rev-parse", "HEAD"])).trim(); + } catch (e) { + console.warn(`[git] commit skipped: ${e.message}`); + return null; + } +} diff --git a/content-pipeline/scripts/lib/lock.mjs b/content-pipeline/scripts/lib/lock.mjs new file mode 100644 index 0000000..42e6fff --- /dev/null +++ b/content-pipeline/scripts/lib/lock.mjs @@ -0,0 +1,46 @@ +// Generic exclusive file lock, same idiom as calendar.mjs but reusable for any +// critical section. Used for the global BUILD lock so cron builds (promoteDraft) +// and Developer Console builds (preview + publish) never run npm/astro +// concurrently — important on this memory-constrained box. +import { writeFileSync, openSync, closeSync, unlinkSync, statSync } from "node:fs"; + +const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); + +/** + * Acquire an exclusive lockfile, run fn, always release. + * @param {string} lockPath absolute path to the lockfile + * @param {() => Promise|T} fn critical section + * @param {{timeoutMs?: number, staleMs?: number}} [opts] + * @returns {Promise} + */ +export async function withLock(lockPath, fn, { timeoutMs = 300000, staleMs = 15 * 60 * 1000 } = {}) { + const start = Date.now(); + for (;;) { + try { + const fd = openSync(lockPath, "wx"); // exclusive create — fails if it exists + writeFileSync(lockPath, `${process.pid} ${new Date().toISOString()}`); + closeSync(fd); + break; + } catch { + try { + if (Date.now() - statSync(lockPath).mtimeMs > staleMs) { + unlinkSync(lockPath); // presumed orphaned + continue; + } + } catch { + /* lock vanished between failed create and stat — retry immediately */ + } + if (Date.now() - start > timeoutMs) throw new Error(`lock timeout: ${lockPath}`); + await sleep(200); + } + } + try { + return await fn(); + } finally { + try { + unlinkSync(lockPath); + } catch { + /* already gone */ + } + } +} diff --git a/content-pipeline/scripts/lib/publish.mjs b/content-pipeline/scripts/lib/publish.mjs new file mode 100644 index 0000000..6561fa7 --- /dev/null +++ b/content-pipeline/scripts/lib/publish.mjs @@ -0,0 +1,84 @@ +import { existsSync, readFileSync, writeFileSync, renameSync, cpSync, rmSync } from "node:fs"; +import { join } from "node:path"; +import { readFrontmatter, fmString, fmArray, titleCase } from "./util.mjs"; +import { readSlugs, ensureEntry } from "./blogData.mjs"; +import { updateEntry } from "./calendar.mjs"; +import { buildSite } from "./build.mjs"; +import { gitCommitPaths } from "./git.mjs"; + +/** + * Promote a draft into the Astro blog and build the site. + * Shared by approve.mjs (manual) and publish-tick.mjs (auto). Does NOT do the SEO gate — + * callers decide whether the draft is allowed to publish. + * @param {object} cfg parsed config.json + * @param {string} slug + * @param {{dateOverride?: string}} opts dateOverride = ISO timestamp to stamp as the post date + * @returns {Promise<{dest:string,url:string,taxonomyAdded:string[]}>} + */ +export async function promoteDraft(cfg, slug, { dateOverride } = {}) { + const root = cfg.paths.projectRoot; + const draftDir = join(root, cfg.paths.draftsDir, slug); + const mdxPath = join(draftDir, "index.mdx"); + if (!existsSync(mdxPath)) throw new Error(`no draft at ${mdxPath}`); + if (!existsSync(join(draftDir, "cover.jpg"))) throw new Error("draft is missing cover.jpg"); + + // Stamp a specific published date/time into the frontmatter (randomized publish time). + if (dateOverride) { + const raw = readFileSync(mdxPath, "utf8"); + writeFileSync(mdxPath, raw.replace(/^date:.*$/m, `date: ${dateOverride}`)); + } + + const fm = readFrontmatter(readFileSync(mdxPath, "utf8")); + const author = fmString(fm, "author"); + const category = fmString(fm, "category"); + const tags = fmArray(fm, "tags"); + + const blogDataFile = join(root, cfg.paths.blogDataFile); + const known = readSlugs(blogDataFile); + if (author && !known.authors.has(author)) { + throw new Error(`author "${author}" is not in blog-data.js — add it first`); + } + + const added = []; + if (category && !known.categories.has(category)) { + ensureEntry(blogDataFile, "categories", category, titleCase(category)); + added.push(`category:${category}`); + } + for (const t of tags) { + if (!known.tags.has(t)) { + ensureEntry(blogDataFile, "tags", t, titleCase(t)); + added.push(`tag:${t}`); + } + } + + const dest = join(root, cfg.paths.blogContentDir, slug); + if (existsSync(dest)) throw new Error(`destination already exists: ${dest}`); + try { + renameSync(draftDir, dest); + } catch { + cpSync(draftDir, dest, { recursive: true }); + rmSync(draftDir, { recursive: true, force: true }); + } + + const calendarPath = join(root, cfg.paths.calendar); + await updateEntry( + calendarPath, + slug, + dateOverride ? { status: "published", publishedAt: dateOverride } : { status: "published" } + ); + + await buildSite({ projectRoot: root, appDir: cfg.paths.appDir }); + const out = join(root, "public", "blog", slug, "index.html"); + if (!existsSync(out)) throw new Error(`expected output not found: ${out}`); + + // Version the published content so the working tree stays clean for the + // Developer Console's git worktree/merge operations. Best-effort — a git + // hiccup must never keep already-built content off the live site. + const commit = await gitCommitPaths( + root, + [join(cfg.paths.blogContentDir, slug), cfg.paths.blogDataFile], + `content: publish ${slug}` + ); + + return { dest, url: `${cfg.site.url}/blog/${slug}/`, taxonomyAdded: added, commit }; +} diff --git a/content-pipeline/scripts/lib/util.mjs b/content-pipeline/scripts/lib/util.mjs new file mode 100644 index 0000000..5c7aeff --- /dev/null +++ b/content-pipeline/scripts/lib/util.mjs @@ -0,0 +1,84 @@ +import { readFileSync } from "node:fs"; + +export function slugify(s) { + return String(s) + .toLowerCase() + .normalize("NFKD") + .replace(/[̀-ͯ]/g, "") + .replace(/[^a-z0-9]+/g, "-") + .replace(/^-+|-+$/g, "") + .slice(0, 80); +} + +export function titleCase(slug) { + return String(slug) + .split("-") + .map((w) => w.charAt(0).toUpperCase() + w.slice(1)) + .join(" "); +} + +export function todayInTz(tz) { + const fmt = new Intl.DateTimeFormat("en-CA", { + timeZone: tz, + year: "numeric", + month: "2-digit", + day: "2-digit", + }); + return fmt.format(new Date()); // en-CA => YYYY-MM-DD +} + +/** ISO timestamp with Medellín's fixed -05:00 offset, e.g. 2026-06-28T14:37:09-05:00. */ +export function isoInBogota(date = new Date()) { + const parts = new Intl.DateTimeFormat("en-CA", { + timeZone: "America/Bogota", + year: "numeric", + month: "2-digit", + day: "2-digit", + hour: "2-digit", + minute: "2-digit", + second: "2-digit", + hourCycle: "h23", + }).formatToParts(date); + const g = (t) => parts.find((p) => p.type === t).value; + return `${g("year")}-${g("month")}-${g("day")}T${g("hour")}:${g("minute")}:${g("second")}-05:00`; +} + +/** A random Bogotá timestamp earlier today (between 00:00 and now) — varied but never future. */ +export function randomTimestampTodaySoFar(tz = "America/Bogota") { + const today = todayInTz(tz); + const startMs = Date.parse(`${today}T00:00:00-05:00`); + const nowMs = Date.now(); + const r = startMs + Math.floor(Math.random() * Math.max(1, nowMs - startMs)); + return isoInBogota(new Date(r)); +} + +export function addDays(isoDate, n) { + const d = new Date(isoDate + "T00:00:00Z"); + d.setUTCDate(d.getUTCDate() + n); + return d.toISOString().slice(0, 10); +} + +export function loadJson(path) { + return JSON.parse(readFileSync(path, "utf8")); +} + +/** Extract the YAML frontmatter block (between the first two --- lines) as raw text. */ +export function readFrontmatter(mdx) { + const m = mdx.match(/^---\n([\s\S]*?)\n---/); + return m ? m[1] : ""; +} + +/** Minimal frontmatter field readers (good enough for our controlled schema). */ +export function fmString(fm, key) { + const m = fm.match(new RegExp(`^${key}:\\s*"?([^"\\n]+?)"?\\s*$`, "m")); + return m ? m[1].trim() : null; +} + +export function fmArray(fm, key) { + const m = fm.match(new RegExp(`^${key}:\\s*\\[([^\\]]*)\\]`, "m")); + if (!m) return []; + return m[1] + .split(",") + .map((s) => s.trim().replace(/^["']|["']$/g, "")) + .filter(Boolean); +} diff --git a/content-pipeline/scripts/list-drafts.mjs b/content-pipeline/scripts/list-drafts.mjs new file mode 100644 index 0000000..3452066 --- /dev/null +++ b/content-pipeline/scripts/list-drafts.mjs @@ -0,0 +1,38 @@ +#!/usr/bin/env node +// List pending drafts awaiting review. +// Usage: node content-pipeline/scripts/list-drafts.mjs +import { readdirSync, existsSync, readFileSync } from "node:fs"; +import { dirname, resolve, join } from "node:path"; +import { fileURLToPath } from "node:url"; +import { loadJson, readFrontmatter, fmString } from "./lib/util.mjs"; + +const HERE = dirname(fileURLToPath(import.meta.url)); +const PIPELINE = resolve(HERE, ".."); +const cfg = loadJson(join(PIPELINE, "config.json")); +const draftsRoot = join(cfg.paths.projectRoot, cfg.paths.draftsDir); + +if (!existsSync(draftsRoot)) { + console.log("No drafts directory yet."); + process.exit(0); +} +const dirs = readdirSync(draftsRoot, { withFileTypes: true }).filter((d) => d.isDirectory()); +if (!dirs.length) { + console.log("No drafts pending."); + process.exit(0); +} + +console.log(`Pending drafts (${dirs.length}):\n`); +for (const d of dirs) { + const mdx = join(draftsRoot, d.name, "index.mdx"); + const hasCover = existsSync(join(draftsRoot, d.name, "cover.jpg")); + let title = "(no index.mdx)"; + let cat = ""; + if (existsSync(mdx)) { + const fm = readFrontmatter(readFileSync(mdx, "utf8")); + title = fmString(fm, "title") || d.name; + cat = fmString(fm, "category") || ""; + } + console.log(` • ${d.name}`); + console.log(` ${title}${cat ? ` [${cat}]` : ""} cover:${hasCover ? "yes" : "NO"}`); + console.log(` approve: node content-pipeline/scripts/approve.mjs ${d.name}\n`); +} diff --git a/content-pipeline/scripts/news-radar.mjs b/content-pipeline/scripts/news-radar.mjs new file mode 100644 index 0000000..78c96c2 --- /dev/null +++ b/content-pipeline/scripts/news-radar.mjs @@ -0,0 +1,112 @@ +#!/usr/bin/env node +// News radar: discover timely Medellín food news/events and maintain news-queue.json. +// Usage: node content-pipeline/scripts/news-radar.mjs +import { existsSync, readFileSync, writeFileSync, readdirSync } from "node:fs"; +import { dirname, resolve, join } from "node:path"; +import { fileURLToPath } from "node:url"; +import { runClaude } from "./lib/claude.mjs"; +import { loadJson, todayInTz, addDays } from "./lib/util.mjs"; + +const HERE = dirname(fileURLToPath(import.meta.url)); +const PIPELINE = resolve(HERE, ".."); +const cfg = loadJson(join(PIPELINE, "config.json")); +const root = cfg.paths.projectRoot; + +const queuePath = join(root, "content-pipeline", "news-queue.json"); +const scanPath = join(root, "content-pipeline", "news-scan.json"); + +const loadArr = (p) => (existsSync(p) ? loadJson(p) : []); +const publishedSlugs = () => { + const d = join(root, cfg.paths.blogContentDir); + return existsSync(d) + ? readdirSync(d, { withFileTypes: true }).filter((x) => x.isDirectory()).map((x) => x.name) + : []; +}; +const calendarSlugs = () => { + const p = join(root, cfg.paths.calendar); + return existsSync(p) ? loadJson(p).map((e) => e.slug) : []; +}; + +/** Scan for fresh news and merge into news-queue.json. Returns the merged queue. */ +export async function runNewsRadar() { + if (!cfg.news?.enabled) { + console.log("[news] news.enabled is false — skipping."); + return loadArr(queuePath); + } + const today = todayInTz(cfg.editorial.timezone); + const recencyDays = cfg.news.recencyDays ?? 5; + const maxPerScan = cfg.news.maxPerScan ?? 8; + + const existing = loadArr(queuePath); + const known = new Set([...existing.map((i) => i.slug), ...publishedSlugs(), ...calendarSlugs()]); + + const prompt = `Find timely Medellín food/restaurant news to publish about now. + +- Today: ${today} +- Prefer items from the last ~2-3 weeks. Return at most ${maxPerScan} items (fewer is fine; [] if nothing fresh). +- Write the JSON array to: ${scanPath} +- Do NOT reuse any of these existing slugs: ${[...known].join(", ") || "(none)"} +- Existing published posts: ${join(root, cfg.paths.blogContentDir)} + +Follow the output contract in your system prompt exactly.`; + + await runClaude({ + prompt, + systemPromptFile: join(PIPELINE, "prompts/news-radar.system.md"), + model: cfg.models.research, + allowedTools: "Read Glob Grep WebSearch Write", + addDirs: [root], + cwd: root, + logFile: join(root, cfg.paths.logsDir, `news-${today}.log`), + }); + + const scanned = loadArr(scanPath); + + // Prune: drop stale FRESH items (older than recency window); keep used items for dedup history. + const cutoff = addDays(today, -recencyDays); + const kept = existing.filter((i) => i.status === "used" || (i.discovered || today) >= cutoff); + const keptSlugs = new Set(kept.map((i) => i.slug)); + const dedupe = new Set([...keptSlugs, ...publishedSlugs(), ...calendarSlugs()]); + + let added = 0; + for (const item of scanned) { + if (!item?.slug || dedupe.has(item.slug)) continue; + kept.push({ ...item, discovered: item.discovered || today, status: "fresh" }); + dedupe.add(item.slug); + added++; + } + // Newest + hottest first. + kept.sort((a, b) => (b.freshnessScore ?? 0) - (a.freshnessScore ?? 0) || (b.discovered || "").localeCompare(a.discovered || "")); + writeFileSync(queuePath, JSON.stringify(kept, null, 2)); + + const fresh = kept.filter((i) => i.status === "fresh").length; + console.log(`[news] scanned ${scanned.length}, added ${added}, queue now ${kept.length} (${fresh} fresh).`); + return kept; +} + +/** The freshest unused news item within the recency window, or null. */ +export function pickFreshNews(cfg2 = cfg) { + if (!cfg2.news?.enabled || !existsSync(queuePath)) return null; + const today = todayInTz(cfg2.editorial.timezone); + const cutoff = addDays(today, -(cfg2.news.recencyDays ?? 5)); + const fresh = loadJson(queuePath) + .filter((i) => i.status === "fresh" && (i.discovered || today) >= cutoff) + .sort((a, b) => (b.freshnessScore ?? 0) - (a.freshnessScore ?? 0) || (b.discovered || "").localeCompare(a.discovered || "")); + return fresh[0] || null; +} + +/** Mark a news item used (after it's been drafted). */ +export function markNewsUsed(slug) { + if (!existsSync(queuePath)) return; + const q = loadJson(queuePath); + const item = q.find((i) => i.slug === slug); + if (item) { + item.status = "used"; + writeFileSync(queuePath, JSON.stringify(q, null, 2)); + } +} + +// CLI +if (import.meta.url === `file://${process.argv[1]}`) { + await runNewsRadar(); +} diff --git a/content-pipeline/scripts/publish-tick.mjs b/content-pipeline/scripts/publish-tick.mjs new file mode 100644 index 0000000..520dbdb --- /dev/null +++ b/content-pipeline/scripts/publish-tick.mjs @@ -0,0 +1,101 @@ +#!/usr/bin/env node +// Auto-publisher. Run frequently by cron (e.g. every 15 min). Once per day, after a morning +// floor time, it publishes ONE eligible draft (passing the SEO gate) so posts go live in the +// morning — but stamps each with a RANDOM earlier-today timestamp so published times aren't a +// fixed-minute metronome. News drafts are fast-tracked ahead of evergreen. +// Usage: node content-pipeline/scripts/publish-tick.mjs [--now] (--now ignores the floor) +import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync } from "node:fs"; +import { dirname, resolve, join } from "node:path"; +import { fileURLToPath } from "node:url"; +import { loadJson, todayInTz, randomTimestampTodaySoFar } from "./lib/util.mjs"; +import { promoteDraft } from "./lib/publish.mjs"; + +const HERE = dirname(fileURLToPath(import.meta.url)); +const PIPELINE = resolve(HERE, ".."); +const cfg = loadJson(join(PIPELINE, "config.json")); +const root = cfg.paths.projectRoot; +const FORCE_NOW = process.argv.includes("--now"); + +if (!cfg.publish?.auto) { + console.log("[publish-tick] publish.auto is false — nothing to do."); + process.exit(0); +} + +const today = todayInTz(cfg.editorial.timezone); +const stateDir = join(root, "content-pipeline", "state"); +mkdirSync(stateDir, { recursive: true }); +const statePath = join(stateDir, `publish-${today}.json`); + +let state = existsSync(statePath) ? loadJson(statePath) : { published: false, slug: null }; + +if (state.published) { + console.log(`[publish-tick] already published today (${state.slug}).`); + process.exit(0); +} + +// Morning floor: don't publish before this Bogotá time (gives the 06:00 writer time to draft + +// audit + auto-revise so the day's freshest article is the one that goes live). +const notBefore = cfg.publish.notBefore || "07:00"; +const floorMs = Date.parse(`${today}T${notBefore}:00-05:00`); +if (!FORCE_NOW && Date.now() < floorMs) { + console.log(`[publish-tick] waiting — floor ${notBefore} Bogotá (${new Date(floorMs).toISOString()}).`); + process.exit(0); +} + +// Find an eligible draft: on disk, has cover, and passes the SEO gate. +const minScore = cfg.seo?.minScore ?? 85; +const draftsRoot = join(root, cfg.paths.draftsDir); +const calPath = join(root, cfg.paths.calendar); +const calendar = existsSync(calPath) ? loadJson(calPath) : []; +const entryOf = (slug) => calendar.find((e) => e.slug === slug) || {}; +const dateOf = (slug) => entryOf(slug).date || "9999-12-31"; + +function eligible(slug) { + const d = join(draftsRoot, slug); + if (!existsSync(join(d, "index.mdx")) || !existsSync(join(d, "cover.jpg"))) return false; + const auditPath = join(d, "seo-review.json"); + if (!existsSync(auditPath)) return false; + try { + const a = loadJson(auditPath); + return a.verdict !== "fail" && (a.overall ?? 0) >= minScore; + } catch { + return false; + } +} + +const candidates = existsSync(draftsRoot) + ? readdirSync(draftsRoot, { withFileTypes: true }) + .filter((x) => x.isDirectory()) + .map((x) => x.name) + .filter(eligible) + // News drafts first (hottest, then by date); evergreen after, oldest first. + .sort((a, b) => { + const ea = entryOf(a); + const eb = entryOf(b); + const na = ea.news ? 1 : 0; + const nb = eb.news ? 1 : 0; + if (na !== nb) return nb - na; + if (na) return (eb.freshnessScore ?? 0) - (ea.freshnessScore ?? 0) || dateOf(a).localeCompare(dateOf(b)); + return dateOf(a).localeCompare(dateOf(b)); + }) + : []; + +if (!candidates.length) { + console.log("[publish-tick] no SEO-passing draft ready to publish (leaving queue as-is)."); + process.exit(0); +} + +const slug = candidates[0]; +const stamp = randomTimestampTodaySoFar(cfg.editorial.timezone); // morning go-live, random timestamp +console.log(`[publish-tick] auto-publishing "${slug}" with timestamp ${stamp}…`); + +try { + const { url, taxonomyAdded } = await promoteDraft(cfg, slug, { dateOverride: stamp }); + if (taxonomyAdded.length) console.log(`[publish-tick] registered: ${taxonomyAdded.join(", ")}`); + state = { published: true, slug, publishedAt: stamp }; + writeFileSync(statePath, JSON.stringify(state, null, 2)); + console.log(`[publish-tick] LIVE → ${url}`); +} catch (e) { + console.error(`[publish-tick] publish failed: ${e.message}`); + process.exit(1); +} diff --git a/content-pipeline/scripts/research.mjs b/content-pipeline/scripts/research.mjs new file mode 100644 index 0000000..6464b2e --- /dev/null +++ b/content-pipeline/scripts/research.mjs @@ -0,0 +1,94 @@ +#!/usr/bin/env node +// Editorial research: top up the EVERGREEN backlog in calendar.json (additive, deduped). +// Timely news is handled separately by the news radar. Usage: +// node content-pipeline/scripts/research.mjs [count] +import { readdirSync, existsSync } from "node:fs"; +import { dirname, resolve, join } from "node:path"; +import { fileURLToPath } from "node:url"; +import { runClaude } from "./lib/claude.mjs"; +import { loadJson, todayInTz, addDays } from "./lib/util.mjs"; +import { readCalendar, mergeEntries } from "./lib/calendar.mjs"; + +const HERE = dirname(fileURLToPath(import.meta.url)); +const PIPELINE = resolve(HERE, ".."); +const cfg = loadJson(join(PIPELINE, "config.json")); +const root = cfg.paths.projectRoot; + +const publishedSlugs = () => { + const d = join(root, cfg.paths.blogContentDir); + return existsSync(d) + ? readdirSync(d, { withFileTypes: true }).filter((x) => x.isDirectory()).map((x) => x.name) + : []; +}; + +/** Scan for evergreen topics and additively merge them into calendar.json. Returns count added. */ +export async function runResearch({ count } = {}) { + const n = count || cfg.editorial.calendarDays; + const today = todayInTz(cfg.editorial.timezone); + const calendarPath = join(root, cfg.paths.calendar); + const scanPath = join(root, "content-pipeline", "research-scan.json"); + + // Per-type target counts from the configured mix. + const types = Object.keys(cfg.editorial.contentMix); + const counts = {}; + let assigned = 0; + for (const t of types) { + counts[t] = Math.round(cfg.editorial.contentMix[t] * n); + assigned += counts[t]; + } + const biggest = types.reduce((a, b) => (counts[a] >= counts[b] ? a : b)); + counts[biggest] += n - assigned; + + const exclude = [...new Set([...readCalendar(calendarPath).map((e) => e.slug), ...publishedSlugs()])]; + + const prompt = `Propose ${n} evergreen Comiida topics. + +- Write a JSON array of ${n} topic proposals to: ${scanPath} +- Do NOT assign dates (the pipeline schedules them). +- Allowed content types: ${types.join(", ")} +- Rough target counts per type: ${JSON.stringify(counts)} +- DO NOT reuse any of these slugs: ${exclude.join(", ") || "(none yet)"} +- Read existing posts if useful: ${join(root, cfg.paths.blogContentDir)} + +Follow the output contract in your system prompt exactly.`; + + console.log(`[research] proposing ${n} evergreen topics, mix ${JSON.stringify(counts)}`); + + await runClaude({ + prompt, + systemPromptFile: join(PIPELINE, "prompts/research.system.md"), + model: cfg.models.research, + allowedTools: "Read Glob Grep WebSearch Write", + addDirs: [root], + cwd: root, + logFile: join(root, cfg.paths.logsDir, `research-${today}.log`), + }); + + const proposals = existsSync(scanPath) ? loadJson(scanPath) : []; + if (!proposals.length) { + console.log("[research] no proposals produced."); + return 0; + } + + // Schedule new topics one per day, starting the day after the latest existing date. + const dates = readCalendar(calendarPath).map((e) => (e.date || "").slice(0, 10)).filter(Boolean); + const maxDate = dates.sort().pop(); + let next = maxDate && maxDate >= today ? addDays(maxDate, 1) : today; + + const entries = proposals.map((p) => { + const entry = { ...p, date: next, status: "planned" }; + next = addDays(next, 1); + return entry; + }); + + const added = await mergeEntries(calendarPath, entries); + const planned = readCalendar(calendarPath).filter((e) => e.status === "planned").length; + console.log(`[research] added ${added} evergreen topics. Planned backlog now: ${planned}.`); + return added; +} + +// CLI +if (import.meta.url === `file://${process.argv[1]}`) { + const count = parseInt(process.argv[2], 10) || undefined; + await runResearch({ count }); +} diff --git a/content-pipeline/scripts/revise.mjs b/content-pipeline/scripts/revise.mjs new file mode 100644 index 0000000..a5fa1cb --- /dev/null +++ b/content-pipeline/scripts/revise.mjs @@ -0,0 +1,91 @@ +#!/usr/bin/env node +// Auto-revise loop: feed the SEO audit's fixes back to the writer, re-audit, repeat until the +// draft passes the gate or maxReviseAttempts is reached. +// Usage: node content-pipeline/scripts/revise.mjs +import { existsSync } from "node:fs"; +import { dirname, resolve, join } from "node:path"; +import { fileURLToPath } from "node:url"; +import { runClaude } from "./lib/claude.mjs"; +import { loadJson } from "./lib/util.mjs"; +import { runSeoReview } from "./seo-review.mjs"; + +const HERE = dirname(fileURLToPath(import.meta.url)); +const PIPELINE = resolve(HERE, ".."); +const cfg = loadJson(join(PIPELINE, "config.json")); +const root = cfg.paths.projectRoot; + +const failing = (audit, minScore) => + !audit || audit.verdict === "fail" || (audit.overall ?? 0) < minScore; + +/** + * Revise a draft until it passes the SEO gate (or attempts run out). + * @returns {Promise<{audit:object|null, attempts:number}>} + */ +export async function runRevise(slug, { maxAttempts } = {}) { + const max = maxAttempts ?? cfg.seo?.maxReviseAttempts ?? 2; + const minScore = cfg.seo?.minScore ?? 85; + const draftDir = join(root, cfg.paths.draftsDir, slug); + const mdxPath = join(draftDir, "index.mdx"); + const auditPath = join(draftDir, "seo-review.json"); + if (!existsSync(mdxPath)) { + console.error(`[revise] no draft at ${mdxPath}`); + return { audit: null, attempts: 0 }; + } + + const calPathForEntry = join(root, cfg.paths.calendar); + const calEntry = existsSync(calPathForEntry) + ? loadJson(calPathForEntry).find((e) => e.slug === slug) + : null; + const firsthandNote = calEntry?.authorFirsthand + ? `\n\nIMPORTANT — authorFirsthand piece: a real, named author (${cfg.author?.name || "the site author"}) wrote this personally. Do NOT strip or neutralize the first-person voice and subjective opinions — that is authentic Experience and must be kept. Only add citations for objective facts (addresses, prices, dates, hours) or frame them honestly.` + : ""; + + let audit = existsSync(auditPath) ? loadJson(auditPath) : await runSeoReview(slug, { published: false }); + + let attempts = 0; + while (failing(audit, minScore) && attempts < max) { + attempts++; + const keyword = audit?.primaryKeyword || slug.replace(/-/g, " "); + console.log(`[revise] attempt ${attempts}/${max} on ${slug} (current: ${audit?.verdict} ${audit?.overall})`); + + const prompt = `Revise this draft to pass the SEO/EEAT gate (target ≥ ${minScore}). + +- Draft: ${mdxPath} +- Citations file: ${join(draftDir, "sources.json")} +- Audit to address (work through blocking + topFixes): ${auditPath} +- Primary keyword: ${keyword} +- Existing published posts (for internal links): ${join(root, cfg.paths.blogContentDir)}${firsthandNote} + +Follow the reviser instructions in your system prompt exactly. Edit the files in place.`; + + await runClaude({ + prompt, + systemPromptFile: join(PIPELINE, "prompts/reviser.system.md"), + model: cfg.models.writer, + allowedTools: "Read Write Edit Glob Grep WebSearch", + addDirs: [root], + cwd: root, + logFile: join(root, cfg.paths.logsDir, `revise-${slug}.log`), + }); + + audit = await runSeoReview(slug, { published: false }); + } + + const ok = !failing(audit, minScore); + console.log( + `[revise] ${slug}: ${ok ? "PASS" : "still failing"} after ${attempts} attempt(s) — ` + + `${audit?.verdict} ${audit?.overall}` + ); + return { audit, attempts }; +} + +// CLI +if (import.meta.url === `file://${process.argv[1]}`) { + const slug = process.argv[2]; + if (!slug) { + console.error("Usage: revise.mjs "); + process.exit(1); + } + const { audit } = await runRevise(slug); + process.exit(audit && audit.verdict === "pass" ? 0 : 2); +} diff --git a/content-pipeline/scripts/seo-review.mjs b/content-pipeline/scripts/seo-review.mjs new file mode 100644 index 0000000..e33e964 --- /dev/null +++ b/content-pipeline/scripts/seo-review.mjs @@ -0,0 +1,112 @@ +#!/usr/bin/env node +// SEO Specialist agent: audit one article for EEAT / spam-policy / on-page SEO / AEO / readability. +// Writes /seo-review.json. Usage: +// node scripts/seo-review.mjs [--published] +// (default: review the draft in content-pipeline/drafts/; --published reviews the +// live post in app/src/content/blog/) +import { existsSync, readFileSync } from "node:fs"; +import { dirname, resolve, join } from "node:path"; +import { fileURLToPath } from "node:url"; +import { runClaude } from "./lib/claude.mjs"; +import { loadJson, readFrontmatter, fmString } from "./lib/util.mjs"; +import { readSlugs } from "./lib/blogData.mjs"; + +const HERE = dirname(fileURLToPath(import.meta.url)); +const PIPELINE = resolve(HERE, ".."); +const cfg = loadJson(join(PIPELINE, "config.json")); +const root = cfg.paths.projectRoot; + +function postDirFor(slug, published) { + const draftDir = join(root, cfg.paths.draftsDir, slug); + const liveDir = join(root, cfg.paths.blogContentDir, slug); + if (published) return liveDir; + if (existsSync(join(draftDir, "index.mdx"))) return draftDir; + if (existsSync(join(liveDir, "index.mdx"))) return liveDir; + return draftDir; // will fail the existence check below +} + +function primaryKeywordFor(slug, mdxPath) { + const calPath = join(root, cfg.paths.calendar); + if (existsSync(calPath)) { + const entry = loadJson(calPath).find((e) => e.slug === slug); + if (entry?.primaryKeyword) return entry.primaryKeyword; + } + if (existsSync(mdxPath)) { + const t = fmString(readFrontmatter(readFileSync(mdxPath, "utf8")), "title"); + if (t) return t; + } + return slug.replace(/-/g, " "); +} + +/** Run the SEO audit for one post. Returns the parsed audit object, or null on failure. */ +export async function runSeoReview(slug, { published = false } = {}) { + const postDir = postDirFor(slug, published); + const mdxPath = join(postDir, "index.mdx"); + if (!existsSync(mdxPath)) { + console.error(`[seo] no article at ${mdxPath}`); + return null; + } + + const keyword = primaryKeywordFor(slug, mdxPath); + const calPathForEntry = join(root, cfg.paths.calendar); + const calEntry = existsSync(calPathForEntry) + ? loadJson(calPathForEntry).find((e) => e.slug === slug) + : null; + const firsthandNote = calEntry?.authorFirsthand + ? `\n\nIMPORTANT — authorFirsthand piece: a real, named author (${cfg.author?.name || "the site author"}) wrote this personally from genuine first-hand experience. First-person voice and subjective opinions are LEGITIMATE Experience (positive E-E-A-T), NOT fabrication. Do not flag the personal voice as an EEAT/spam violation; only require citations for objective, verifiable facts (addresses, prices, dates, hours).` + : ""; + const blogDir = join(root, cfg.paths.blogContentDir); + const categories = [...readSlugs(join(root, cfg.paths.blogDataFile)).categories]; + const auditPath = join(postDir, "seo-review.json"); + const logFile = join(root, cfg.paths.logsDir, `seo-${slug}.log`); + + const prompt = `Audit this Comiida article. + +- Article: ${mdxPath} +- Citations file (if present): ${join(postDir, "sources.json")} +- Primary keyword to target: ${keyword} +- Existing published posts (for internal-link validation): ${blogDir} +- Allowed category slugs: ${categories.join(", ")} +- Write your audit JSON to: ${auditPath}${firsthandNote} + +Follow the rubric and output contract in your system prompt exactly.`; + + await runClaude({ + prompt, + systemPromptFile: join(PIPELINE, "prompts/seo-review.system.md"), + model: cfg.models.reviewer, + allowedTools: "Read Glob Grep WebSearch Write", + addDirs: [root], + cwd: root, + logFile, + }); + + if (!existsSync(auditPath)) { + console.error(`[seo] audit not written: ${auditPath}`); + return null; + } + try { + return loadJson(auditPath); + } catch (e) { + console.error(`[seo] could not parse audit: ${e.message}`); + return null; + } +} + +// CLI +if (import.meta.url === `file://${process.argv[1]}`) { + const slug = process.argv[2]; + const published = process.argv.includes("--published"); + if (!slug) { + console.error("Usage: seo-review.mjs [--published]"); + process.exit(1); + } + const audit = await runSeoReview(slug, { published }); + if (!audit) process.exit(1); + const nb = (audit.blocking || []).length; + const nf = (audit.topFixes || []).length; + console.log(`[seo] ${slug}: ${audit.verdict} · ${audit.overall} · ${nb} blocking · ${nf} fixes`); + if (audit.summary) console.log(`[seo] ${audit.summary}`); + if (nb) console.log("[seo] blocking:\n - " + audit.blocking.join("\n - ")); + process.exit(audit.verdict === "pass" ? 0 : audit.verdict === "revise" ? 2 : 3); +} diff --git a/content-pipeline/scripts/write-daily.mjs b/content-pipeline/scripts/write-daily.mjs new file mode 100644 index 0000000..7fe153d --- /dev/null +++ b/content-pipeline/scripts/write-daily.mjs @@ -0,0 +1,199 @@ +#!/usr/bin/env node +// Daily writer: draft one article (MDX + cover.jpg + sources.json) into drafts//. +// Selection order: today's news (news radar) first → else the next evergreen calendar topic. +// Refills the evergreen backlog on demand when it runs low. All calendar writes are atomic. +// Usage: node content-pipeline/scripts/write-daily.mjs [slug] +import { existsSync, mkdirSync, writeFileSync } from "node:fs"; +import { dirname, resolve, join } from "node:path"; +import { fileURLToPath } from "node:url"; +import { runClaude } from "./lib/claude.mjs"; +import { loadJson, todayInTz } from "./lib/util.mjs"; +import { readCalendar, updateEntry, addEntry } from "./lib/calendar.mjs"; +import { runSeoReview } from "./seo-review.mjs"; +import { runRevise } from "./revise.mjs"; +import { runNewsRadar, pickFreshNews, markNewsUsed } from "./news-radar.mjs"; +import { runResearch } from "./research.mjs"; + +const HERE = dirname(fileURLToPath(import.meta.url)); +const PIPELINE = resolve(HERE, ".."); +const cfg = loadJson(join(PIPELINE, "config.json")); +const root = cfg.paths.projectRoot; + +if (cfg.author.slug === "REPLACE_ME") { + console.error("[write-daily] config.author is not set. Add the real author first."); + process.exit(1); +} + +const calendarPath = join(root, cfg.paths.calendar); +const today = todayInTz(cfg.editorial.timezone); +const stateDir = join(root, "content-pipeline", "state"); + +const argSlug = process.argv[2]; +let entry; + +if (argSlug) { + entry = readCalendar(calendarPath).find((e) => e.slug === argSlug); + if (!entry) { + console.error(`[write-daily] slug not found in calendar: ${argSlug}`); + process.exit(1); + } +} else { + // 0) User suggestions (from the admin dashboard) take top priority. + const suggestion = readCalendar(calendarPath) + .filter((e) => e.suggested && e.status === "planned") + .sort((a, b) => (a.date || "").localeCompare(b.date || ""))[0]; + if (suggestion) { + entry = suggestion; + console.log(`[write-daily] suggestion-first: "${entry.workingTitle}"`); + } else { + mkdirSync(stateDir, { recursive: true }); + + // 1) Ensure today's news scan ran (guarded once/day so the cadence doesn't matter). + if (cfg.news?.enabled) { + const newsStatePath = join(stateDir, `news-${today}.json`); + if (!existsSync(newsStatePath)) { + try { + await runNewsRadar(); + } catch (e) { + console.log(`[write-daily] news radar error (continuing): ${e.message}`); + } + writeFileSync(newsStatePath, JSON.stringify({ scanned: today }, null, 2)); + } + } + + // 2) Demand-driven evergreen refill when the backlog is low (guarded once/day). + const threshold = cfg.editorial.refillThreshold ?? 7; + const plannedCount = readCalendar(calendarPath).filter((e) => e.status === "planned").length; + const refillStatePath = join(stateDir, `research-${today}.json`); + if (plannedCount < threshold && !existsSync(refillStatePath)) { + console.log(`[write-daily] evergreen backlog low (${plannedCount} < ${threshold}) — refilling…`); + try { + const added = await runResearch({ count: cfg.editorial.refillCount ?? 14 }); + console.log(`[write-daily] refill added ${added} topics.`); + } catch (e) { + console.log(`[write-daily] refill error (continuing): ${e.message}`); + } + writeFileSync(refillStatePath, JSON.stringify({ refilled: today }, null, 2)); + } + + // 3) News-first selection; inject the chosen news item as a calendar entry. Else evergreen. + const news = pickFreshNews(cfg); + if (news) { + entry = { + date: today, + slug: news.slug, + workingTitle: news.workingTitle, + type: news.type || "news-roundup", + primaryKeyword: news.primaryKeyword, + secondaryKeywords: news.secondaryKeywords || [], + searchIntent: news.searchIntent || "informational", + audienceAngle: news.newsHook || "", + sourceHints: news.sourceLinks || [], + eeatAngle: "Timely, sourced news — cite every claim.", + news: true, + freshnessScore: news.freshnessScore ?? 3, + status: "planned", + }; + await addEntry(calendarPath, entry); + markNewsUsed(news.slug); + console.log(`[write-daily] news-first: "${entry.workingTitle}" (freshness ${entry.freshnessScore})`); + } else { + const cal = readCalendar(calendarPath); + const planned = cal.filter((e) => e.status === "planned"); + const due = planned.filter((e) => e.date <= today).sort((a, b) => a.date.localeCompare(b.date)); + entry = due[0] || planned.sort((a, b) => a.date.localeCompare(b.date))[0]; + } + } +} + +if (!entry) { + console.log("[write-daily] Nothing left to write — no fresh news and no planned topics."); + process.exit(0); +} + +const draftDir = join(root, cfg.paths.draftsDir, entry.slug); +mkdirSync(draftDir, { recursive: true }); + +const [wMin, wMax] = cfg.editorial.wordCounts[entry.type] || [800, 1200]; +const blogDir = join(root, cfg.paths.blogContentDir); +const logFile = join(root, cfg.paths.logsDir, `write-${entry.slug}.log`); + +const prompt = `Write today's Comiida article from this calendar entry: + +${JSON.stringify({ ...entry, draft: undefined, instructions: undefined }, null, 2)} + +- Today's date: ${today} +- Author slug to byline (exact): ${cfg.author.slug} +- Word count range for type "${entry.type}": ${wMin}–${wMax} words +- Draft output directory (write index.mdx, cover.jpg, sources.json here): ${draftDir} +- Existing published posts to read for internal links: ${blogDir} +- Image: use the Higgsfield MCP (preferred model "${cfg.image.preferredModel}", aspect ratio + ~${cfg.image.aspectRatio}). Read content-pipeline/prompts/image.md and follow that flow + (recommend model → generate_image → job_status sync → curl the result URL). + Save the final image to: ${join(draftDir, "cover.jpg")}. +${ + entry.authorFirsthand + ? `\nAUTHOR FIRST-HAND PIECE: this is ${cfg.author.name}'s own draft, written from genuine first-hand experience. Follow the "Author first-hand pieces" EXCEPTION in your system prompt — keep the FIRST-PERSON voice and the author's honest opinions (do not neutralize them), and only fact-check/cite objective specifics (addresses, opening dates, prices, hours). SCOPE: the article is about the venues and points the author actually names — cover those; do NOT pad the piece with generic restaurants the author didn't mention just to hit a word count. Aim for the LOWER end of the word-count range; a tight, genuine ~800–1000 words beats bloated filler.\n` + : "" +}${ + entry.instructions + ? `\nEDITOR INSTRUCTIONS (from the person who suggested this — follow them carefully):\n${entry.instructions}\n` + : "" +}${ + entry.draft + ? `\nEDITOR-PROVIDED DRAFT — use this as the basis for the article. Keep its intent and key points, but fact-check every claim, add real citations, improve structure/SEO, and expand it to meet the contract:\n"""\n${entry.draft}\n"""\n` + : "" +} +Follow the EEAT/SEO contract in your system prompt exactly.`; + +await updateEntry(calendarPath, entry.slug, { status: "drafting" }); +console.log(`[write-daily] drafting "${entry.workingTitle}" (${entry.type}) → ${draftDir}`); + +const res = await runClaude({ + prompt, + systemPromptFile: join(PIPELINE, "prompts/writer.system.md"), + model: cfg.models.writer, + // Higgsfield is configured globally via claude.ai (reachable headless) — no local --mcp-config. + allowedTools: "Read Write Edit Glob Grep WebSearch Bash mcp__claude_ai_Higgsfield", + addDirs: [root], + cwd: root, + logFile, +}); + +console.log(`[write-daily] agent: ${res.result || "(no text)"}`); + +const hasMdx = existsSync(join(draftDir, "index.mdx")); +const hasCover = existsSync(join(draftDir, "cover.jpg")); +const finalStatus = hasMdx ? (hasCover ? "drafted" : "drafted-no-image") : "draft-failed"; +await updateEntry(calendarPath, entry.slug, { status: finalStatus }); + +console.log( + `[write-daily] status=${finalStatus} mdx=${hasMdx} cover=${hasCover}\n` + + `[write-daily] review: ${draftDir} | log: ${logFile}` +); +if (!hasMdx) process.exit(1); + +// SEO Specialist pass + auto-revise loop, so the approve/publish gate has a verdict. +try { + let audit = await runSeoReview(entry.slug, { published: false }); + const minScore = cfg.seo?.minScore ?? 85; + const fails = (a) => !a || a.verdict === "fail" || (a.overall ?? 0) < minScore; + + if (audit && cfg.seo?.autoRevise && fails(audit)) { + console.log(`[write-daily] SEO ${audit.verdict} ${audit.overall} < ${minScore} — auto-revising…`); + const { audit: revised, attempts } = await runRevise(entry.slug, {}); + if (revised) audit = revised; + console.log(`[write-daily] auto-revise done after ${attempts} attempt(s): ${audit?.verdict} ${audit?.overall}`); + } + + if (audit) { + await updateEntry(calendarPath, entry.slug, { seo: { verdict: audit.verdict, overall: audit.overall } }); + console.log( + `[write-daily] SEO: ${audit.verdict} · ${audit.overall} · ${(audit.blocking || []).length} blocking` + ); + } else { + console.log("[write-daily] SEO: audit could not be generated (continuing)."); + } +} catch (e) { + console.log(`[write-daily] SEO review error (continuing): ${e.message}`); +} diff --git a/content-pipeline/state/.gitkeep b/content-pipeline/state/.gitkeep new file mode 100644 index 0000000..e69de29