diff --git a/.devcontainer/cli/Dockerfile b/.devcontainer/cli/Dockerfile index 0317097..ab13330 100644 --- a/.devcontainer/cli/Dockerfile +++ b/.devcontainer/cli/Dockerfile @@ -9,7 +9,8 @@ RUN apk add --no-cache composer # Add Chromium and Image Magick for puppeteer. RUN apk add --no-cache \ imagemagick-dev \ - chromium + chromium \ + libzip-dev ENV PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium ENV PUPPETEER_DOCKER=1 @@ -19,7 +20,7 @@ RUN chmod 777 /usr/src/php/ext/imagick RUN curl -fsSL https://github.com/Imagick/imagick/archive/refs/tags/3.8.0.tar.gz | tar xvz -C "/usr/src/php/ext/imagick" --strip 1 # Install PHP extensions -RUN docker-php-ext-install imagick +RUN docker-php-ext-install imagick zip # Composer uses its php binary, but we want it to use the container's one RUN rm -f /usr/bin/php84 diff --git a/.devcontainer/fpm/Dockerfile b/.devcontainer/fpm/Dockerfile index 8c585c8..3e658b6 100644 --- a/.devcontainer/fpm/Dockerfile +++ b/.devcontainer/fpm/Dockerfile @@ -14,7 +14,8 @@ RUN apk add --no-cache \ nodejs \ npm \ imagemagick-dev \ - chromium + chromium \ + libzip-dev ENV PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium ENV PUPPETEER_DOCKER=1 @@ -24,7 +25,7 @@ RUN chmod 777 /usr/src/php/ext/imagick RUN curl -fsSL https://github.com/Imagick/imagick/archive/refs/tags/3.8.0.tar.gz | tar xvz -C "/usr/src/php/ext/imagick" --strip 1 # Install PHP extensions -RUN docker-php-ext-install imagick +RUN docker-php-ext-install imagick zip RUN rm -f /usr/bin/php84 RUN ln -s /usr/local/bin/php /usr/bin/php84 diff --git a/.gitignore b/.gitignore index 02f3d78..1f4f617 100644 --- a/.gitignore +++ b/.gitignore @@ -29,3 +29,8 @@ yarn-error.log /.junie/guidelines.md /CLAUDE.md /.mcp.json +/.ai +.DS_Store +/boost.json +/.gemini +/GEMINI.md diff --git a/Dockerfile b/Dockerfile index 57a919f..2d761ed 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,7 @@ ######################## # Base Image ######################## -FROM bnussbau/serversideup-php:8.4-fpm-nginx-alpine-imagick-chromium AS base +FROM bnussbau/serversideup-php:8.4-fpm-nginx-alpine-imagick-chromium@sha256:52ac545fdb57b2ab7568b1c7fc0a98cb1a69a275d8884249778a80914272fa48 AS base LABEL org.opencontainers.image.source=https://github.com/usetrmnl/byos_laravel LABEL org.opencontainers.image.description="TRMNL BYOS Laravel" @@ -12,9 +12,14 @@ ENV APP_VERSION=${APP_VERSION} ENV AUTORUN_ENABLED="true" +# Mark trmnl-liquid-cli as installed +ENV TRMNL_LIQUID_ENABLED=1 + # Switch to the root user so we can do root things USER root +COPY --chown=www-data:www-data --from=bnussbau/trmnl-liquid-cli:0.1.0 /usr/local/bin/trmnl-liquid-cli /usr/local/bin/ + # Set the working directory WORKDIR /var/www/html @@ -48,6 +53,5 @@ FROM base AS production # Copy the assets from the assets image COPY --chown=www-data:www-data --from=assets /app/public/build /var/www/html/public/build COPY --chown=www-data:www-data --from=assets /app/node_modules /var/www/html/node_modules - # Drop back to the www-data user USER www-data diff --git a/README.md b/README.md index f3b5fd5..acb0b5c 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ [![tests](https://github.com/usetrmnl/byos_laravel/actions/workflows/test.yml/badge.svg)](https://github.com/usetrmnl/byos_laravel/actions/workflows/test.yml) TRMNL BYOS Laravel is a self-hostable implementation of a TRMNL server, built with Laravel. -It allows you to manage TRMNL devices, generate screens using native plugins, recipes (100+ from the [community catalog](https://bnussbau.github.io/trmnl-recipe-catalog/)), or the API, and can also act as a proxy for the native cloud service (Core). With over 20k downloads and 100+ stars, itโ€™s the most popular community-driven BYOS. +It allows you to manage TRMNL devices, generate screens using **native plugins** (Screens API, Markup), **recipes** (120+ from the [OSS community catalog](https://bnussbau.github.io/trmnl-recipe-catalog/), 600+ from the [TRMNL catalog](https://usetrmnl.com/recipes), or your own), or the **API**, and can also act as a **proxy** for the native cloud service (Core). With over 40k downloads and 160+ stars, itโ€™s the most popular community-driven BYOS. ![Screenshot](README_byos-screenshot.png) ![Screenshot](README_byos-screenshot-dark.png) @@ -16,7 +16,8 @@ It allows you to manage TRMNL devices, generate screens using native plugins, re * ๐Ÿ” Auto-Join โ€“ Automatically detects and adds devices from your local network. * ๐Ÿ–ฅ๏ธ Screen Generation โ€“ Supports Plugins (including Mashups), Recipes, API, Markup, or updates via Code. * Support for TRMNL [Design Framework](https://usetrmnl.com/framework) - * Over 45 compatible open-source recipes are available in the [community catalog](https://bnussbau.github.io/trmnl-recipe-catalog/) + * Compatible open-source recipes are available in the [community catalog](https://bnussbau.github.io/trmnl-recipe-catalog/) + * Import from the [TRMNL community recipe catalog](https://usetrmnl.com/recipes) * Supported Devices * TRMNL OG (1-bit & 2-bit) * SeeedStudio TRMNL 7,5" (OG) DIY Kit @@ -24,6 +25,7 @@ It allows you to manage TRMNL devices, generate screens using native plugins, re * reTerminal E1001 Monochrome ePaper Display * Custom ESP32 with TRMNL firmware * E-Reader Devices + * KOReader ([trmnl-koreader](https://github.com/usetrmnl/trmnl-koreader)) * Kindle ([trmnl-kindle](https://github.com/usetrmnl/byos_laravel/pull/27)) * Nook ([trmnl-nook](https://github.com/usetrmnl/trmnl-nook)) * Kobo ([trmnl-kobo](https://github.com/usetrmnl/trmnl-kobo)) diff --git a/app/Console/Commands/GenerateDefaultImagesCommand.php b/app/Console/Commands/GenerateDefaultImagesCommand.php index c326dd6..e2887df 100644 --- a/app/Console/Commands/GenerateDefaultImagesCommand.php +++ b/app/Console/Commands/GenerateDefaultImagesCommand.php @@ -121,6 +121,10 @@ class GenerateDefaultImagesCommand extends Command $browserStage = new BrowserStage($browsershotInstance); $browserStage->html($html); + + // Set timezone from app config (no user context in this command) + $browserStage->timezone(config('app.timezone')); + $browserStage ->width($deviceModel->width) ->height($deviceModel->height); diff --git a/app/Jobs/FetchDeviceModelsJob.php b/app/Jobs/FetchDeviceModelsJob.php index 695041f..475c5c7 100644 --- a/app/Jobs/FetchDeviceModelsJob.php +++ b/app/Jobs/FetchDeviceModelsJob.php @@ -5,6 +5,7 @@ declare(strict_types=1); namespace App\Jobs; use App\Models\DeviceModel; +use App\Models\DevicePalette; use Exception; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; @@ -20,6 +21,8 @@ final class FetchDeviceModelsJob implements ShouldQueue private const API_URL = 'https://usetrmnl.com/api/models'; + private const PALETTES_API_URL = 'http://usetrmnl.com/api/palettes'; + /** * Create a new job instance. */ @@ -34,6 +37,8 @@ final class FetchDeviceModelsJob implements ShouldQueue public function handle(): void { try { + $this->processPalettes(); + $response = Http::timeout(30)->get(self::API_URL); if (! $response->successful()) { @@ -69,6 +74,86 @@ final class FetchDeviceModelsJob implements ShouldQueue } } + /** + * Process palettes from API and update/create records. + */ + private function processPalettes(): void + { + try { + $response = Http::timeout(30)->get(self::PALETTES_API_URL); + + if (! $response->successful()) { + Log::error('Failed to fetch palettes from API', [ + 'status' => $response->status(), + 'body' => $response->body(), + ]); + + return; + } + + $data = $response->json('data', []); + + if (! is_array($data)) { + Log::error('Invalid response format from palettes API', [ + 'response' => $response->json(), + ]); + + return; + } + + foreach ($data as $paletteData) { + try { + $this->updateOrCreatePalette($paletteData); + } catch (Exception $e) { + Log::error('Failed to process palette', [ + 'palette_data' => $paletteData, + 'error' => $e->getMessage(), + ]); + } + } + + Log::info('Successfully fetched and updated palettes', [ + 'count' => count($data), + ]); + + } catch (Exception $e) { + Log::error('Exception occurred while fetching palettes', [ + 'message' => $e->getMessage(), + 'trace' => $e->getTraceAsString(), + ]); + } + } + + /** + * Update or create a palette record. + */ + private function updateOrCreatePalette(array $paletteData): void + { + $name = $paletteData['id'] ?? null; + + if (! $name) { + Log::warning('Palette data missing id field', [ + 'palette_data' => $paletteData, + ]); + + return; + } + + $attributes = [ + 'name' => $name, + 'description' => $paletteData['name'] ?? '', + 'grays' => $paletteData['grays'] ?? 2, + 'colors' => $paletteData['colors'] ?? null, + 'framework_class' => $paletteData['framework_class'] ?? '', + 'source' => 'api', + ]; + + DevicePalette::updateOrCreate( + ['name' => $name], + $attributes + ); + } + /** * Process the device models data and update/create records. */ @@ -114,12 +199,49 @@ final class FetchDeviceModelsJob implements ShouldQueue 'offset_x' => $modelData['offset_x'] ?? 0, 'offset_y' => $modelData['offset_y'] ?? 0, 'published_at' => $modelData['published_at'] ?? null, + 'kind' => $modelData['kind'] ?? null, 'source' => 'api', ]; + // Set palette_id to the first palette from the model's palettes array + $firstPaletteId = $this->getFirstPaletteId($modelData); + if ($firstPaletteId) { + $attributes['palette_id'] = $firstPaletteId; + } + DeviceModel::updateOrCreate( ['name' => $name], $attributes ); } + + /** + * Get the first palette ID from model data. + */ + private function getFirstPaletteId(array $modelData): ?int + { + $paletteName = null; + + // Check for palette_ids array + if (isset($modelData['palette_ids']) && is_array($modelData['palette_ids']) && $modelData['palette_ids'] !== []) { + $paletteName = $modelData['palette_ids'][0]; + } + + // Check for palettes array (array of objects with id) + if (! $paletteName && isset($modelData['palettes']) && is_array($modelData['palettes']) && $modelData['palettes'] !== []) { + $firstPalette = $modelData['palettes'][0]; + if (is_array($firstPalette) && isset($firstPalette['id'])) { + $paletteName = $firstPalette['id']; + } + } + + if (! $paletteName) { + return null; + } + + // Look up palette by name to get the integer ID + $palette = DevicePalette::where('name', $paletteName)->first(); + + return $palette?->id; + } } diff --git a/app/Liquid/Filters/Data.php b/app/Liquid/Filters/Data.php index dd81ad8..2387ac5 100644 --- a/app/Liquid/Filters/Data.php +++ b/app/Liquid/Filters/Data.php @@ -131,6 +131,6 @@ class Data extends FiltersProvider */ public function map_to_i(array $input): array { - return array_map('intval', $input); + return array_map(intval(...), $input); } } diff --git a/app/Models/Device.php b/app/Models/Device.php index 6a99fcd..3583f48 100644 --- a/app/Models/Device.php +++ b/app/Models/Device.php @@ -12,6 +12,7 @@ use Illuminate\Support\Facades\Storage; /** * @property-read DeviceModel|null $deviceModel + * @property-read DevicePalette|null $palette */ class Device extends Model { @@ -19,6 +20,14 @@ class Device extends Model protected $guarded = ['id']; + /** + * Set the MAC address attribute, normalizing to uppercase. + */ + public function setMacAddressAttribute(?string $value): void + { + $this->attributes['mac_address'] = $value ? mb_strtoupper($value) : null; + } + protected $casts = [ 'battery_notification_sent' => 'boolean', 'proxy_cloud' => 'boolean', @@ -187,6 +196,11 @@ class Device extends Model return $this->belongsTo(DeviceModel::class); } + public function palette(): BelongsTo + { + return $this->belongsTo(DevicePalette::class, 'palette_id'); + } + /** * Get the color depth string (e.g., "4bit") for the associated device model. */ diff --git a/app/Models/DeviceModel.php b/app/Models/DeviceModel.php index 4dfaf1e..6132a76 100644 --- a/app/Models/DeviceModel.php +++ b/app/Models/DeviceModel.php @@ -6,7 +6,11 @@ namespace App\Models; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Relations\BelongsTo; +/** + * @property-read DevicePalette|null $palette + */ final class DeviceModel extends Model { use HasFactory; @@ -35,7 +39,7 @@ final class DeviceModel extends Model return '2bit'; } - // if higher then 4 return 4bit + // if higher than 4 return 4bit if ($this->bit_depth > 4) { return '4bit'; } @@ -66,4 +70,9 @@ final class DeviceModel extends Model return null; } + + public function palette(): BelongsTo + { + return $this->belongsTo(DevicePalette::class, 'palette_id'); + } } diff --git a/app/Models/DevicePalette.php b/app/Models/DevicePalette.php new file mode 100644 index 0000000..54b0876 --- /dev/null +++ b/app/Models/DevicePalette.php @@ -0,0 +1,23 @@ + 'integer', + 'colors' => 'array', + ]; +} diff --git a/app/Models/Playlist.php b/app/Models/Playlist.php index 7b55a73..b4daf5e 100644 --- a/app/Models/Playlist.php +++ b/app/Models/Playlist.php @@ -37,21 +37,32 @@ class Playlist extends Model return false; } - // Check weekday - if ($this->weekdays !== null && ! in_array(now()->dayOfWeek, $this->weekdays)) { + // Get user's timezone or fall back to app timezone + $timezone = $this->device->user->timezone ?? config('app.timezone'); + $now = now($timezone); + + // Check weekday (using timezone-aware time) + if ($this->weekdays !== null && ! in_array($now->dayOfWeek, $this->weekdays)) { return false; } if ($this->active_from !== null && $this->active_until !== null) { - $now = now(); + // Create timezone-aware datetime objects for active_from and active_until + $activeFrom = $now->copy() + ->setTimeFrom($this->active_from) + ->timezone($timezone); + + $activeUntil = $now->copy() + ->setTimeFrom($this->active_until) + ->timezone($timezone); // Handle time ranges that span across midnight - if ($this->active_from > $this->active_until) { + if ($activeFrom > $activeUntil) { // Time range spans midnight (e.g., 09:01 to 03:58) - if ($now >= $this->active_from || $now <= $this->active_until) { + if ($now >= $activeFrom || $now <= $activeUntil) { return true; } - } elseif ($now >= $this->active_from && $now <= $this->active_until) { + } elseif ($now >= $activeFrom && $now <= $activeUntil) { return true; } diff --git a/app/Models/Plugin.php b/app/Models/Plugin.php index 33a29d5..68f8e7e 100644 --- a/app/Models/Plugin.php +++ b/app/Models/Plugin.php @@ -11,6 +11,9 @@ use App\Liquid\Filters\StandardFilters; use App\Liquid\Filters\StringMarkup; use App\Liquid\Filters\Uniqueness; use App\Liquid\Tags\TemplateTag; +use App\Services\Plugin\Parsers\ResponseParserRegistry; +use App\Services\PluginImportService; +use Carbon\Carbon; use Exception; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; @@ -19,11 +22,12 @@ use Illuminate\Support\Facades\App; use Illuminate\Support\Facades\Blade; use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Log; +use Illuminate\Support\Facades\Process; use Illuminate\Support\Str; +use InvalidArgumentException; use Keepsuit\LaravelLiquid\LaravelLiquidExtension; use Keepsuit\Liquid\Exceptions\LiquidException; use Keepsuit\Liquid\Extensions\StandardExtension; -use SimpleXMLElement; class Plugin extends Model { @@ -40,6 +44,9 @@ class Plugin extends Model 'configuration_template' => 'json', 'no_bleed' => 'boolean', 'dark_mode' => 'boolean', + 'preferred_renderer' => 'string', + 'plugin_type' => 'string', + 'alias' => 'boolean', ]; protected static function boot() @@ -51,6 +58,18 @@ class Plugin extends Model $model->uuid = Str::uuid(); } }); + + static::updating(function ($model): void { + // Reset image cache when markup changes + if ($model->isDirty('render_markup')) { + $model->current_image = null; + } + }); + + // Sanitize configuration template on save + static::saving(function ($model): void { + $model->sanitizeTemplate(); + }); } public function user() @@ -58,6 +77,25 @@ class Plugin extends Model return $this->belongsTo(User::class); } + // sanitize configuration template descriptions and help texts (since they allow HTML rendering) + protected function sanitizeTemplate(): void + { + $template = $this->configuration_template; + + if (isset($template['custom_fields']) && is_array($template['custom_fields'])) { + foreach ($template['custom_fields'] as &$field) { + if (isset($field['description'])) { + $field['description'] = \Stevebauman\Purify\Facades\Purify::clean($field['description']); + } + if (isset($field['help_text'])) { + $field['help_text'] = \Stevebauman\Purify\Facades\Purify::clean($field['help_text']); + } + } + + $this->configuration_template = $template; + } + } + public function hasMissingRequiredConfigurationFields(): bool { if (! isset($this->configuration_template['custom_fields']) || empty($this->configuration_template['custom_fields'])) { @@ -98,6 +136,11 @@ class Plugin extends Model public function isDataStale(): bool { + // Image webhook plugins don't use data staleness - images are pushed directly + if ($this->plugin_type === 'image_webhook') { + return false; + } + if ($this->data_strategy === 'webhook') { // Treat as stale if any webhook event has occurred in the past hour return $this->data_payload_updated_at && $this->data_payload_updated_at->gt(now()->subHour()); @@ -111,160 +154,88 @@ class Plugin extends Model public function updateDataPayload(): void { - if ($this->data_strategy === 'polling' && $this->polling_url) { - - $headers = ['User-Agent' => 'usetrmnl/byos_laravel', 'Accept' => 'application/json']; - - if ($this->polling_header) { - // Resolve Liquid variables in the polling header - $resolvedHeader = $this->resolveLiquidVariables($this->polling_header); - $headerLines = explode("\n", mb_trim($resolvedHeader)); - foreach ($headerLines as $line) { - $parts = explode(':', $line, 2); - if (count($parts) === 2) { - $headers[mb_trim($parts[0])] = mb_trim($parts[1]); - } - } - } - - // Split URLs by newline and filter out empty lines - $urls = array_filter( - array_map('trim', explode("\n", $this->polling_url)), - fn ($url): bool => ! empty($url) - ); - - // If only one URL, use the original logic without nesting - if (count($urls) === 1) { - $url = reset($urls); - $httpRequest = Http::withHeaders($headers); - - if ($this->polling_verb === 'post' && $this->polling_body) { - // Resolve Liquid variables in the polling body - $resolvedBody = $this->resolveLiquidVariables($this->polling_body); - $httpRequest = $httpRequest->withBody($resolvedBody); - } - - // Resolve Liquid variables in the polling URL - $resolvedUrl = $this->resolveLiquidVariables($url); - - try { - // Make the request based on the verb - $httpResponse = $this->polling_verb === 'post' ? $httpRequest->post($resolvedUrl) : $httpRequest->get($resolvedUrl); - - $response = $this->parseResponse($httpResponse); - - $this->update([ - 'data_payload' => $response, - 'data_payload_updated_at' => now(), - ]); - } catch (Exception $e) { - Log::warning("Failed to fetch data from URL {$resolvedUrl}: ".$e->getMessage()); - $this->update([ - 'data_payload' => ['error' => 'Failed to fetch data'], - 'data_payload_updated_at' => now(), - ]); - } - - return; - } - - // Multiple URLs - use nested response logic - $combinedResponse = []; - - foreach ($urls as $index => $url) { - $httpRequest = Http::withHeaders($headers); - - if ($this->polling_verb === 'post' && $this->polling_body) { - // Resolve Liquid variables in the polling body - $resolvedBody = $this->resolveLiquidVariables($this->polling_body); - $httpRequest = $httpRequest->withBody($resolvedBody); - } - - // Resolve Liquid variables in the polling URL - $resolvedUrl = $this->resolveLiquidVariables($url); - - try { - // Make the request based on the verb - $httpResponse = $this->polling_verb === 'post' ? $httpRequest->post($resolvedUrl) : $httpRequest->get($resolvedUrl); - - $response = $this->parseResponse($httpResponse); - - // Check if response is an array at root level - if (array_keys($response) === range(0, count($response) - 1)) { - // Response is a sequential array, nest under .data - $combinedResponse["IDX_{$index}"] = ['data' => $response]; - } else { - // Response is an object or associative array, keep as is - $combinedResponse["IDX_{$index}"] = $response; - } - } catch (Exception $e) { - // Log error and continue with other URLs - Log::warning("Failed to fetch data from URL {$resolvedUrl}: ".$e->getMessage()); - $combinedResponse["IDX_{$index}"] = ['error' => 'Failed to fetch data']; - } - } - - $this->update([ - 'data_payload' => $combinedResponse, - 'data_payload_updated_at' => now(), - ]); + if ($this->data_strategy !== 'polling' || ! $this->polling_url) { + return; } + $headers = ['User-Agent' => 'usetrmnl/byos_laravel', 'Accept' => 'application/json']; + + // resolve headers + if ($this->polling_header) { + $resolvedHeader = $this->resolveLiquidVariables($this->polling_header); + $headerLines = explode("\n", mb_trim($resolvedHeader)); + foreach ($headerLines as $line) { + $parts = explode(':', $line, 2); + if (count($parts) === 2) { + $headers[mb_trim($parts[0])] = mb_trim($parts[1]); + } + } + } + + // resolve and clean URLs + $resolvedPollingUrls = $this->resolveLiquidVariables($this->polling_url); + $urls = array_values(array_filter( // array_values ensures 0, 1, 2... + array_map('trim', explode("\n", $resolvedPollingUrls)), + fn ($url): bool => filled($url) + )); + + $combinedResponse = []; + + // Loop through all URLs (Handles 1 or many) + foreach ($urls as $index => $url) { + $httpRequest = Http::withHeaders($headers); + + if ($this->polling_verb === 'post' && $this->polling_body) { + $resolvedBody = $this->resolveLiquidVariables($this->polling_body); + $httpRequest = $httpRequest->withBody($resolvedBody); + } + + try { + $httpResponse = ($this->polling_verb === 'post') + ? $httpRequest->post($url) + : $httpRequest->get($url); + + $response = $this->parseResponse($httpResponse); + + // Nest if it's a sequential array + if (array_keys($response) === range(0, count($response) - 1)) { + $combinedResponse["IDX_{$index}"] = ['data' => $response]; + } else { + $combinedResponse["IDX_{$index}"] = $response; + } + } catch (Exception $e) { + Log::warning("Failed to fetch data from URL {$url}: ".$e->getMessage()); + $combinedResponse["IDX_{$index}"] = ['error' => 'Failed to fetch data']; + } + } + + // unwrap IDX_0 if only one URL + $finalPayload = (count($urls) === 1) ? reset($combinedResponse) : $combinedResponse; + + $this->update([ + 'data_payload' => $finalPayload, + 'data_payload_updated_at' => now(), + ]); } - /** - * Parse HTTP response, handling both JSON and XML content types - */ private function parseResponse(Response $httpResponse): array { - if ($httpResponse->header('Content-Type') && str_contains($httpResponse->header('Content-Type'), 'xml')) { + $parsers = app(ResponseParserRegistry::class)->getParsers(); + + foreach ($parsers as $parser) { + $parserName = class_basename($parser); + try { - // Convert XML to array and wrap under 'rss' key - $xml = simplexml_load_string($httpResponse->body()); - if ($xml === false) { - throw new Exception('Invalid XML content'); + $result = $parser->parse($httpResponse); + + if ($result !== null) { + return $result; } - - // Convert SimpleXML directly to array - $xmlArray = $this->xmlToArray($xml); - - return ['rss' => $xmlArray]; } catch (Exception $e) { - Log::warning('Failed to parse XML response: '.$e->getMessage()); - - return ['error' => 'Failed to parse XML response']; + Log::warning("Failed to parse {$parserName} response: ".$e->getMessage()); } } - try { - // Attempt to parse it into JSON - $json = $httpResponse->json(); - if($json !== null) { - return $json; - } - - // Response doesn't seem to be JSON, wrap the response body text as a JSON object - return ['data' => $httpResponse->body()]; - } catch (Exception $e) { - Log::warning('Failed to parse JSON response: '.$e->getMessage()); - - return ['error' => 'Failed to parse JSON response']; - } - } - - /** - * Convert SimpleXML object to array recursively - */ - private function xmlToArray(SimpleXMLElement $xml): array - { - $array = (array) $xml; - - foreach ($array as $key => $value) { - if ($value instanceof SimpleXMLElement) { - $array[$key] = $this->xmlToArray($value); - } - } - - return $array; + return ['error' => 'Failed to parse response']; } /** @@ -341,19 +312,48 @@ class Plugin extends Model return $template; } + /** + * Check if a template contains a Liquid for loop pattern + * + * @param string $template The template string to check + * @return bool True if the template contains a for loop pattern + */ + private function containsLiquidForLoop(string $template): bool + { + return preg_match('/{%-?\s*for\s+/i', $template) === 1; + } + /** * Resolve Liquid variables in a template string using the Liquid template engine * + * Uses the external trmnl-liquid renderer when: + * - preferred_renderer is 'trmnl-liquid' + * - External renderer is enabled in config + * - Template contains a Liquid for loop pattern + * + * Otherwise uses the internal PHP-based Liquid renderer. + * * @param string $template The template string containing Liquid variables * @return string The resolved template with variables replaced with their values * * @throws LiquidException + * @throws Exception */ public function resolveLiquidVariables(string $template): string { // Get configuration variables - make them available at root level $variables = $this->configuration ?? []; + // Check if external renderer should be used + $useExternalRenderer = $this->preferred_renderer === 'trmnl-liquid' + && config('services.trmnl.liquid_enabled') + && $this->containsLiquidForLoop($template); + + if ($useExternalRenderer) { + // Use external Ruby liquid renderer + return $this->renderWithExternalLiquidRenderer($template, $variables); + } + // Use the Liquid template engine to resolve variables $environment = App::make('liquid.environment'); $environment->filterRegistry->register(StandardFilters::class); @@ -363,6 +363,53 @@ class Plugin extends Model return $liquidTemplate->render($context); } + /** + * Render template using external Ruby liquid renderer + * + * @param string $template The liquid template string + * @param array $context The render context data + * @return string The rendered HTML + * + * @throws Exception + */ + private function renderWithExternalLiquidRenderer(string $template, array $context): string + { + $liquidPath = config('services.trmnl.liquid_path'); + + if (empty($liquidPath)) { + throw new Exception('External liquid renderer path is not configured'); + } + + // HTML encode the template + $encodedTemplate = htmlspecialchars($template, ENT_QUOTES, 'UTF-8'); + + // Encode context as JSON + $jsonContext = json_encode($context, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); + + if ($jsonContext === false) { + throw new Exception('Failed to encode render context as JSON: '.json_last_error_msg()); + } + + // Validate argument sizes + app(PluginImportService::class)->validateExternalRendererArguments($encodedTemplate, $jsonContext, $liquidPath); + + // Execute the external renderer + $process = Process::run([ + $liquidPath, + '--template', + $encodedTemplate, + '--context', + $jsonContext, + ]); + + if (! $process->successful()) { + $errorOutput = $process->errorOutput() ?: $process->output(); + throw new Exception('External liquid renderer failed: '.$errorOutput); + } + + return $process->output(); + } + /** * Render the plugin's markup * @@ -370,63 +417,81 @@ class Plugin extends Model */ public function render(string $size = 'full', bool $standalone = true, ?Device $device = null): string { + if ($this->plugin_type !== 'recipe') { + throw new InvalidArgumentException('Render method is only applicable for recipe plugins.'); + } + if ($this->render_markup) { $renderedContent = ''; if ($this->markup_language === 'liquid') { - // Create a custom environment with inline templates support - $inlineFileSystem = new InlineTemplatesFileSystem(); - $environment = new \Keepsuit\Liquid\Environment( - fileSystem: $inlineFileSystem, - extensions: [new StandardExtension(), new LaravelLiquidExtension()] - ); + // Get timezone from user or fall back to app timezone + $timezone = $this->user->timezone ?? config('app.timezone'); - // Register all custom filters - $environment->filterRegistry->register(Data::class); - $environment->filterRegistry->register(Date::class); - $environment->filterRegistry->register(Localization::class); - $environment->filterRegistry->register(Numbers::class); - $environment->filterRegistry->register(StringMarkup::class); - $environment->filterRegistry->register(Uniqueness::class); + // Calculate UTC offset in seconds + $utcOffset = (string) Carbon::now($timezone)->getOffset(); - // Register the template tag for inline templates - $environment->tagRegistry->register(TemplateTag::class); - - // Apply Liquid replacements (including 'with' syntax conversion) - $processedMarkup = $this->applyLiquidReplacements($this->render_markup); - - $template = $environment->parseString($processedMarkup); - $context = $environment->newRenderContext( - data: [ - 'size' => $size, - 'data' => $this->data_payload, - 'config' => $this->configuration ?? [], - ...(is_array($this->data_payload) ? $this->data_payload : []), - 'trmnl' => [ - 'system' => [ - 'timestamp_utc' => now()->utc()->timestamp, - ], - 'user' => [ - 'utc_offset' => '0', - 'name' => $this->user->name ?? 'Unknown User', - 'locale' => 'en', - 'time_zone_iana' => config('app.timezone'), - ], - 'plugin_settings' => [ - 'instance_name' => $this->name, - 'strategy' => $this->data_strategy, - 'dark_mode' => $this->dark_mode ? 'yes' : 'no', - 'no_screen_padding' => $this->no_bleed ? 'yes' : 'no', - 'polling_headers' => $this->polling_header, - 'polling_url' => $this->polling_url, - 'custom_fields_values' => [ - ...(is_array($this->configuration) ? $this->configuration : []), - ], + // Build render context + $context = [ + 'size' => $size, + 'data' => $this->data_payload, + 'config' => $this->configuration ?? [], + ...(is_array($this->data_payload) ? $this->data_payload : []), + 'trmnl' => [ + 'system' => [ + 'timestamp_utc' => now()->utc()->timestamp, + ], + 'user' => [ + 'utc_offset' => $utcOffset, + 'name' => $this->user->name ?? 'Unknown User', + 'locale' => 'en', + 'time_zone_iana' => $timezone, + ], + 'plugin_settings' => [ + 'instance_name' => $this->name, + 'strategy' => $this->data_strategy, + 'dark_mode' => $this->dark_mode ? 'yes' : 'no', + 'no_screen_padding' => $this->no_bleed ? 'yes' : 'no', + 'polling_headers' => $this->polling_header, + 'polling_url' => $this->polling_url, + 'custom_fields_values' => [ + ...(is_array($this->configuration) ? $this->configuration : []), ], ], - ] - ); - $renderedContent = $template->render($context); + ], + ]; + + // Check if external renderer should be used + if ($this->preferred_renderer === 'trmnl-liquid' && config('services.trmnl.liquid_enabled')) { + // Use external Ruby renderer - pass raw template without preprocessing + $renderedContent = $this->renderWithExternalLiquidRenderer($this->render_markup, $context); + } else { + // Use PHP keepsuit/liquid renderer + // Create a custom environment with inline templates support + $inlineFileSystem = new InlineTemplatesFileSystem(); + $environment = new \Keepsuit\Liquid\Environment( + fileSystem: $inlineFileSystem, + extensions: [new StandardExtension(), new LaravelLiquidExtension()] + ); + + // Register all custom filters + $environment->filterRegistry->register(Data::class); + $environment->filterRegistry->register(Date::class); + $environment->filterRegistry->register(Localization::class); + $environment->filterRegistry->register(Numbers::class); + $environment->filterRegistry->register(StringMarkup::class); + $environment->filterRegistry->register(Uniqueness::class); + + // Register the template tag for inline templates + $environment->tagRegistry->register(TemplateTag::class); + + // Apply Liquid replacements (including 'with' syntax conversion) + $processedMarkup = $this->applyLiquidReplacements($this->render_markup); + + $template = $environment->parseString($processedMarkup); + $liquidContext = $environment->newRenderContext(data: $context); + $renderedContent = $template->render($liquidContext); + } } else { $renderedContent = Blade::render($this->render_markup, [ 'size' => $size, @@ -463,17 +528,30 @@ class Plugin extends Model if ($this->render_markup_view) { if ($standalone) { - return view('trmnl-layouts.single', [ + $renderedView = view($this->render_markup_view, [ + 'size' => $size, + 'data' => $this->data_payload, + 'config' => $this->configuration ?? [], + ])->render(); + + if ($size === 'full') { + return view('trmnl-layouts.single', [ + 'colorDepth' => $device?->colorDepth(), + 'deviceVariant' => $device?->deviceVariant() ?? 'og', + 'noBleed' => $this->no_bleed, + 'darkMode' => $this->dark_mode, + 'scaleLevel' => $device?->scaleLevel(), + 'slot' => $renderedView, + ])->render(); + } + + return view('trmnl-layouts.mashup', [ + 'mashupLayout' => $this->getPreviewMashupLayoutForSize($size), 'colorDepth' => $device?->colorDepth(), 'deviceVariant' => $device?->deviceVariant() ?? 'og', - 'noBleed' => $this->no_bleed, 'darkMode' => $this->dark_mode, 'scaleLevel' => $device?->scaleLevel(), - 'slot' => view($this->render_markup_view, [ - 'size' => $size, - 'data' => $this->data_payload, - 'config' => $this->configuration ?? [], - ])->render(), + 'slot' => $renderedView, ])->render(); } @@ -504,4 +582,61 @@ class Plugin extends Model default => '1Tx1B', }; } + + /** + * Duplicate the plugin, copying all attributes and handling render_markup_view + * + * @param int|null $userId Optional user ID for the duplicate. If not provided, uses the original plugin's user_id. + * @return Plugin The newly created duplicate plugin + */ + public function duplicate(?int $userId = null): self + { + // Get all attributes except id and uuid + // Use toArray() to get cast values (respects JSON casts) + $attributes = $this->toArray(); + unset($attributes['id'], $attributes['uuid']); + + // Handle render_markup_view - copy file content to render_markup + if ($this->render_markup_view) { + try { + $basePath = resource_path('views/'.str_replace('.', '/', $this->render_markup_view)); + $paths = [ + $basePath.'.blade.php', + $basePath.'.liquid', + ]; + + $fileContent = null; + $markupLanguage = null; + foreach ($paths as $path) { + if (file_exists($path)) { + $fileContent = file_get_contents($path); + // Determine markup language based on file extension + $markupLanguage = str_ends_with($path, '.liquid') ? 'liquid' : 'blade'; + break; + } + } + + if ($fileContent !== null) { + $attributes['render_markup'] = $fileContent; + $attributes['markup_language'] = $markupLanguage; + $attributes['render_markup_view'] = null; + } else { + // File doesn't exist, remove the view reference + $attributes['render_markup_view'] = null; + } + } catch (Exception $e) { + // If file reading fails, remove the view reference + $attributes['render_markup_view'] = null; + } + } + + // Append " (Copy)" to the name + $attributes['name'] = $this->name.' (Copy)'; + + // Set user_id - use provided userId or fall back to original plugin's user_id + $attributes['user_id'] = $userId ?? $this->user_id; + + // Create and return the new plugin + return self::create($attributes); + } } diff --git a/app/Models/User.php b/app/Models/User.php index a1c83ab..c6d39b8 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -27,6 +27,7 @@ class User extends Authenticatable // implements MustVerifyEmail 'assign_new_devices', 'assign_new_device_id', 'oidc_sub', + 'timezone', ]; /** diff --git a/app/Services/ImageGenerationService.php b/app/Services/ImageGenerationService.php index 76be3bb..405ea3f 100644 --- a/app/Services/ImageGenerationService.php +++ b/app/Services/ImageGenerationService.php @@ -25,12 +25,45 @@ class ImageGenerationService { public static function generateImage(string $markup, $deviceId): string { - $device = Device::with('deviceModel')->find($deviceId); + $device = Device::with(['deviceModel', 'palette', 'deviceModel.palette', 'user'])->find($deviceId); + $uuid = self::generateImageFromModel( + markup: $markup, + deviceModel: $device->deviceModel, + user: $device->user, + palette: $device->palette ?? $device->deviceModel?->palette, + device: $device + ); + + $device->update(['current_screen_image' => $uuid]); + Log::info("Device $device->id: updated with new image: $uuid"); + + return $uuid; + } + + /** + * Generate an image from markup using a DeviceModel + * + * @param string $markup The HTML markup to render + * @param DeviceModel|null $deviceModel The device model to use for image generation + * @param \App\Models\User|null $user Optional user for timezone settings + * @param \App\Models\DevicePalette|null $palette Optional palette, falls back to device model's palette + * @param Device|null $device Optional device for legacy devices without DeviceModel + * @return string The UUID of the generated image + */ + public static function generateImageFromModel( + string $markup, + ?DeviceModel $deviceModel = null, + ?\App\Models\User $user = null, + ?\App\Models\DevicePalette $palette = null, + ?Device $device = null + ): string { $uuid = Uuid::uuid4()->toString(); try { - // Get image generation settings from DeviceModel if available, otherwise use device settings - $imageSettings = self::getImageSettings($device); + // Get image generation settings from DeviceModel or Device (for legacy devices) + $imageSettings = $deviceModel + ? self::getImageSettingsFromModel($deviceModel) + : ($device ? self::getImageSettings($device) : self::getImageSettingsFromModel(null)); $fileExtension = $imageSettings['mime_type'] === 'image/bmp' ? 'bmp' : 'png'; $outputPath = Storage::disk('public')->path('/images/generated/'.$uuid.'.'.$fileExtension); @@ -44,6 +77,10 @@ class ImageGenerationService $browserStage = new BrowserStage($browsershotInstance); $browserStage->html($markup); + // Set timezone from user or fall back to app timezone + $timezone = $user?->timezone ?? config('app.timezone'); + $browserStage->timezone($timezone); + if (config('app.puppeteer_window_size_strategy') === 'v2') { $browserStage ->width($imageSettings['width']) @@ -61,6 +98,14 @@ class ImageGenerationService $browserStage->setBrowsershotOption('args', ['--no-sandbox', '--disable-setuid-sandbox', '--disable-gpu']); } + // Get palette from parameter or fallback to device model's default palette + $colorPalette = null; + if ($palette && $palette->colors) { + $colorPalette = $palette->colors; + } elseif ($deviceModel?->palette && $deviceModel->palette->colors) { + $colorPalette = $deviceModel->palette->colors; + } + $imageStage = new ImageStage(); $imageStage->format($fileExtension) ->width($imageSettings['width']) @@ -72,6 +117,11 @@ class ImageGenerationService ->offsetY($imageSettings['offset_y']) ->outputPath($outputPath); + // Apply color palette if available + if ($colorPalette) { + $imageStage->colormap($colorPalette); + } + // Apply dithering if requested by markup $shouldDither = self::markupContainsDitherImage($markup); if ($shouldDither) { @@ -90,8 +140,7 @@ class ImageGenerationService throw new RuntimeException('Image file is empty: '.$outputPath); } - $device->update(['current_screen_image' => $uuid]); - Log::info("Device $device->id: updated with new image: $uuid"); + Log::info("Generated image: $uuid"); return $uuid; @@ -108,22 +157,7 @@ class ImageGenerationService { // If device has a DeviceModel, use its settings if ($device->deviceModel) { - /** @var DeviceModel $model */ - $model = $device->deviceModel; - - return [ - 'width' => $model->width, - 'height' => $model->height, - 'colors' => $model->colors, - 'bit_depth' => $model->bit_depth, - 'scale_factor' => $model->scale_factor, - 'rotation' => $model->rotation, - 'mime_type' => $model->mime_type, - 'offset_x' => $model->offset_x, - 'offset_y' => $model->offset_y, - 'image_format' => self::determineImageFormatFromModel($model), - 'use_model_settings' => true, - ]; + return self::getImageSettingsFromModel($device->deviceModel); } // Fallback to device settings @@ -147,6 +181,43 @@ class ImageGenerationService ]; } + /** + * Get image generation settings from a DeviceModel + */ + private static function getImageSettingsFromModel(?DeviceModel $deviceModel): array + { + if ($deviceModel) { + return [ + 'width' => $deviceModel->width, + 'height' => $deviceModel->height, + 'colors' => $deviceModel->colors, + 'bit_depth' => $deviceModel->bit_depth, + 'scale_factor' => $deviceModel->scale_factor, + 'rotation' => $deviceModel->rotation, + 'mime_type' => $deviceModel->mime_type, + 'offset_x' => $deviceModel->offset_x, + 'offset_y' => $deviceModel->offset_y, + 'image_format' => self::determineImageFormatFromModel($deviceModel), + 'use_model_settings' => true, + ]; + } + + // Default settings if no device model provided + return [ + 'width' => 800, + 'height' => 480, + 'colors' => 2, + 'bit_depth' => 1, + 'scale_factor' => 1.0, + 'rotation' => 0, + 'mime_type' => 'image/png', + 'offset_x' => 0, + 'offset_y' => 0, + 'image_format' => ImageFormat::AUTO->value, + 'use_model_settings' => false, + ]; + } + /** * Determine the appropriate ImageFormat based on DeviceModel settings */ @@ -263,6 +334,10 @@ class ImageGenerationService public static function resetIfNotCacheable(?Plugin $plugin): void { if ($plugin?->id) { + // Image webhook plugins have finalized images that shouldn't be reset + if ($plugin->plugin_type === 'image_webhook') { + return; + } // Check if any devices have custom dimensions or use non-standard DeviceModels $hasCustomDimensions = Device::query() ->where(function ($query): void { @@ -294,7 +369,7 @@ class ImageGenerationService public static function getDeviceSpecificDefaultImage(Device $device, string $imageType): ?string { // Validate image type - if (! in_array($imageType, ['setup-logo', 'sleep'])) { + if (! in_array($imageType, ['setup-logo', 'sleep', 'error'])) { return null; } @@ -328,16 +403,19 @@ class ImageGenerationService /** * Generate a default screen image from Blade template */ - public static function generateDefaultScreenImage(Device $device, string $imageType): string + public static function generateDefaultScreenImage(Device $device, string $imageType, ?string $pluginName = null): string { // Validate image type - if (! in_array($imageType, ['setup-logo', 'sleep'])) { + if (! in_array($imageType, ['setup-logo', 'sleep', 'error'])) { throw new InvalidArgumentException("Invalid image type: {$imageType}"); } $uuid = Uuid::uuid4()->toString(); try { + // Load device with relationships + $device->load(['palette', 'deviceModel.palette', 'user']); + // Get image generation settings from DeviceModel if available, otherwise use device settings $imageSettings = self::getImageSettings($device); @@ -345,7 +423,7 @@ class ImageGenerationService $outputPath = Storage::disk('public')->path('/images/generated/'.$uuid.'.'.$fileExtension); // Generate HTML from Blade template - $html = self::generateDefaultScreenHtml($device, $imageType); + $html = self::generateDefaultScreenHtml($device, $imageType, $pluginName); // Create custom Browsershot instance if using AWS Lambda $browsershotInstance = null; @@ -356,6 +434,10 @@ class ImageGenerationService $browserStage = new BrowserStage($browsershotInstance); $browserStage->html($html); + // Set timezone from user or fall back to app timezone + $timezone = $device->user->timezone ?? config('app.timezone'); + $browserStage->timezone($timezone); + if (config('app.puppeteer_window_size_strategy') === 'v2') { $browserStage ->width($imageSettings['width']) @@ -372,6 +454,14 @@ class ImageGenerationService $browserStage->setBrowsershotOption('args', ['--no-sandbox', '--disable-setuid-sandbox', '--disable-gpu']); } + // Get palette from device or fallback to device model's default palette + $palette = $device->palette ?? $device->deviceModel?->palette; + $colorPalette = null; + + if ($palette && $palette->colors) { + $colorPalette = $palette->colors; + } + $imageStage = new ImageStage(); $imageStage->format($fileExtension) ->width($imageSettings['width']) @@ -383,6 +473,11 @@ class ImageGenerationService ->offsetY($imageSettings['offset_y']) ->outputPath($outputPath); + // Apply color palette if available + if ($colorPalette) { + $imageStage->colormap($colorPalette); + } + (new TrmnlPipeline())->pipe($browserStage) ->pipe($imageStage) ->process(); @@ -408,12 +503,13 @@ class ImageGenerationService /** * Generate HTML from Blade template for default screens */ - private static function generateDefaultScreenHtml(Device $device, string $imageType): string + private static function generateDefaultScreenHtml(Device $device, string $imageType, ?string $pluginName = null): string { // Map image type to template name $templateName = match ($imageType) { 'setup-logo' => 'default-screens.setup', 'sleep' => 'default-screens.sleep', + 'error' => 'default-screens.error', default => throw new InvalidArgumentException("Invalid image type: {$imageType}") }; @@ -424,14 +520,22 @@ class ImageGenerationService $scaleLevel = $device->scaleLevel(); $darkMode = $imageType === 'sleep'; // Sleep mode uses dark mode, setup uses light mode - // Render the Blade template - return view($templateName, [ + // Build view data + $viewData = [ 'noBleed' => false, 'darkMode' => $darkMode, 'deviceVariant' => $deviceVariant, 'deviceOrientation' => $deviceOrientation, 'colorDepth' => $colorDepth, 'scaleLevel' => $scaleLevel, - ])->render(); + ]; + + // Add plugin name for error screens + if ($imageType === 'error' && $pluginName !== null) { + $viewData['pluginName'] = $pluginName; + } + + // Render the Blade template + return view($templateName, $viewData)->render(); } } diff --git a/app/Services/Plugin/Parsers/IcalResponseParser.php b/app/Services/Plugin/Parsers/IcalResponseParser.php new file mode 100644 index 0000000..c8f2b58 --- /dev/null +++ b/app/Services/Plugin/Parsers/IcalResponseParser.php @@ -0,0 +1,111 @@ +header('Content-Type'); + $body = $response->body(); + + if (! $this->isIcalResponse($contentType, $body)) { + return null; + } + + try { + $this->parser->parseString($body); + + $events = $this->parser->getEvents()->sorted()->getArrayCopy(); + $windowStart = now()->subDays(7); + $windowEnd = now()->addDays(30); + + $filteredEvents = array_values(array_filter($events, function (array $event) use ($windowStart, $windowEnd): bool { + $startDate = $this->asCarbon($event['DTSTART'] ?? null); + + if (! $startDate instanceof Carbon) { + return false; + } + + return $startDate->between($windowStart, $windowEnd, true); + })); + + $normalizedEvents = array_map($this->normalizeIcalEvent(...), $filteredEvents); + + return ['ical' => $normalizedEvents]; + } catch (Exception $exception) { + Log::warning('Failed to parse iCal response: '.$exception->getMessage()); + + return ['error' => 'Failed to parse iCal response']; + } + } + + private function isIcalResponse(?string $contentType, string $body): bool + { + $normalizedContentType = $contentType ? mb_strtolower($contentType) : ''; + + if ($normalizedContentType && str_contains($normalizedContentType, 'text/calendar')) { + return true; + } + + return str_contains($body, 'BEGIN:VCALENDAR'); + } + + private function asCarbon(DateTimeInterface|string|null $value): ?Carbon + { + if ($value instanceof Carbon) { + return $value; + } + + if ($value instanceof DateTimeInterface) { + return Carbon::instance($value); + } + + if (is_string($value) && $value !== '') { + try { + return Carbon::parse($value); + } catch (Exception $exception) { + Log::warning('Failed to parse date value: '.$exception->getMessage()); + + return null; + } + } + + return null; + } + + private function normalizeIcalEvent(array $event): array + { + $normalized = []; + + foreach ($event as $key => $value) { + $normalized[$key] = $this->normalizeIcalValue($value); + } + + return $normalized; + } + + private function normalizeIcalValue(mixed $value): mixed + { + if ($value instanceof DateTimeInterface) { + return Carbon::instance($value)->toAtomString(); + } + + if (is_array($value)) { + return array_map($this->normalizeIcalValue(...), $value); + } + + return $value; + } +} diff --git a/app/Services/Plugin/Parsers/JsonOrTextResponseParser.php b/app/Services/Plugin/Parsers/JsonOrTextResponseParser.php new file mode 100644 index 0000000..44ea0cb --- /dev/null +++ b/app/Services/Plugin/Parsers/JsonOrTextResponseParser.php @@ -0,0 +1,26 @@ +json(); + if ($json !== null) { + return $json; + } + + return ['data' => $response->body()]; + } catch (Exception $e) { + Log::warning('Failed to parse JSON response: '.$e->getMessage()); + + return ['error' => 'Failed to parse JSON response']; + } + } +} diff --git a/app/Services/Plugin/Parsers/ResponseParser.php b/app/Services/Plugin/Parsers/ResponseParser.php new file mode 100644 index 0000000..b8f9c05 --- /dev/null +++ b/app/Services/Plugin/Parsers/ResponseParser.php @@ -0,0 +1,15 @@ + + */ + private readonly array $parsers; + + /** + * @param array $parsers + */ + public function __construct(array $parsers = []) + { + $this->parsers = $parsers ?: [ + new XmlResponseParser(), + new IcalResponseParser(), + new JsonOrTextResponseParser(), + ]; + } + + /** + * @return array + */ + public function getParsers(): array + { + return $this->parsers; + } +} diff --git a/app/Services/Plugin/Parsers/XmlResponseParser.php b/app/Services/Plugin/Parsers/XmlResponseParser.php new file mode 100644 index 0000000..b82ba80 --- /dev/null +++ b/app/Services/Plugin/Parsers/XmlResponseParser.php @@ -0,0 +1,46 @@ +header('Content-Type'); + + if (! $contentType || ! str_contains(mb_strtolower($contentType), 'xml')) { + return null; + } + + try { + $xml = simplexml_load_string($response->body()); + if ($xml === false) { + throw new Exception('Invalid XML content'); + } + + return ['rss' => $this->xmlToArray($xml)]; + } catch (Exception $exception) { + Log::warning('Failed to parse XML response: '.$exception->getMessage()); + + return ['error' => 'Failed to parse XML response']; + } + } + + private function xmlToArray(SimpleXMLElement $xml): array + { + $array = (array) $xml; + + foreach ($array as $key => $value) { + if ($value instanceof SimpleXMLElement) { + $array[$key] = $this->xmlToArray($value); + } + } + + return $array; + } +} diff --git a/app/Services/PluginImportService.php b/app/Services/PluginImportService.php index a9d93b3..49dce99 100644 --- a/app/Services/PluginImportService.php +++ b/app/Services/PluginImportService.php @@ -17,6 +17,34 @@ use ZipArchive; class PluginImportService { + /** + * Validate YAML settings + * + * @param array $settings The parsed YAML settings + * + * @throws Exception + */ + private function validateYAML(array $settings): void + { + if (! isset($settings['custom_fields']) || ! is_array($settings['custom_fields'])) { + return; + } + + foreach ($settings['custom_fields'] as $field) { + if (isset($field['field_type']) && $field['field_type'] === 'multi_string') { + + if (isset($field['default']) && str_contains($field['default'], ',')) { + throw new Exception("Validation Error: The default value for multistring fields like `{$field['keyname']}` cannot contain commas."); + } + + if (isset($field['placeholder']) && str_contains($field['placeholder'], ',')) { + throw new Exception("Validation Error: The placeholder value for multistring fields like `{$field['keyname']}` cannot contain commas."); + } + + } + } + } + /** * Import a plugin from a ZIP file * @@ -47,32 +75,55 @@ class PluginImportService $zip->extractTo($tempDir); $zip->close(); - // Find the required files (settings.yml and full.liquid/full.blade.php) + // Find the required files (settings.yml and full.liquid/full.blade.php/shared.liquid/shared.blade.php) $filePaths = $this->findRequiredFiles($tempDir, $zipEntryPath); // Validate that we found the required files - if (! $filePaths['settingsYamlPath'] || ! $filePaths['fullLiquidPath']) { - throw new Exception('Invalid ZIP structure. Required files settings.yml and full.liquid are missing.'); // full.blade.php + if (! $filePaths['settingsYamlPath']) { + throw new Exception('Invalid ZIP structure. Required file settings.yml is missing.'); + } + + // Validate that we have at least one template file + if (! $filePaths['fullLiquidPath'] && ! $filePaths['sharedLiquidPath'] && ! $filePaths['sharedBladePath']) { + throw new Exception('Invalid ZIP structure. At least one of the following files is required: full.liquid, full.blade.php, shared.liquid, or shared.blade.php.'); } // Parse settings.yml $settingsYaml = File::get($filePaths['settingsYamlPath']); $settings = Yaml::parse($settingsYaml); + $this->validateYAML($settings); - // Read full.liquid content - $fullLiquid = File::get($filePaths['fullLiquidPath']); - - // Prepend shared.liquid content if available - if ($filePaths['sharedLiquidPath'] && File::exists($filePaths['sharedLiquidPath'])) { - $sharedLiquid = File::get($filePaths['sharedLiquidPath']); - $fullLiquid = $sharedLiquid."\n".$fullLiquid; - } - - // Check if the file ends with .liquid to set markup language + // Determine which template file to use and read its content + $templatePath = null; $markupLanguage = 'blade'; - if (pathinfo((string) $filePaths['fullLiquidPath'], PATHINFO_EXTENSION) === 'liquid') { + + if ($filePaths['fullLiquidPath']) { + $templatePath = $filePaths['fullLiquidPath']; + $fullLiquid = File::get($templatePath); + + // Prepend shared.liquid or shared.blade.php content if available + if ($filePaths['sharedLiquidPath'] && File::exists($filePaths['sharedLiquidPath'])) { + $sharedLiquid = File::get($filePaths['sharedLiquidPath']); + $fullLiquid = $sharedLiquid."\n".$fullLiquid; + } elseif ($filePaths['sharedBladePath'] && File::exists($filePaths['sharedBladePath'])) { + $sharedBlade = File::get($filePaths['sharedBladePath']); + $fullLiquid = $sharedBlade."\n".$fullLiquid; + } + + // Check if the file ends with .liquid to set markup language + if (pathinfo((string) $templatePath, PATHINFO_EXTENSION) === 'liquid') { + $markupLanguage = 'liquid'; + $fullLiquid = '
'."\n".$fullLiquid."\n".'
'; + } + } elseif ($filePaths['sharedLiquidPath']) { + $templatePath = $filePaths['sharedLiquidPath']; + $fullLiquid = File::get($templatePath); $markupLanguage = 'liquid'; $fullLiquid = '
'."\n".$fullLiquid."\n".'
'; + } elseif ($filePaths['sharedBladePath']) { + $templatePath = $filePaths['sharedBladePath']; + $fullLiquid = File::get($templatePath); + $markupLanguage = 'blade'; } // Ensure custom_fields is properly formatted @@ -80,6 +131,9 @@ class PluginImportService $settings['custom_fields'] = []; } + // Normalize options in custom_fields (convert non-named values to named values) + $settings['custom_fields'] = $this->normalizeCustomFieldsOptions($settings['custom_fields']); + // Create configuration template with the custom fields $configurationTemplate = [ 'custom_fields' => $settings['custom_fields'], @@ -139,11 +193,14 @@ class PluginImportService * @param string $zipUrl The URL to the ZIP file * @param User $user The user importing the plugin * @param string|null $zipEntryPath Optional path to specific plugin in monorepo + * @param string|null $preferredRenderer Optional preferred renderer (e.g., 'trmnl-liquid') + * @param string|null $iconUrl Optional icon URL to set on the plugin + * @param bool $allowDuplicate If true, generate a new UUID for trmnlp_id if a plugin with the same trmnlp_id already exists * @return Plugin The created plugin instance * * @throws Exception If the ZIP file is invalid or required files are missing */ - public function importFromUrl(string $zipUrl, User $user, ?string $zipEntryPath = null): Plugin + public function importFromUrl(string $zipUrl, User $user, ?string $zipEntryPath = null, $preferredRenderer = null, ?string $iconUrl = null, bool $allowDuplicate = false): Plugin { // Download the ZIP file $response = Http::timeout(60)->get($zipUrl); @@ -171,32 +228,55 @@ class PluginImportService $zip->extractTo($tempDir); $zip->close(); - // Find the required files (settings.yml and full.liquid/full.blade.php) + // Find the required files (settings.yml and full.liquid/full.blade.php/shared.liquid/shared.blade.php) $filePaths = $this->findRequiredFiles($tempDir, $zipEntryPath); // Validate that we found the required files - if (! $filePaths['settingsYamlPath'] || ! $filePaths['fullLiquidPath']) { - throw new Exception('Invalid ZIP structure. Required files settings.yml and full.liquid/full.blade.php are missing.'); + if (! $filePaths['settingsYamlPath']) { + throw new Exception('Invalid ZIP structure. Required file settings.yml is missing.'); + } + + // Validate that we have at least one template file + if (! $filePaths['fullLiquidPath'] && ! $filePaths['sharedLiquidPath'] && ! $filePaths['sharedBladePath']) { + throw new Exception('Invalid ZIP structure. At least one of the following files is required: full.liquid, full.blade.php, shared.liquid, or shared.blade.php.'); } // Parse settings.yml $settingsYaml = File::get($filePaths['settingsYamlPath']); $settings = Yaml::parse($settingsYaml); + $this->validateYAML($settings); - // Read full.liquid content - $fullLiquid = File::get($filePaths['fullLiquidPath']); - - // Prepend shared.liquid content if available - if ($filePaths['sharedLiquidPath'] && File::exists($filePaths['sharedLiquidPath'])) { - $sharedLiquid = File::get($filePaths['sharedLiquidPath']); - $fullLiquid = $sharedLiquid."\n".$fullLiquid; - } - - // Check if the file ends with .liquid to set markup language + // Determine which template file to use and read its content + $templatePath = null; $markupLanguage = 'blade'; - if (pathinfo((string) $filePaths['fullLiquidPath'], PATHINFO_EXTENSION) === 'liquid') { + + if ($filePaths['fullLiquidPath']) { + $templatePath = $filePaths['fullLiquidPath']; + $fullLiquid = File::get($templatePath); + + // Prepend shared.liquid or shared.blade.php content if available + if ($filePaths['sharedLiquidPath'] && File::exists($filePaths['sharedLiquidPath'])) { + $sharedLiquid = File::get($filePaths['sharedLiquidPath']); + $fullLiquid = $sharedLiquid."\n".$fullLiquid; + } elseif ($filePaths['sharedBladePath'] && File::exists($filePaths['sharedBladePath'])) { + $sharedBlade = File::get($filePaths['sharedBladePath']); + $fullLiquid = $sharedBlade."\n".$fullLiquid; + } + + // Check if the file ends with .liquid to set markup language + if (pathinfo((string) $templatePath, PATHINFO_EXTENSION) === 'liquid') { + $markupLanguage = 'liquid'; + $fullLiquid = '
'."\n".$fullLiquid."\n".'
'; + } + } elseif ($filePaths['sharedLiquidPath']) { + $templatePath = $filePaths['sharedLiquidPath']; + $fullLiquid = File::get($templatePath); $markupLanguage = 'liquid'; $fullLiquid = '
'."\n".$fullLiquid."\n".'
'; + } elseif ($filePaths['sharedBladePath']) { + $templatePath = $filePaths['sharedBladePath']; + $fullLiquid = File::get($templatePath); + $markupLanguage = 'blade'; } // Ensure custom_fields is properly formatted @@ -204,22 +284,34 @@ class PluginImportService $settings['custom_fields'] = []; } + // Normalize options in custom_fields (convert non-named values to named values) + $settings['custom_fields'] = $this->normalizeCustomFieldsOptions($settings['custom_fields']); + // Create configuration template with the custom fields $configurationTemplate = [ 'custom_fields' => $settings['custom_fields'], ]; - $plugin_updated = isset($settings['id']) + // Determine the trmnlp_id to use + $trmnlpId = $settings['id'] ?? Uuid::v7(); + + // If allowDuplicate is true and a plugin with this trmnlp_id already exists, generate a new UUID + if ($allowDuplicate && isset($settings['id']) && Plugin::where('user_id', $user->id)->where('trmnlp_id', $settings['id'])->exists()) { + $trmnlpId = Uuid::v7(); + } + + $plugin_updated = ! $allowDuplicate && isset($settings['id']) && Plugin::where('user_id', $user->id)->where('trmnlp_id', $settings['id'])->exists(); + // Create a new plugin $plugin = Plugin::updateOrCreate( [ - 'user_id' => $user->id, 'trmnlp_id' => $settings['id'] ?? Uuid::v7(), + 'user_id' => $user->id, 'trmnlp_id' => $trmnlpId, ], [ 'user_id' => $user->id, 'name' => $settings['name'] ?? 'Imported Plugin', - 'trmnlp_id' => $settings['id'] ?? Uuid::v7(), + 'trmnlp_id' => $trmnlpId, 'data_stale_minutes' => $settings['refresh_interval'] ?? 15, 'data_strategy' => $settings['strategy'] ?? 'static', 'polling_url' => $settings['polling_url'] ?? null, @@ -232,6 +324,8 @@ class PluginImportService 'render_markup' => $fullLiquid, 'configuration_template' => $configurationTemplate, 'data_payload' => json_decode($settings['static_data'] ?? '{}', true), + 'preferred_renderer' => $preferredRenderer, + 'icon_url' => $iconUrl, ]); if (! $plugin_updated) { @@ -262,6 +356,7 @@ class PluginImportService $settingsYamlPath = null; $fullLiquidPath = null; $sharedLiquidPath = null; + $sharedBladePath = null; // If zipEntryPath is specified, look for files in that specific directory first if ($zipEntryPath) { @@ -279,6 +374,8 @@ class PluginImportService if (File::exists($targetDir.'/shared.liquid')) { $sharedLiquidPath = $targetDir.'/shared.liquid'; + } elseif (File::exists($targetDir.'/shared.blade.php')) { + $sharedBladePath = $targetDir.'/shared.blade.php'; } } @@ -294,15 +391,18 @@ class PluginImportService if (File::exists($targetDir.'/src/shared.liquid')) { $sharedLiquidPath = $targetDir.'/src/shared.liquid'; + } elseif (File::exists($targetDir.'/src/shared.blade.php')) { + $sharedBladePath = $targetDir.'/src/shared.blade.php'; } } // If we found the required files in the target directory, return them - if ($settingsYamlPath && $fullLiquidPath) { + if ($settingsYamlPath && ($fullLiquidPath || $sharedLiquidPath || $sharedBladePath)) { return [ 'settingsYamlPath' => $settingsYamlPath, 'fullLiquidPath' => $fullLiquidPath, 'sharedLiquidPath' => $sharedLiquidPath, + 'sharedBladePath' => $sharedBladePath, ]; } } @@ -319,9 +419,11 @@ class PluginImportService $fullLiquidPath = $tempDir.'/src/full.blade.php'; } - // Check for shared.liquid in the same directory + // Check for shared.liquid or shared.blade.php in the same directory if (File::exists($tempDir.'/src/shared.liquid')) { $sharedLiquidPath = $tempDir.'/src/shared.liquid'; + } elseif (File::exists($tempDir.'/src/shared.blade.php')) { + $sharedBladePath = $tempDir.'/src/shared.blade.php'; } } else { // Search for the files in the extracted directory structure @@ -338,17 +440,24 @@ class PluginImportService $fullLiquidPath = $filepath; } elseif ($filename === 'shared.liquid') { $sharedLiquidPath = $filepath; + } elseif ($filename === 'shared.blade.php') { + $sharedBladePath = $filepath; } + } - // If we found both required files, break the loop - if ($settingsYamlPath && $fullLiquidPath) { - break; + // Check if shared.liquid or shared.blade.php exists in the same directory as full.liquid + if ($settingsYamlPath && $fullLiquidPath && ! $sharedLiquidPath && ! $sharedBladePath) { + $fullLiquidDir = dirname((string) $fullLiquidPath); + if (File::exists($fullLiquidDir.'/shared.liquid')) { + $sharedLiquidPath = $fullLiquidDir.'/shared.liquid'; + } elseif (File::exists($fullLiquidDir.'/shared.blade.php')) { + $sharedBladePath = $fullLiquidDir.'/shared.blade.php'; } } // If we found the files but they're not in the src folder, // check if they're in the root of the ZIP or in a subfolder - if ($settingsYamlPath && $fullLiquidPath) { + if ($settingsYamlPath && ($fullLiquidPath || $sharedLiquidPath || $sharedBladePath)) { // If the files are in the root of the ZIP, create a src folder and move them there $srcDir = dirname((string) $settingsYamlPath); @@ -359,17 +468,25 @@ class PluginImportService // Copy the files to the src directory File::copy($settingsYamlPath, $newSrcDir.'/settings.yml'); - File::copy($fullLiquidPath, $newSrcDir.'/full.liquid'); - // Copy shared.liquid if it exists + // Copy full.liquid or full.blade.php if it exists + if ($fullLiquidPath) { + $extension = pathinfo((string) $fullLiquidPath, PATHINFO_EXTENSION); + File::copy($fullLiquidPath, $newSrcDir.'/full.'.$extension); + $fullLiquidPath = $newSrcDir.'/full.'.$extension; + } + + // Copy shared.liquid or shared.blade.php if it exists if ($sharedLiquidPath) { File::copy($sharedLiquidPath, $newSrcDir.'/shared.liquid'); $sharedLiquidPath = $newSrcDir.'/shared.liquid'; + } elseif ($sharedBladePath) { + File::copy($sharedBladePath, $newSrcDir.'/shared.blade.php'); + $sharedBladePath = $newSrcDir.'/shared.blade.php'; } // Update the paths $settingsYamlPath = $newSrcDir.'/settings.yml'; - $fullLiquidPath = $newSrcDir.'/full.liquid'; } } } @@ -378,6 +495,104 @@ class PluginImportService 'settingsYamlPath' => $settingsYamlPath, 'fullLiquidPath' => $fullLiquidPath, 'sharedLiquidPath' => $sharedLiquidPath, + 'sharedBladePath' => $sharedBladePath, ]; } + + /** + * Normalize options in custom_fields by converting non-named values to named values + * This ensures that options like ["true", "false"] become [["true" => "true"], ["false" => "false"]] + * + * @param array $customFields The custom_fields array from settings + * @return array The normalized custom_fields array + */ + private function normalizeCustomFieldsOptions(array $customFields): array + { + foreach ($customFields as &$field) { + // Only process select fields with options + if (isset($field['field_type']) && $field['field_type'] === 'select' && isset($field['options']) && is_array($field['options'])) { + $normalizedOptions = []; + foreach ($field['options'] as $option) { + // If option is already a named value (array with key-value pair), keep it as is + if (is_array($option)) { + $normalizedOptions[] = $option; + } else { + // Convert non-named value to named value + // Convert boolean to string, use lowercase for label + $value = is_bool($option) ? ($option ? 'true' : 'false') : (string) $option; + $normalizedOptions[] = [$value => $value]; + } + } + $field['options'] = $normalizedOptions; + + // Normalize default value to match normalized option values + if (isset($field['default'])) { + $default = $field['default']; + // If default is boolean, convert to string to match normalized options + if (is_bool($default)) { + $field['default'] = $default ? 'true' : 'false'; + } else { + // Convert to string to ensure consistency + $field['default'] = (string) $default; + } + } + } + } + + return $customFields; + } + + /** + * Validate that template and context are within command-line argument limits + * + * @param string $template The liquid template string + * @param string $jsonContext The JSON-encoded context + * @param string $liquidPath The path to the liquid renderer executable + * + * @throws Exception If the template or context exceeds argument limits + */ + public function validateExternalRendererArguments(string $template, string $jsonContext, string $liquidPath): void + { + // MAX_ARG_STRLEN on Linux is typically 131072 (128KB) for individual arguments + // ARG_MAX is the total size of all arguments (typically 2MB on modern systems) + $maxIndividualArgLength = 131072; // 128KB - MAX_ARG_STRLEN limit + $maxTotalArgLength = $this->getMaxArgumentLength(); + + // Check individual argument sizes (template and context are the largest) + if (mb_strlen($template) > $maxIndividualArgLength || mb_strlen($jsonContext) > $maxIndividualArgLength) { + throw new Exception('Context too large for external liquid renderer. Reduce the size of the Payload or Template.'); + } + + // Calculate total size of all arguments (path + flags + template + context) + // Add overhead for path, flags, and separators (conservative estimate: ~200 bytes) + $totalArgSize = mb_strlen($liquidPath) + mb_strlen('--template') + mb_strlen($template) + + mb_strlen('--context') + mb_strlen($jsonContext) + 200; + + if ($totalArgSize > $maxTotalArgLength) { + throw new Exception('Context too large for external liquid renderer. Reduce the size of the Payload or Template.'); + } + } + + /** + * Get the maximum argument length for command-line arguments + * + * @return int Maximum argument length in bytes + */ + private function getMaxArgumentLength(): int + { + // Try to get ARG_MAX from system using getconf + $argMax = null; + if (function_exists('shell_exec')) { + $result = @shell_exec('getconf ARG_MAX 2>/dev/null'); + if ($result !== null && is_numeric(mb_trim($result))) { + $argMax = (int) mb_trim($result); + } + } + + // Use conservative fallback if ARG_MAX cannot be determined + // ARG_MAX on macOS is typically 262144 (256KB), on Linux it's usually 2097152 (2MB) + // We use 200KB as a conservative limit that works on both systems + // Note: ARG_MAX includes environment variables, so we leave headroom + return $argMax !== null ? min($argMax, 204800) : 204800; + } } diff --git a/boost.json b/boost.json deleted file mode 100644 index 53962fa..0000000 --- a/boost.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "agents": [ - "claude_code", - "copilot", - "cursor", - "phpstorm" - ], - "editors": [ - "claude_code", - "cursor", - "phpstorm", - "vscode" - ], - "guidelines": [] -} diff --git a/composer.json b/composer.json index 79306ce..0ced4da 100644 --- a/composer.json +++ b/composer.json @@ -6,6 +6,7 @@ "keywords": [ "trmnl", "trmnl-server", + "trmnl-byos", "laravel" ], "license": "MIT", @@ -14,8 +15,8 @@ "ext-imagick": "*", "ext-simplexml": "*", "ext-zip": "*", - "bnussbau/laravel-trmnl-blade": "2.0.*", - "bnussbau/trmnl-pipeline-php": "^0.4.0", + "bnussbau/laravel-trmnl-blade": "2.1.*", + "bnussbau/trmnl-pipeline-php": "^0.6.0", "keepsuit/laravel-liquid": "^0.5.2", "laravel/framework": "^12.1", "laravel/sanctum": "^4.0", @@ -23,7 +24,9 @@ "laravel/tinker": "^2.10.1", "livewire/flux": "^2.0", "livewire/volt": "^1.7", + "om/icalparser": "^3.2", "spatie/browsershot": "^5.0", + "stevebauman/purify": "^6.3", "symfony/yaml": "^7.3", "wnx/sidecar-browsershot": "^2.6" }, diff --git a/composer.lock b/composer.lock index 60b450d..d23d014 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "3d743ce4dc2742c59ed6f9cc8ed36e04", + "content-hash": "25c2a1a4a2f2594adefe25ddb6a072fb", "packages": [ { "name": "aws/aws-crt-php", @@ -62,16 +62,16 @@ }, { "name": "aws/aws-sdk-php", - "version": "3.359.6", + "version": "3.369.10", "source": { "type": "git", "url": "https://github.com/aws/aws-sdk-php.git", - "reference": "8d2ab3687196f15209c316080a431911f2e02bb5" + "reference": "e179090bf2d658be7be37afc146111966ba6f41b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/8d2ab3687196f15209c316080a431911f2e02bb5", - "reference": "8d2ab3687196f15209c316080a431911f2e02bb5", + "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/e179090bf2d658be7be37afc146111966ba6f41b", + "reference": "e179090bf2d658be7be37afc146111966ba6f41b", "shasum": "" }, "require": { @@ -84,7 +84,8 @@ "guzzlehttp/psr7": "^2.4.5", "mtdowling/jmespath.php": "^2.8.0", "php": ">=8.1", - "psr/http-message": "^1.0 || ^2.0" + "psr/http-message": "^1.0 || ^2.0", + "symfony/filesystem": "^v5.4.45 || ^v6.4.3 || ^v7.1.0 || ^v8.0.0" }, "require-dev": { "andrewsville/php-token-reflection": "^1.4", @@ -95,13 +96,11 @@ "doctrine/cache": "~1.4", "ext-dom": "*", "ext-openssl": "*", - "ext-pcntl": "*", "ext-sockets": "*", - "phpunit/phpunit": "^5.6.3 || ^8.5 || ^9.5", + "phpunit/phpunit": "^9.6", "psr/cache": "^2.0 || ^3.0", "psr/simple-cache": "^2.0 || ^3.0", "sebastian/comparator": "^1.2.3 || ^4.0 || ^5.0", - "symfony/filesystem": "^v6.4.0 || ^v7.1.0", "yoast/phpunit-polyfills": "^2.0" }, "suggest": { @@ -109,6 +108,7 @@ "doctrine/cache": "To use the DoctrineCacheAdapter", "ext-curl": "To send requests using cURL", "ext-openssl": "Allows working with CloudFront private distributions and verifying received SNS messages", + "ext-pcntl": "To use client-side monitoring", "ext-sockets": "To use client-side monitoring" }, "type": "library", @@ -153,22 +153,22 @@ "support": { "forum": "https://github.com/aws/aws-sdk-php/discussions", "issues": "https://github.com/aws/aws-sdk-php/issues", - "source": "https://github.com/aws/aws-sdk-php/tree/3.359.6" + "source": "https://github.com/aws/aws-sdk-php/tree/3.369.10" }, - "time": "2025-11-05T19:08:10+00:00" + "time": "2026-01-09T19:08:12+00:00" }, { "name": "bnussbau/laravel-trmnl-blade", - "version": "2.0.1", + "version": "2.1.0", "source": { "type": "git", "url": "https://github.com/bnussbau/laravel-trmnl-blade.git", - "reference": "59343cfa9c41c7c7f9285b366584cde92bf1294e" + "reference": "1e1cabfead00118d7a80c86ac6108aece2989bc7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/bnussbau/laravel-trmnl-blade/zipball/59343cfa9c41c7c7f9285b366584cde92bf1294e", - "reference": "59343cfa9c41c7c7f9285b366584cde92bf1294e", + "url": "https://api.github.com/repos/bnussbau/laravel-trmnl-blade/zipball/1e1cabfead00118d7a80c86ac6108aece2989bc7", + "reference": "1e1cabfead00118d7a80c86ac6108aece2989bc7", "shasum": "" }, "require": { @@ -223,7 +223,7 @@ ], "support": { "issues": "https://github.com/bnussbau/laravel-trmnl-blade/issues", - "source": "https://github.com/bnussbau/laravel-trmnl-blade/tree/2.0.1" + "source": "https://github.com/bnussbau/laravel-trmnl-blade/tree/2.1.0" }, "funding": [ { @@ -239,20 +239,20 @@ "type": "github" } ], - "time": "2025-09-22T12:12:00+00:00" + "time": "2026-01-02T20:38:51+00:00" }, { "name": "bnussbau/trmnl-pipeline-php", - "version": "0.4.0", + "version": "0.6.0", "source": { "type": "git", "url": "https://github.com/bnussbau/trmnl-pipeline-php.git", - "reference": "b58b937f36e55aa7cbd4859cbe7a902bf1050bf8" + "reference": "228505afa8a39a5033916e9ae5ccbf1733092c1f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/bnussbau/trmnl-pipeline-php/zipball/b58b937f36e55aa7cbd4859cbe7a902bf1050bf8", - "reference": "b58b937f36e55aa7cbd4859cbe7a902bf1050bf8", + "url": "https://api.github.com/repos/bnussbau/trmnl-pipeline-php/zipball/228505afa8a39a5033916e9ae5ccbf1733092c1f", + "reference": "228505afa8a39a5033916e9ae5ccbf1733092c1f", "shasum": "" }, "require": { @@ -294,7 +294,7 @@ ], "support": { "issues": "https://github.com/bnussbau/trmnl-pipeline-php/issues", - "source": "https://github.com/bnussbau/trmnl-pipeline-php/tree/0.4.0" + "source": "https://github.com/bnussbau/trmnl-pipeline-php/tree/0.6.0" }, "funding": [ { @@ -310,20 +310,20 @@ "type": "github" } ], - "time": "2025-10-30T11:52:17+00:00" + "time": "2025-12-02T15:18:51+00:00" }, { "name": "brick/math", - "version": "0.14.0", + "version": "0.14.1", "source": { "type": "git", "url": "https://github.com/brick/math.git", - "reference": "113a8ee2656b882d4c3164fa31aa6e12cbb7aaa2" + "reference": "f05858549e5f9d7bb45875a75583240a38a281d0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/brick/math/zipball/113a8ee2656b882d4c3164fa31aa6e12cbb7aaa2", - "reference": "113a8ee2656b882d4c3164fa31aa6e12cbb7aaa2", + "url": "https://api.github.com/repos/brick/math/zipball/f05858549e5f9d7bb45875a75583240a38a281d0", + "reference": "f05858549e5f9d7bb45875a75583240a38a281d0", "shasum": "" }, "require": { @@ -362,7 +362,7 @@ ], "support": { "issues": "https://github.com/brick/math/issues", - "source": "https://github.com/brick/math/tree/0.14.0" + "source": "https://github.com/brick/math/tree/0.14.1" }, "funding": [ { @@ -370,7 +370,7 @@ "type": "github" } ], - "time": "2025-08-29T12:40:03+00:00" + "time": "2025-11-24T14:40:29+00:00" }, { "name": "carbonphp/carbon-doctrine-types", @@ -815,17 +815,78 @@ "time": "2025-03-06T22:45:56+00:00" }, { - "name": "firebase/php-jwt", - "version": "v6.11.1", + "name": "ezyang/htmlpurifier", + "version": "v4.19.0", "source": { "type": "git", - "url": "https://github.com/firebase/php-jwt.git", - "reference": "d1e91ecf8c598d073d0995afa8cd5c75c6e19e66" + "url": "https://github.com/ezyang/htmlpurifier.git", + "reference": "b287d2a16aceffbf6e0295559b39662612b77fcf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/firebase/php-jwt/zipball/d1e91ecf8c598d073d0995afa8cd5c75c6e19e66", - "reference": "d1e91ecf8c598d073d0995afa8cd5c75c6e19e66", + "url": "https://api.github.com/repos/ezyang/htmlpurifier/zipball/b287d2a16aceffbf6e0295559b39662612b77fcf", + "reference": "b287d2a16aceffbf6e0295559b39662612b77fcf", + "shasum": "" + }, + "require": { + "php": "~5.6.0 || ~7.0.0 || ~7.1.0 || ~7.2.0 || ~7.3.0 || ~7.4.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0" + }, + "require-dev": { + "cerdic/css-tidy": "^1.7 || ^2.0", + "simpletest/simpletest": "dev-master" + }, + "suggest": { + "cerdic/css-tidy": "If you want to use the filter 'Filter.ExtractStyleBlocks'.", + "ext-bcmath": "Used for unit conversion and imagecrash protection", + "ext-iconv": "Converts text to and from non-UTF-8 encodings", + "ext-tidy": "Used for pretty-printing HTML" + }, + "type": "library", + "autoload": { + "files": [ + "library/HTMLPurifier.composer.php" + ], + "psr-0": { + "HTMLPurifier": "library/" + }, + "exclude-from-classmap": [ + "/library/HTMLPurifier/Language/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-2.1-or-later" + ], + "authors": [ + { + "name": "Edward Z. Yang", + "email": "admin@htmlpurifier.org", + "homepage": "http://ezyang.com" + } + ], + "description": "Standards compliant HTML filter written in PHP", + "homepage": "http://htmlpurifier.org/", + "keywords": [ + "html" + ], + "support": { + "issues": "https://github.com/ezyang/htmlpurifier/issues", + "source": "https://github.com/ezyang/htmlpurifier/tree/v4.19.0" + }, + "time": "2025-10-17T16:34:55+00:00" + }, + { + "name": "firebase/php-jwt", + "version": "v7.0.2", + "source": { + "type": "git", + "url": "https://github.com/firebase/php-jwt.git", + "reference": "5645b43af647b6947daac1d0f659dd1fbe8d3b65" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/firebase/php-jwt/zipball/5645b43af647b6947daac1d0f659dd1fbe8d3b65", + "reference": "5645b43af647b6947daac1d0f659dd1fbe8d3b65", "shasum": "" }, "require": { @@ -873,37 +934,37 @@ ], "support": { "issues": "https://github.com/firebase/php-jwt/issues", - "source": "https://github.com/firebase/php-jwt/tree/v6.11.1" + "source": "https://github.com/firebase/php-jwt/tree/v7.0.2" }, - "time": "2025-04-09T20:32:01+00:00" + "time": "2025-12-16T22:17:28+00:00" }, { "name": "fruitcake/php-cors", - "version": "v1.3.0", + "version": "v1.4.0", "source": { "type": "git", "url": "https://github.com/fruitcake/php-cors.git", - "reference": "3d158f36e7875e2f040f37bc0573956240a5a38b" + "reference": "38aaa6c3fd4c157ffe2a4d10aa8b9b16ba8de379" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/fruitcake/php-cors/zipball/3d158f36e7875e2f040f37bc0573956240a5a38b", - "reference": "3d158f36e7875e2f040f37bc0573956240a5a38b", + "url": "https://api.github.com/repos/fruitcake/php-cors/zipball/38aaa6c3fd4c157ffe2a4d10aa8b9b16ba8de379", + "reference": "38aaa6c3fd4c157ffe2a4d10aa8b9b16ba8de379", "shasum": "" }, "require": { - "php": "^7.4|^8.0", - "symfony/http-foundation": "^4.4|^5.4|^6|^7" + "php": "^8.1", + "symfony/http-foundation": "^5.4|^6.4|^7.3|^8" }, "require-dev": { - "phpstan/phpstan": "^1.4", + "phpstan/phpstan": "^2", "phpunit/phpunit": "^9", - "squizlabs/php_codesniffer": "^3.5" + "squizlabs/php_codesniffer": "^4" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.2-dev" + "dev-master": "1.3-dev" } }, "autoload": { @@ -934,7 +995,7 @@ ], "support": { "issues": "https://github.com/fruitcake/php-cors/issues", - "source": "https://github.com/fruitcake/php-cors/tree/v1.3.0" + "source": "https://github.com/fruitcake/php-cors/tree/v1.4.0" }, "funding": [ { @@ -946,28 +1007,28 @@ "type": "github" } ], - "time": "2023-10-12T05:21:21+00:00" + "time": "2025-12-03T09:33:47+00:00" }, { "name": "graham-campbell/result-type", - "version": "v1.1.3", + "version": "v1.1.4", "source": { "type": "git", "url": "https://github.com/GrahamCampbell/Result-Type.git", - "reference": "3ba905c11371512af9d9bdd27d99b782216b6945" + "reference": "e01f4a821471308ba86aa202fed6698b6b695e3b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/GrahamCampbell/Result-Type/zipball/3ba905c11371512af9d9bdd27d99b782216b6945", - "reference": "3ba905c11371512af9d9bdd27d99b782216b6945", + "url": "https://api.github.com/repos/GrahamCampbell/Result-Type/zipball/e01f4a821471308ba86aa202fed6698b6b695e3b", + "reference": "e01f4a821471308ba86aa202fed6698b6b695e3b", "shasum": "" }, "require": { "php": "^7.2.5 || ^8.0", - "phpoption/phpoption": "^1.9.3" + "phpoption/phpoption": "^1.9.5" }, "require-dev": { - "phpunit/phpunit": "^8.5.39 || ^9.6.20 || ^10.5.28" + "phpunit/phpunit": "^8.5.41 || ^9.6.22 || ^10.5.45 || ^11.5.7" }, "type": "library", "autoload": { @@ -996,7 +1057,7 @@ ], "support": { "issues": "https://github.com/GrahamCampbell/Result-Type/issues", - "source": "https://github.com/GrahamCampbell/Result-Type/tree/v1.1.3" + "source": "https://github.com/GrahamCampbell/Result-Type/tree/v1.1.4" }, "funding": [ { @@ -1008,7 +1069,7 @@ "type": "tidelift" } ], - "time": "2024-07-20T21:45:45+00:00" + "time": "2025-12-27T19:43:20+00:00" }, { "name": "guzzlehttp/guzzle", @@ -1556,16 +1617,16 @@ }, { "name": "keepsuit/liquid", - "version": "v0.9.0", + "version": "v0.9.1", "source": { "type": "git", "url": "https://github.com/keepsuit/php-liquid.git", - "reference": "f5d81df3689acb79b04c7be3d13778e1f138185f" + "reference": "844d88540524f99d9039916e0ef688b7f222ebc0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/keepsuit/php-liquid/zipball/f5d81df3689acb79b04c7be3d13778e1f138185f", - "reference": "f5d81df3689acb79b04c7be3d13778e1f138185f", + "url": "https://api.github.com/repos/keepsuit/php-liquid/zipball/844d88540524f99d9039916e0ef688b7f222ebc0", + "reference": "844d88540524f99d9039916e0ef688b7f222ebc0", "shasum": "" }, "require": { @@ -1574,17 +1635,17 @@ }, "require-dev": { "laravel/pint": "^1.2", - "pestphp/pest": "^2.36 || ^3.0", - "pestphp/pest-plugin-arch": "^2.7 || ^3.0", + "pestphp/pest": "^2.36 || ^3.0 || ^4.0", + "pestphp/pest-plugin-arch": "^2.7 || ^3.0 || ^4.0", "phpbench/phpbench": "dev-master", "phpstan/extension-installer": "^1.3", "phpstan/phpstan": "^2.0", "phpstan/phpstan-deprecation-rules": "^2.0", "spatie/invade": "^2.0", "spatie/ray": "^1.28", - "symfony/console": "^6.1 || ^7.0", - "symfony/var-exporter": "^6.1 || ^7.0", - "symfony/yaml": "^6.1 || ^7.0" + "symfony/console": "^6.1 || ^7.0 || ^8.0", + "symfony/var-exporter": "^6.1 || ^7.0 || ^8.0", + "symfony/yaml": "^6.1 || ^7.0 || ^8.0" }, "type": "library", "autoload": { @@ -1611,22 +1672,22 @@ ], "support": { "issues": "https://github.com/keepsuit/php-liquid/issues", - "source": "https://github.com/keepsuit/php-liquid/tree/v0.9.0" + "source": "https://github.com/keepsuit/php-liquid/tree/v0.9.1" }, - "time": "2025-06-15T12:02:45+00:00" + "time": "2025-12-01T12:01:51+00:00" }, { "name": "laravel/framework", - "version": "v12.37.0", + "version": "v12.46.0", "source": { "type": "git", "url": "https://github.com/laravel/framework.git", - "reference": "3c3c4ad30f5b528b164a7c09aa4ad03118c4c125" + "reference": "9dcff48d25a632c1fadb713024c952fec489c4ae" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/framework/zipball/3c3c4ad30f5b528b164a7c09aa4ad03118c4c125", - "reference": "3c3c4ad30f5b528b164a7c09aa4ad03118c4c125", + "url": "https://api.github.com/repos/laravel/framework/zipball/9dcff48d25a632c1fadb713024c952fec489c4ae", + "reference": "9dcff48d25a632c1fadb713024c952fec489c4ae", "shasum": "" }, "require": { @@ -1714,6 +1775,7 @@ "illuminate/process": "self.version", "illuminate/queue": "self.version", "illuminate/redis": "self.version", + "illuminate/reflection": "self.version", "illuminate/routing": "self.version", "illuminate/session": "self.version", "illuminate/support": "self.version", @@ -1738,13 +1800,13 @@ "league/flysystem-sftp-v3": "^3.25.1", "mockery/mockery": "^1.6.10", "opis/json-schema": "^2.4.1", - "orchestra/testbench-core": "^10.7.0", + "orchestra/testbench-core": "^10.8.1", "pda/pheanstalk": "^5.0.6|^7.0.0", "php-http/discovery": "^1.15", "phpstan/phpstan": "^2.0", "phpunit/phpunit": "^10.5.35|^11.5.3|^12.0.1", "predis/predis": "^2.3|^3.0", - "resend/resend-php": "^0.10.0", + "resend/resend-php": "^0.10.0|^1.0", "symfony/cache": "^7.2.0", "symfony/http-client": "^7.2.0", "symfony/psr-http-message-bridge": "^7.2.0", @@ -1778,7 +1840,7 @@ "predis/predis": "Required to use the predis connector (^2.3|^3.0).", "psr/http-message": "Required to allow Storage::put to accept a StreamInterface (^1.0).", "pusher/pusher-php-server": "Required to use the Pusher broadcast driver (^6.0|^7.0).", - "resend/resend-php": "Required to enable support for the Resend mail transport (^0.10.0).", + "resend/resend-php": "Required to enable support for the Resend mail transport (^0.10.0|^1.0).", "symfony/cache": "Required to PSR-6 cache bridge (^7.2).", "symfony/filesystem": "Required to enable support for relative symbolic links (^7.2).", "symfony/http-client": "Required to enable support for the Symfony API mail transports (^7.2).", @@ -1800,6 +1862,7 @@ "src/Illuminate/Filesystem/functions.php", "src/Illuminate/Foundation/helpers.php", "src/Illuminate/Log/functions.php", + "src/Illuminate/Reflection/helpers.php", "src/Illuminate/Support/functions.php", "src/Illuminate/Support/helpers.php" ], @@ -1808,7 +1871,8 @@ "Illuminate\\Support\\": [ "src/Illuminate/Macroable/", "src/Illuminate/Collections/", - "src/Illuminate/Conditionable/" + "src/Illuminate/Conditionable/", + "src/Illuminate/Reflection/" ] } }, @@ -1832,20 +1896,20 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2025-11-04T15:39:33+00:00" + "time": "2026-01-07T23:26:53+00:00" }, { "name": "laravel/prompts", - "version": "v0.3.7", + "version": "v0.3.8", "source": { "type": "git", "url": "https://github.com/laravel/prompts.git", - "reference": "a1891d362714bc40c8d23b0b1d7090f022ea27cc" + "reference": "096748cdfb81988f60090bbb839ce3205ace0d35" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/prompts/zipball/a1891d362714bc40c8d23b0b1d7090f022ea27cc", - "reference": "a1891d362714bc40c8d23b0b1d7090f022ea27cc", + "url": "https://api.github.com/repos/laravel/prompts/zipball/096748cdfb81988f60090bbb839ce3205ace0d35", + "reference": "096748cdfb81988f60090bbb839ce3205ace0d35", "shasum": "" }, "require": { @@ -1861,7 +1925,7 @@ "require-dev": { "illuminate/collections": "^10.0|^11.0|^12.0", "mockery/mockery": "^1.5", - "pestphp/pest": "^2.3|^3.4", + "pestphp/pest": "^2.3|^3.4|^4.0", "phpstan/phpstan": "^1.12.28", "phpstan/phpstan-mockery": "^1.1.3" }, @@ -1889,22 +1953,22 @@ "description": "Add beautiful and user-friendly forms to your command-line applications.", "support": { "issues": "https://github.com/laravel/prompts/issues", - "source": "https://github.com/laravel/prompts/tree/v0.3.7" + "source": "https://github.com/laravel/prompts/tree/v0.3.8" }, - "time": "2025-09-19T13:47:56+00:00" + "time": "2025-11-21T20:52:52+00:00" }, { "name": "laravel/sanctum", - "version": "v4.2.0", + "version": "v4.2.2", "source": { "type": "git", "url": "https://github.com/laravel/sanctum.git", - "reference": "fd6df4f79f48a72992e8d29a9c0ee25422a0d677" + "reference": "fd447754d2d3f56950d53b930128af2e3b617de9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/sanctum/zipball/fd6df4f79f48a72992e8d29a9c0ee25422a0d677", - "reference": "fd6df4f79f48a72992e8d29a9c0ee25422a0d677", + "url": "https://api.github.com/repos/laravel/sanctum/zipball/fd447754d2d3f56950d53b930128af2e3b617de9", + "reference": "fd447754d2d3f56950d53b930128af2e3b617de9", "shasum": "" }, "require": { @@ -1918,9 +1982,8 @@ }, "require-dev": { "mockery/mockery": "^1.6", - "orchestra/testbench": "^9.0|^10.0", - "phpstan/phpstan": "^1.10", - "phpunit/phpunit": "^11.3" + "orchestra/testbench": "^9.15|^10.8", + "phpstan/phpstan": "^1.10" }, "type": "library", "extra": { @@ -1955,20 +2018,20 @@ "issues": "https://github.com/laravel/sanctum/issues", "source": "https://github.com/laravel/sanctum" }, - "time": "2025-07-09T19:45:24+00:00" + "time": "2026-01-06T23:11:51+00:00" }, { "name": "laravel/serializable-closure", - "version": "v2.0.6", + "version": "v2.0.7", "source": { "type": "git", "url": "https://github.com/laravel/serializable-closure.git", - "reference": "038ce42edee619599a1debb7e81d7b3759492819" + "reference": "cb291e4c998ac50637c7eeb58189c14f5de5b9dd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/serializable-closure/zipball/038ce42edee619599a1debb7e81d7b3759492819", - "reference": "038ce42edee619599a1debb7e81d7b3759492819", + "url": "https://api.github.com/repos/laravel/serializable-closure/zipball/cb291e4c998ac50637c7eeb58189c14f5de5b9dd", + "reference": "cb291e4c998ac50637c7eeb58189c14f5de5b9dd", "shasum": "" }, "require": { @@ -1977,7 +2040,7 @@ "require-dev": { "illuminate/support": "^10.0|^11.0|^12.0", "nesbot/carbon": "^2.67|^3.0", - "pestphp/pest": "^2.36|^3.0", + "pestphp/pest": "^2.36|^3.0|^4.0", "phpstan/phpstan": "^2.0", "symfony/var-dumper": "^6.2.0|^7.0.0" }, @@ -2016,25 +2079,25 @@ "issues": "https://github.com/laravel/serializable-closure/issues", "source": "https://github.com/laravel/serializable-closure" }, - "time": "2025-10-09T13:42:30+00:00" + "time": "2025-11-21T20:52:36+00:00" }, { "name": "laravel/socialite", - "version": "v5.23.1", + "version": "v5.24.1", "source": { "type": "git", "url": "https://github.com/laravel/socialite.git", - "reference": "83d7523c97c1101d288126948947891319eef800" + "reference": "25e28c14d55404886777af1d77cf030e0f633142" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/socialite/zipball/83d7523c97c1101d288126948947891319eef800", - "reference": "83d7523c97c1101d288126948947891319eef800", + "url": "https://api.github.com/repos/laravel/socialite/zipball/25e28c14d55404886777af1d77cf030e0f633142", + "reference": "25e28c14d55404886777af1d77cf030e0f633142", "shasum": "" }, "require": { "ext-json": "*", - "firebase/php-jwt": "^6.4", + "firebase/php-jwt": "^6.4|^7.0", "guzzlehttp/guzzle": "^6.0|^7.0", "illuminate/contracts": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0", "illuminate/http": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0", @@ -2045,9 +2108,9 @@ }, "require-dev": { "mockery/mockery": "^1.0", - "orchestra/testbench": "^4.0|^5.0|^6.0|^7.0|^8.0|^9.0|^10.0", + "orchestra/testbench": "^4.18|^5.20|^6.47|^7.55|^8.36|^9.15|^10.8", "phpstan/phpstan": "^1.12.23", - "phpunit/phpunit": "^8.0|^9.3|^10.4|^11.5" + "phpunit/phpunit": "^8.0|^9.3|^10.4|^11.5|^12.0" }, "type": "library", "extra": { @@ -2088,20 +2151,20 @@ "issues": "https://github.com/laravel/socialite/issues", "source": "https://github.com/laravel/socialite" }, - "time": "2025-10-27T15:36:41+00:00" + "time": "2026-01-01T02:57:21+00:00" }, { "name": "laravel/tinker", - "version": "v2.10.1", + "version": "v2.11.0", "source": { "type": "git", "url": "https://github.com/laravel/tinker.git", - "reference": "22177cc71807d38f2810c6204d8f7183d88a57d3" + "reference": "3d34b97c9a1747a81a3fde90482c092bd8b66468" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/tinker/zipball/22177cc71807d38f2810c6204d8f7183d88a57d3", - "reference": "22177cc71807d38f2810c6204d8f7183d88a57d3", + "url": "https://api.github.com/repos/laravel/tinker/zipball/3d34b97c9a1747a81a3fde90482c092bd8b66468", + "reference": "3d34b97c9a1747a81a3fde90482c092bd8b66468", "shasum": "" }, "require": { @@ -2110,7 +2173,7 @@ "illuminate/support": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0", "php": "^7.2.5|^8.0", "psy/psysh": "^0.11.1|^0.12.0", - "symfony/var-dumper": "^4.3.4|^5.0|^6.0|^7.0" + "symfony/var-dumper": "^4.3.4|^5.0|^6.0|^7.0|^8.0" }, "require-dev": { "mockery/mockery": "~1.3.3|^1.4.2", @@ -2152,22 +2215,22 @@ ], "support": { "issues": "https://github.com/laravel/tinker/issues", - "source": "https://github.com/laravel/tinker/tree/v2.10.1" + "source": "https://github.com/laravel/tinker/tree/v2.11.0" }, - "time": "2025-01-27T14:24:01+00:00" + "time": "2025-12-19T19:16:45+00:00" }, { "name": "league/commonmark", - "version": "2.7.1", + "version": "2.8.0", "source": { "type": "git", "url": "https://github.com/thephpleague/commonmark.git", - "reference": "10732241927d3971d28e7ea7b5712721fa2296ca" + "reference": "4efa10c1e56488e658d10adf7b7b7dcd19940bfb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/10732241927d3971d28e7ea7b5712721fa2296ca", - "reference": "10732241927d3971d28e7ea7b5712721fa2296ca", + "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/4efa10c1e56488e658d10adf7b7b7dcd19940bfb", + "reference": "4efa10c1e56488e658d10adf7b7b7dcd19940bfb", "shasum": "" }, "require": { @@ -2204,7 +2267,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "2.8-dev" + "dev-main": "2.9-dev" } }, "autoload": { @@ -2261,7 +2324,7 @@ "type": "tidelift" } ], - "time": "2025-07-20T12:47:49+00:00" + "time": "2025-11-26T21:48:24+00:00" }, { "name": "league/config", @@ -2347,16 +2410,16 @@ }, { "name": "league/flysystem", - "version": "3.30.1", + "version": "3.30.2", "source": { "type": "git", "url": "https://github.com/thephpleague/flysystem.git", - "reference": "c139fd65c1f796b926f4aec0df37f6caa959a8da" + "reference": "5966a8ba23e62bdb518dd9e0e665c2dbd4b5b277" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/c139fd65c1f796b926f4aec0df37f6caa959a8da", - "reference": "c139fd65c1f796b926f4aec0df37f6caa959a8da", + "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/5966a8ba23e62bdb518dd9e0e665c2dbd4b5b277", + "reference": "5966a8ba23e62bdb518dd9e0e665c2dbd4b5b277", "shasum": "" }, "require": { @@ -2424,22 +2487,22 @@ ], "support": { "issues": "https://github.com/thephpleague/flysystem/issues", - "source": "https://github.com/thephpleague/flysystem/tree/3.30.1" + "source": "https://github.com/thephpleague/flysystem/tree/3.30.2" }, - "time": "2025-10-20T15:35:26+00:00" + "time": "2025-11-10T17:13:11+00:00" }, { "name": "league/flysystem-local", - "version": "3.30.0", + "version": "3.30.2", "source": { "type": "git", "url": "https://github.com/thephpleague/flysystem-local.git", - "reference": "6691915f77c7fb69adfb87dcd550052dc184ee10" + "reference": "ab4f9d0d672f601b102936aa728801dd1a11968d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/flysystem-local/zipball/6691915f77c7fb69adfb87dcd550052dc184ee10", - "reference": "6691915f77c7fb69adfb87dcd550052dc184ee10", + "url": "https://api.github.com/repos/thephpleague/flysystem-local/zipball/ab4f9d0d672f601b102936aa728801dd1a11968d", + "reference": "ab4f9d0d672f601b102936aa728801dd1a11968d", "shasum": "" }, "require": { @@ -2473,9 +2536,9 @@ "local" ], "support": { - "source": "https://github.com/thephpleague/flysystem-local/tree/3.30.0" + "source": "https://github.com/thephpleague/flysystem-local/tree/3.30.2" }, - "time": "2025-05-21T10:34:19+00:00" + "time": "2025-11-10T11:23:37+00:00" }, { "name": "league/mime-type-detection", @@ -2667,33 +2730,38 @@ }, { "name": "league/uri", - "version": "7.5.1", + "version": "7.7.0", "source": { "type": "git", "url": "https://github.com/thephpleague/uri.git", - "reference": "81fb5145d2644324614cc532b28efd0215bda430" + "reference": "8d587cddee53490f9b82bf203d3a9aa7ea4f9807" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/uri/zipball/81fb5145d2644324614cc532b28efd0215bda430", - "reference": "81fb5145d2644324614cc532b28efd0215bda430", + "url": "https://api.github.com/repos/thephpleague/uri/zipball/8d587cddee53490f9b82bf203d3a9aa7ea4f9807", + "reference": "8d587cddee53490f9b82bf203d3a9aa7ea4f9807", "shasum": "" }, "require": { - "league/uri-interfaces": "^7.5", - "php": "^8.1" + "league/uri-interfaces": "^7.7", + "php": "^8.1", + "psr/http-factory": "^1" }, "conflict": { "league/uri-schemes": "^1.0" }, "suggest": { "ext-bcmath": "to improve IPV4 host parsing", + "ext-dom": "to convert the URI into an HTML anchor tag", "ext-fileinfo": "to create Data URI from file contennts", "ext-gmp": "to improve IPV4 host parsing", "ext-intl": "to handle IDN host with the best performance", + "ext-uri": "to use the PHP native URI class", "jeremykendall/php-domain-parser": "to resolve Public Suffix and Top Level Domain", "league/uri-components": "Needed to easily manipulate URI objects components", + "league/uri-polyfill": "Needed to backport the PHP URI extension for older versions of PHP", "php-64bit": "to improve IPV4 host parsing", + "rowbot/url": "to handle WHATWG URL", "symfony/polyfill-intl-idn": "to handle IDN host via the Symfony polyfill if ext-intl is not present" }, "type": "library", @@ -2721,6 +2789,7 @@ "description": "URI manipulation library", "homepage": "https://uri.thephpleague.com", "keywords": [ + "URN", "data-uri", "file-uri", "ftp", @@ -2733,9 +2802,11 @@ "psr-7", "query-string", "querystring", + "rfc2141", "rfc3986", "rfc3987", "rfc6570", + "rfc8141", "uri", "uri-template", "url", @@ -2745,7 +2816,7 @@ "docs": "https://uri.thephpleague.com", "forum": "https://thephpleague.slack.com", "issues": "https://github.com/thephpleague/uri-src/issues", - "source": "https://github.com/thephpleague/uri/tree/7.5.1" + "source": "https://github.com/thephpleague/uri/tree/7.7.0" }, "funding": [ { @@ -2753,26 +2824,25 @@ "type": "github" } ], - "time": "2024-12-08T08:40:02+00:00" + "time": "2025-12-07T16:02:06+00:00" }, { "name": "league/uri-interfaces", - "version": "7.5.0", + "version": "7.7.0", "source": { "type": "git", "url": "https://github.com/thephpleague/uri-interfaces.git", - "reference": "08cfc6c4f3d811584fb09c37e2849e6a7f9b0742" + "reference": "62ccc1a0435e1c54e10ee6022df28d6c04c2946c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/uri-interfaces/zipball/08cfc6c4f3d811584fb09c37e2849e6a7f9b0742", - "reference": "08cfc6c4f3d811584fb09c37e2849e6a7f9b0742", + "url": "https://api.github.com/repos/thephpleague/uri-interfaces/zipball/62ccc1a0435e1c54e10ee6022df28d6c04c2946c", + "reference": "62ccc1a0435e1c54e10ee6022df28d6c04c2946c", "shasum": "" }, "require": { "ext-filter": "*", "php": "^8.1", - "psr/http-factory": "^1", "psr/http-message": "^1.1 || ^2.0" }, "suggest": { @@ -2780,6 +2850,7 @@ "ext-gmp": "to improve IPV4 host parsing", "ext-intl": "to handle IDN host with the best performance", "php-64bit": "to improve IPV4 host parsing", + "rowbot/url": "to handle WHATWG URL", "symfony/polyfill-intl-idn": "to handle IDN host via the Symfony polyfill if ext-intl is not present" }, "type": "library", @@ -2804,7 +2875,7 @@ "homepage": "https://nyamsprod.com" } ], - "description": "Common interfaces and classes for URI representation and interaction", + "description": "Common tools for parsing and resolving RFC3987/RFC3986 URI", "homepage": "https://uri.thephpleague.com", "keywords": [ "data-uri", @@ -2829,7 +2900,7 @@ "docs": "https://uri.thephpleague.com", "forum": "https://thephpleague.slack.com", "issues": "https://github.com/thephpleague/uri-src/issues", - "source": "https://github.com/thephpleague/uri-interfaces/tree/7.5.0" + "source": "https://github.com/thephpleague/uri-interfaces/tree/7.7.0" }, "funding": [ { @@ -2837,20 +2908,20 @@ "type": "github" } ], - "time": "2024-12-08T08:18:47+00:00" + "time": "2025-12-07T16:03:21+00:00" }, { "name": "livewire/flux", - "version": "v2.6.1", + "version": "v2.10.2", "source": { "type": "git", "url": "https://github.com/livewire/flux.git", - "reference": "227b88db0a02db91666af2303ea6727a3af78c51" + "reference": "e7a93989788429bb6c0a908a056d22ea3a6c7975" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/livewire/flux/zipball/227b88db0a02db91666af2303ea6727a3af78c51", - "reference": "227b88db0a02db91666af2303ea6727a3af78c51", + "url": "https://api.github.com/repos/livewire/flux/zipball/e7a93989788429bb6c0a908a056d22ea3a6c7975", + "reference": "e7a93989788429bb6c0a908a056d22ea3a6c7975", "shasum": "" }, "require": { @@ -2858,12 +2929,12 @@ "illuminate/support": "^10.0|^11.0|^12.0", "illuminate/view": "^10.0|^11.0|^12.0", "laravel/prompts": "^0.1|^0.2|^0.3", - "livewire/livewire": "^3.5.19|^4.0", + "livewire/livewire": "^3.7.3|^4.0", "php": "^8.1", "symfony/console": "^6.0|^7.0" }, "conflict": { - "livewire/blaze": "<0.1.0" + "livewire/blaze": "<1.0.0" }, "type": "library", "extra": { @@ -2901,22 +2972,22 @@ ], "support": { "issues": "https://github.com/livewire/flux/issues", - "source": "https://github.com/livewire/flux/tree/v2.6.1" + "source": "https://github.com/livewire/flux/tree/v2.10.2" }, - "time": "2025-10-28T21:12:05+00:00" + "time": "2025-12-19T02:11:45+00:00" }, { "name": "livewire/livewire", - "version": "v3.6.4", + "version": "v3.7.3", "source": { "type": "git", "url": "https://github.com/livewire/livewire.git", - "reference": "ef04be759da41b14d2d129e670533180a44987dc" + "reference": "a5384df9fbd3eaf02e053bc49aabc8ace293fc1c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/livewire/livewire/zipball/ef04be759da41b14d2d129e670533180a44987dc", - "reference": "ef04be759da41b14d2d129e670533180a44987dc", + "url": "https://api.github.com/repos/livewire/livewire/zipball/a5384df9fbd3eaf02e053bc49aabc8ace293fc1c", + "reference": "a5384df9fbd3eaf02e053bc49aabc8ace293fc1c", "shasum": "" }, "require": { @@ -2971,7 +3042,7 @@ "description": "A front-end framework for Laravel.", "support": { "issues": "https://github.com/livewire/livewire/issues", - "source": "https://github.com/livewire/livewire/tree/v3.6.4" + "source": "https://github.com/livewire/livewire/tree/v3.7.3" }, "funding": [ { @@ -2979,20 +3050,20 @@ "type": "github" } ], - "time": "2025-07-17T05:12:15+00:00" + "time": "2025-12-19T02:00:29+00:00" }, { "name": "livewire/volt", - "version": "v1.9.0", + "version": "v1.10.1", "source": { "type": "git", "url": "https://github.com/livewire/volt.git", - "reference": "4b289eef2f15398987a923d9f813cad6a6a19ea4" + "reference": "48cff133990c6261c63ee279fc091af6f6c6654e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/livewire/volt/zipball/4b289eef2f15398987a923d9f813cad6a6a19ea4", - "reference": "4b289eef2f15398987a923d9f813cad6a6a19ea4", + "url": "https://api.github.com/repos/livewire/volt/zipball/48cff133990c6261c63ee279fc091af6f6c6654e", + "reference": "48cff133990c6261c63ee279fc091af6f6c6654e", "shasum": "" }, "require": { @@ -3002,9 +3073,8 @@ }, "require-dev": { "laravel/folio": "^1.1", - "mockery/mockery": "^1.6", - "orchestra/testbench": "^8.15.0|^9.0|^10.0", - "pestphp/pest": "^2.9.5|^3.0", + "orchestra/testbench": "^8.36|^9.15|^10.8", + "pestphp/pest": "^2.9.5|^3.0|^4.0", "phpstan/phpstan": "^1.10" }, "type": "library", @@ -3051,20 +3121,20 @@ "issues": "https://github.com/livewire/volt/issues", "source": "https://github.com/livewire/volt" }, - "time": "2025-10-30T02:46:00+00:00" + "time": "2025-11-25T16:19:15+00:00" }, { "name": "maennchen/zipstream-php", - "version": "3.2.0", + "version": "3.2.1", "source": { "type": "git", "url": "https://github.com/maennchen/ZipStream-PHP.git", - "reference": "9712d8fa4cdf9240380b01eb4be55ad8dcf71416" + "reference": "682f1098a8fddbaf43edac2306a691c7ad508ec5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/maennchen/ZipStream-PHP/zipball/9712d8fa4cdf9240380b01eb4be55ad8dcf71416", - "reference": "9712d8fa4cdf9240380b01eb4be55ad8dcf71416", + "url": "https://api.github.com/repos/maennchen/ZipStream-PHP/zipball/682f1098a8fddbaf43edac2306a691c7ad508ec5", + "reference": "682f1098a8fddbaf43edac2306a691c7ad508ec5", "shasum": "" }, "require": { @@ -3075,7 +3145,7 @@ "require-dev": { "brianium/paratest": "^7.7", "ext-zip": "*", - "friendsofphp/php-cs-fixer": "^3.16", + "friendsofphp/php-cs-fixer": "^3.86", "guzzlehttp/guzzle": "^7.5", "mikey179/vfsstream": "^1.6", "php-coveralls/php-coveralls": "^2.5", @@ -3121,7 +3191,7 @@ ], "support": { "issues": "https://github.com/maennchen/ZipStream-PHP/issues", - "source": "https://github.com/maennchen/ZipStream-PHP/tree/3.2.0" + "source": "https://github.com/maennchen/ZipStream-PHP/tree/3.2.1" }, "funding": [ { @@ -3129,20 +3199,20 @@ "type": "github" } ], - "time": "2025-07-17T11:15:13+00:00" + "time": "2025-12-10T09:58:31+00:00" }, { "name": "monolog/monolog", - "version": "3.9.0", + "version": "3.10.0", "source": { "type": "git", "url": "https://github.com/Seldaek/monolog.git", - "reference": "10d85740180ecba7896c87e06a166e0c95a0e3b6" + "reference": "b321dd6749f0bf7189444158a3ce785cc16d69b0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Seldaek/monolog/zipball/10d85740180ecba7896c87e06a166e0c95a0e3b6", - "reference": "10d85740180ecba7896c87e06a166e0c95a0e3b6", + "url": "https://api.github.com/repos/Seldaek/monolog/zipball/b321dd6749f0bf7189444158a3ce785cc16d69b0", + "reference": "b321dd6749f0bf7189444158a3ce785cc16d69b0", "shasum": "" }, "require": { @@ -3160,7 +3230,7 @@ "graylog2/gelf-php": "^1.4.2 || ^2.0", "guzzlehttp/guzzle": "^7.4.5", "guzzlehttp/psr7": "^2.2", - "mongodb/mongodb": "^1.8", + "mongodb/mongodb": "^1.8 || ^2.0", "php-amqplib/php-amqplib": "~2.4 || ^3", "php-console/php-console": "^3.1.8", "phpstan/phpstan": "^2", @@ -3220,7 +3290,7 @@ ], "support": { "issues": "https://github.com/Seldaek/monolog/issues", - "source": "https://github.com/Seldaek/monolog/tree/3.9.0" + "source": "https://github.com/Seldaek/monolog/tree/3.10.0" }, "funding": [ { @@ -3232,7 +3302,7 @@ "type": "tidelift" } ], - "time": "2025-03-24T10:02:05+00:00" + "time": "2026-01-02T08:56:05+00:00" }, { "name": "mtdowling/jmespath.php", @@ -3302,16 +3372,16 @@ }, { "name": "nesbot/carbon", - "version": "3.10.3", + "version": "3.11.0", "source": { "type": "git", "url": "https://github.com/CarbonPHP/carbon.git", - "reference": "8e3643dcd149ae0fe1d2ff4f2c8e4bbfad7c165f" + "reference": "bdb375400dcd162624531666db4799b36b64e4a1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/CarbonPHP/carbon/zipball/8e3643dcd149ae0fe1d2ff4f2c8e4bbfad7c165f", - "reference": "8e3643dcd149ae0fe1d2ff4f2c8e4bbfad7c165f", + "url": "https://api.github.com/repos/CarbonPHP/carbon/zipball/bdb375400dcd162624531666db4799b36b64e4a1", + "reference": "bdb375400dcd162624531666db4799b36b64e4a1", "shasum": "" }, "require": { @@ -3319,9 +3389,9 @@ "ext-json": "*", "php": "^8.1", "psr/clock": "^1.0", - "symfony/clock": "^6.3.12 || ^7.0", + "symfony/clock": "^6.3.12 || ^7.0 || ^8.0", "symfony/polyfill-mbstring": "^1.0", - "symfony/translation": "^4.4.18 || ^5.2.1 || ^6.0 || ^7.0" + "symfony/translation": "^4.4.18 || ^5.2.1 || ^6.0 || ^7.0 || ^8.0" }, "provide": { "psr/clock-implementation": "1.0" @@ -3403,7 +3473,7 @@ "type": "tidelift" } ], - "time": "2025-09-06T13:39:36+00:00" + "time": "2025-12-02T21:04:28+00:00" }, { "name": "nette/schema", @@ -3472,20 +3542,20 @@ }, { "name": "nette/utils", - "version": "v4.0.8", + "version": "v4.1.1", "source": { "type": "git", "url": "https://github.com/nette/utils.git", - "reference": "c930ca4e3cf4f17dcfb03037703679d2396d2ede" + "reference": "c99059c0315591f1a0db7ad6002000288ab8dc72" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nette/utils/zipball/c930ca4e3cf4f17dcfb03037703679d2396d2ede", - "reference": "c930ca4e3cf4f17dcfb03037703679d2396d2ede", + "url": "https://api.github.com/repos/nette/utils/zipball/c99059c0315591f1a0db7ad6002000288ab8dc72", + "reference": "c99059c0315591f1a0db7ad6002000288ab8dc72", "shasum": "" }, "require": { - "php": "8.0 - 8.5" + "php": "8.2 - 8.5" }, "conflict": { "nette/finder": "<3", @@ -3508,7 +3578,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "4.0-dev" + "dev-master": "4.1-dev" } }, "autoload": { @@ -3555,22 +3625,22 @@ ], "support": { "issues": "https://github.com/nette/utils/issues", - "source": "https://github.com/nette/utils/tree/v4.0.8" + "source": "https://github.com/nette/utils/tree/v4.1.1" }, - "time": "2025-08-06T21:43:34+00:00" + "time": "2025-12-22T12:14:32+00:00" }, { "name": "nikic/php-parser", - "version": "v5.6.2", + "version": "v5.7.0", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "3a454ca033b9e06b63282ce19562e892747449bb" + "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/3a454ca033b9e06b63282ce19562e892747449bb", - "reference": "3a454ca033b9e06b63282ce19562e892747449bb", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/dca41cd15c2ac9d055ad70dbfd011130757d1f82", + "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82", "shasum": "" }, "require": { @@ -3613,37 +3683,37 @@ ], "support": { "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v5.6.2" + "source": "https://github.com/nikic/PHP-Parser/tree/v5.7.0" }, - "time": "2025-10-21T19:32:17+00:00" + "time": "2025-12-06T11:56:16+00:00" }, { "name": "nunomaduro/termwind", - "version": "v2.3.2", + "version": "v2.3.3", "source": { "type": "git", "url": "https://github.com/nunomaduro/termwind.git", - "reference": "eb61920a53057a7debd718a5b89c2178032b52c0" + "reference": "6fb2a640ff502caace8e05fd7be3b503a7e1c017" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nunomaduro/termwind/zipball/eb61920a53057a7debd718a5b89c2178032b52c0", - "reference": "eb61920a53057a7debd718a5b89c2178032b52c0", + "url": "https://api.github.com/repos/nunomaduro/termwind/zipball/6fb2a640ff502caace8e05fd7be3b503a7e1c017", + "reference": "6fb2a640ff502caace8e05fd7be3b503a7e1c017", "shasum": "" }, "require": { "ext-mbstring": "*", "php": "^8.2", - "symfony/console": "^7.3.4" + "symfony/console": "^7.3.6" }, "require-dev": { "illuminate/console": "^11.46.1", "laravel/pint": "^1.25.1", "mockery/mockery": "^1.6.12", - "pestphp/pest": "^2.36.0 || ^3.8.4", + "pestphp/pest": "^2.36.0 || ^3.8.4 || ^4.1.3", "phpstan/phpstan": "^1.12.32", "phpstan/phpstan-strict-rules": "^1.6.2", - "symfony/var-dumper": "^7.3.4", + "symfony/var-dumper": "^7.3.5", "thecodingmachine/phpstan-strict-rules": "^1.0.0" }, "type": "library", @@ -3686,7 +3756,7 @@ ], "support": { "issues": "https://github.com/nunomaduro/termwind/issues", - "source": "https://github.com/nunomaduro/termwind/tree/v2.3.2" + "source": "https://github.com/nunomaduro/termwind/tree/v2.3.3" }, "funding": [ { @@ -3702,7 +3772,58 @@ "type": "github" } ], - "time": "2025-10-18T11:10:27+00:00" + "time": "2025-11-20T02:34:59+00:00" + }, + { + "name": "om/icalparser", + "version": "v3.2.1", + "source": { + "type": "git", + "url": "https://github.com/OzzyCzech/icalparser.git", + "reference": "bc7a82b12455ae9b62ce8e7f2d0273e86c931ecc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/OzzyCzech/icalparser/zipball/bc7a82b12455ae9b62ce8e7f2d0273e86c931ecc", + "reference": "bc7a82b12455ae9b62ce8e7f2d0273e86c931ecc", + "shasum": "" + }, + "require": { + "php": ">=8.1.0" + }, + "require-dev": { + "nette/tester": "^2.5.7" + }, + "suggest": { + "ext-dom": "for timezone tool" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Roman Oลพana", + "email": "roman@ozana.cz" + } + ], + "description": "Simple iCal parser", + "keywords": [ + "calendar", + "ical", + "parser" + ], + "support": { + "issues": "https://github.com/OzzyCzech/icalparser/issues", + "source": "https://github.com/OzzyCzech/icalparser/tree/v3.2.1" + }, + "time": "2025-12-15T06:25:09+00:00" }, { "name": "paragonie/constant_time_encoding", @@ -3825,16 +3946,16 @@ }, { "name": "phpoption/phpoption", - "version": "1.9.4", + "version": "1.9.5", "source": { "type": "git", "url": "https://github.com/schmittjoh/php-option.git", - "reference": "638a154f8d4ee6a5cfa96d6a34dfbe0cffa9566d" + "reference": "75365b91986c2405cf5e1e012c5595cd487a98be" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/schmittjoh/php-option/zipball/638a154f8d4ee6a5cfa96d6a34dfbe0cffa9566d", - "reference": "638a154f8d4ee6a5cfa96d6a34dfbe0cffa9566d", + "url": "https://api.github.com/repos/schmittjoh/php-option/zipball/75365b91986c2405cf5e1e012c5595cd487a98be", + "reference": "75365b91986c2405cf5e1e012c5595cd487a98be", "shasum": "" }, "require": { @@ -3884,7 +4005,7 @@ ], "support": { "issues": "https://github.com/schmittjoh/php-option/issues", - "source": "https://github.com/schmittjoh/php-option/tree/1.9.4" + "source": "https://github.com/schmittjoh/php-option/tree/1.9.5" }, "funding": [ { @@ -3896,20 +4017,20 @@ "type": "tidelift" } ], - "time": "2025-08-21T11:53:16+00:00" + "time": "2025-12-27T19:41:33+00:00" }, { "name": "phpseclib/phpseclib", - "version": "3.0.47", + "version": "3.0.48", "source": { "type": "git", "url": "https://github.com/phpseclib/phpseclib.git", - "reference": "9d6ca36a6c2dd434765b1071b2644a1c683b385d" + "reference": "64065a5679c50acb886e82c07aa139b0f757bb89" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/9d6ca36a6c2dd434765b1071b2644a1c683b385d", - "reference": "9d6ca36a6c2dd434765b1071b2644a1c683b385d", + "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/64065a5679c50acb886e82c07aa139b0f757bb89", + "reference": "64065a5679c50acb886e82c07aa139b0f757bb89", "shasum": "" }, "require": { @@ -3990,7 +4111,7 @@ ], "support": { "issues": "https://github.com/phpseclib/phpseclib/issues", - "source": "https://github.com/phpseclib/phpseclib/tree/3.0.47" + "source": "https://github.com/phpseclib/phpseclib/tree/3.0.48" }, "funding": [ { @@ -4006,7 +4127,7 @@ "type": "tidelift" } ], - "time": "2025-10-06T01:07:24+00:00" + "time": "2025-12-15T11:51:42+00:00" }, { "name": "psr/clock", @@ -4422,16 +4543,16 @@ }, { "name": "psy/psysh", - "version": "v0.12.14", + "version": "v0.12.18", "source": { "type": "git", "url": "https://github.com/bobthecow/psysh.git", - "reference": "95c29b3756a23855a30566b745d218bee690bef2" + "reference": "ddff0ac01beddc251786fe70367cd8bbdb258196" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/bobthecow/psysh/zipball/95c29b3756a23855a30566b745d218bee690bef2", - "reference": "95c29b3756a23855a30566b745d218bee690bef2", + "url": "https://api.github.com/repos/bobthecow/psysh/zipball/ddff0ac01beddc251786fe70367cd8bbdb258196", + "reference": "ddff0ac01beddc251786fe70367cd8bbdb258196", "shasum": "" }, "require": { @@ -4439,8 +4560,8 @@ "ext-tokenizer": "*", "nikic/php-parser": "^5.0 || ^4.0", "php": "^8.0 || ^7.4", - "symfony/console": "^7.0 || ^6.0 || ^5.0 || ^4.0 || ^3.4", - "symfony/var-dumper": "^7.0 || ^6.0 || ^5.0 || ^4.0 || ^3.4" + "symfony/console": "^8.0 || ^7.0 || ^6.0 || ^5.0 || ^4.0 || ^3.4", + "symfony/var-dumper": "^8.0 || ^7.0 || ^6.0 || ^5.0 || ^4.0 || ^3.4" }, "conflict": { "symfony/console": "4.4.37 || 5.3.14 || 5.3.15 || 5.4.3 || 5.4.4 || 6.0.3 || 6.0.4" @@ -4495,9 +4616,9 @@ ], "support": { "issues": "https://github.com/bobthecow/psysh/issues", - "source": "https://github.com/bobthecow/psysh/tree/v0.12.14" + "source": "https://github.com/bobthecow/psysh/tree/v0.12.18" }, - "time": "2025-10-27T17:15:31+00:00" + "time": "2025-12-17T14:35:46+00:00" }, { "name": "ralouphie/getallheaders", @@ -4621,20 +4742,20 @@ }, { "name": "ramsey/uuid", - "version": "4.9.1", + "version": "4.9.2", "source": { "type": "git", "url": "https://github.com/ramsey/uuid.git", - "reference": "81f941f6f729b1e3ceea61d9d014f8b6c6800440" + "reference": "8429c78ca35a09f27565311b98101e2826affde0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/ramsey/uuid/zipball/81f941f6f729b1e3ceea61d9d014f8b6c6800440", - "reference": "81f941f6f729b1e3ceea61d9d014f8b6c6800440", + "url": "https://api.github.com/repos/ramsey/uuid/zipball/8429c78ca35a09f27565311b98101e2826affde0", + "reference": "8429c78ca35a09f27565311b98101e2826affde0", "shasum": "" }, "require": { - "brick/math": "^0.8.8 || ^0.9 || ^0.10 || ^0.11 || ^0.12 || ^0.13 || ^0.14", + "brick/math": "^0.8.16 || ^0.9 || ^0.10 || ^0.11 || ^0.12 || ^0.13 || ^0.14", "php": "^8.0", "ramsey/collection": "^1.2 || ^2.0" }, @@ -4693,22 +4814,22 @@ ], "support": { "issues": "https://github.com/ramsey/uuid/issues", - "source": "https://github.com/ramsey/uuid/tree/4.9.1" + "source": "https://github.com/ramsey/uuid/tree/4.9.2" }, - "time": "2025-09-04T20:59:21+00:00" + "time": "2025-12-14T04:43:48+00:00" }, { "name": "spatie/browsershot", - "version": "5.0.11", + "version": "5.2.0", "source": { "type": "git", "url": "https://github.com/spatie/browsershot.git", - "reference": "f84d9c332899596d0884922772593a10e3925969" + "reference": "9bc6b8d67175810d7a399b2588c3401efe2d02a8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/spatie/browsershot/zipball/f84d9c332899596d0884922772593a10e3925969", - "reference": "f84d9c332899596d0884922772593a10e3925969", + "url": "https://api.github.com/repos/spatie/browsershot/zipball/9bc6b8d67175810d7a399b2588c3401efe2d02a8", + "reference": "9bc6b8d67175810d7a399b2588c3401efe2d02a8", "shasum": "" }, "require": { @@ -4716,13 +4837,13 @@ "ext-json": "*", "php": "^8.2", "spatie/temporary-directory": "^2.0", - "symfony/process": "^6.0|^7.0" + "symfony/process": "^6.0|^7.0|^8.0" }, "require-dev": { - "pestphp/pest": "^3.0", + "pestphp/pest": "^3.0|^4.0", "spatie/image": "^3.6", "spatie/pdf-to-text": "^1.52", - "spatie/phpunit-snapshot-assertions": "^4.2.3|^5.0" + "spatie/phpunit-snapshot-assertions": "^5.0" }, "type": "library", "autoload": { @@ -4755,7 +4876,7 @@ "webpage" ], "support": { - "source": "https://github.com/spatie/browsershot/tree/5.0.11" + "source": "https://github.com/spatie/browsershot/tree/5.2.0" }, "funding": [ { @@ -4763,7 +4884,7 @@ "type": "github" } ], - "time": "2025-10-08T07:40:52+00:00" + "time": "2025-12-22T10:02:16+00:00" }, { "name": "spatie/laravel-package-tools", @@ -4888,23 +5009,88 @@ "time": "2025-01-13T13:04:43+00:00" }, { - "name": "symfony/clock", - "version": "v7.3.0", + "name": "stevebauman/purify", + "version": "v6.3.1", "source": { "type": "git", - "url": "https://github.com/symfony/clock.git", - "reference": "b81435fbd6648ea425d1ee96a2d8e68f4ceacd24" + "url": "https://github.com/stevebauman/purify.git", + "reference": "3acb5e77904f420ce8aad8fa1c7f394e82daa500" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/clock/zipball/b81435fbd6648ea425d1ee96a2d8e68f4ceacd24", - "reference": "b81435fbd6648ea425d1ee96a2d8e68f4ceacd24", + "url": "https://api.github.com/repos/stevebauman/purify/zipball/3acb5e77904f420ce8aad8fa1c7f394e82daa500", + "reference": "3acb5e77904f420ce8aad8fa1c7f394e82daa500", "shasum": "" }, "require": { - "php": ">=8.2", - "psr/clock": "^1.0", - "symfony/polyfill-php83": "^1.28" + "ezyang/htmlpurifier": "^4.17", + "illuminate/contracts": "^7.0|^8.0|^9.0|^10.0|^11.0|^12.0", + "illuminate/support": "^7.0|^8.0|^9.0|^10.0|^11.0|^12.0", + "php": ">=7.4" + }, + "require-dev": { + "orchestra/testbench": "^5.0|^6.0|^7.0|^8.0|^9.0|^10.0", + "phpunit/phpunit": "^8.0|^9.0|^10.0|^11.5.3" + }, + "type": "library", + "extra": { + "laravel": { + "aliases": { + "Purify": "Stevebauman\\Purify\\Facades\\Purify" + }, + "providers": [ + "Stevebauman\\Purify\\PurifyServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "Stevebauman\\Purify\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Steve Bauman", + "email": "steven_bauman@outlook.com" + } + ], + "description": "An HTML Purifier / Sanitizer for Laravel", + "keywords": [ + "Purifier", + "clean", + "cleaner", + "html", + "laravel", + "purification", + "purify" + ], + "support": { + "issues": "https://github.com/stevebauman/purify/issues", + "source": "https://github.com/stevebauman/purify/tree/v6.3.1" + }, + "time": "2025-05-21T16:53:09+00:00" + }, + { + "name": "symfony/clock", + "version": "v8.0.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/clock.git", + "reference": "832119f9b8dbc6c8e6f65f30c5969eca1e88764f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/clock/zipball/832119f9b8dbc6c8e6f65f30c5969eca1e88764f", + "reference": "832119f9b8dbc6c8e6f65f30c5969eca1e88764f", + "shasum": "" + }, + "require": { + "php": ">=8.4", + "psr/clock": "^1.0" }, "provide": { "psr/clock-implementation": "1.0" @@ -4943,7 +5129,7 @@ "time" ], "support": { - "source": "https://github.com/symfony/clock/tree/v7.3.0" + "source": "https://github.com/symfony/clock/tree/v8.0.0" }, "funding": [ { @@ -4954,25 +5140,29 @@ "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-25T14:21:43+00:00" + "time": "2025-11-12T15:46:48+00:00" }, { "name": "symfony/console", - "version": "v7.3.5", + "version": "v7.4.3", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "cdb80fa5869653c83cfe1a9084a673b6daf57ea7" + "reference": "732a9ca6cd9dfd940c639062d5edbde2f6727fb6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/cdb80fa5869653c83cfe1a9084a673b6daf57ea7", - "reference": "cdb80fa5869653c83cfe1a9084a673b6daf57ea7", + "url": "https://api.github.com/repos/symfony/console/zipball/732a9ca6cd9dfd940c639062d5edbde2f6727fb6", + "reference": "732a9ca6cd9dfd940c639062d5edbde2f6727fb6", "shasum": "" }, "require": { @@ -4980,7 +5170,7 @@ "symfony/deprecation-contracts": "^2.5|^3", "symfony/polyfill-mbstring": "~1.0", "symfony/service-contracts": "^2.5|^3", - "symfony/string": "^7.2" + "symfony/string": "^7.2|^8.0" }, "conflict": { "symfony/dependency-injection": "<6.4", @@ -4994,16 +5184,16 @@ }, "require-dev": { "psr/log": "^1|^2|^3", - "symfony/config": "^6.4|^7.0", - "symfony/dependency-injection": "^6.4|^7.0", - "symfony/event-dispatcher": "^6.4|^7.0", - "symfony/http-foundation": "^6.4|^7.0", - "symfony/http-kernel": "^6.4|^7.0", - "symfony/lock": "^6.4|^7.0", - "symfony/messenger": "^6.4|^7.0", - "symfony/process": "^6.4|^7.0", - "symfony/stopwatch": "^6.4|^7.0", - "symfony/var-dumper": "^6.4|^7.0" + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/event-dispatcher": "^6.4|^7.0|^8.0", + "symfony/http-foundation": "^6.4|^7.0|^8.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/lock": "^6.4|^7.0|^8.0", + "symfony/messenger": "^6.4|^7.0|^8.0", + "symfony/process": "^6.4|^7.0|^8.0", + "symfony/stopwatch": "^6.4|^7.0|^8.0", + "symfony/var-dumper": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { @@ -5037,7 +5227,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v7.3.5" + "source": "https://github.com/symfony/console/tree/v7.4.3" }, "funding": [ { @@ -5057,24 +5247,24 @@ "type": "tidelift" } ], - "time": "2025-10-14T15:46:26+00:00" + "time": "2025-12-23T14:50:43+00:00" }, { "name": "symfony/css-selector", - "version": "v7.3.0", + "version": "v8.0.0", "source": { "type": "git", "url": "https://github.com/symfony/css-selector.git", - "reference": "601a5ce9aaad7bf10797e3663faefce9e26c24e2" + "reference": "6225bd458c53ecdee056214cb4a2ffaf58bd592b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/css-selector/zipball/601a5ce9aaad7bf10797e3663faefce9e26c24e2", - "reference": "601a5ce9aaad7bf10797e3663faefce9e26c24e2", + "url": "https://api.github.com/repos/symfony/css-selector/zipball/6225bd458c53ecdee056214cb4a2ffaf58bd592b", + "reference": "6225bd458c53ecdee056214cb4a2ffaf58bd592b", "shasum": "" }, "require": { - "php": ">=8.2" + "php": ">=8.4" }, "type": "library", "autoload": { @@ -5106,7 +5296,7 @@ "description": "Converts CSS selectors to XPath expressions", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/css-selector/tree/v7.3.0" + "source": "https://github.com/symfony/css-selector/tree/v8.0.0" }, "funding": [ { @@ -5117,12 +5307,16 @@ "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-25T14:21:43+00:00" + "time": "2025-10-30T14:17:19+00:00" }, { "name": "symfony/deprecation-contracts", @@ -5193,32 +5387,33 @@ }, { "name": "symfony/error-handler", - "version": "v7.3.4", + "version": "v7.4.0", "source": { "type": "git", "url": "https://github.com/symfony/error-handler.git", - "reference": "99f81bc944ab8e5dae4f21b4ca9972698bbad0e4" + "reference": "48be2b0653594eea32dcef130cca1c811dcf25c2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/error-handler/zipball/99f81bc944ab8e5dae4f21b4ca9972698bbad0e4", - "reference": "99f81bc944ab8e5dae4f21b4ca9972698bbad0e4", + "url": "https://api.github.com/repos/symfony/error-handler/zipball/48be2b0653594eea32dcef130cca1c811dcf25c2", + "reference": "48be2b0653594eea32dcef130cca1c811dcf25c2", "shasum": "" }, "require": { "php": ">=8.2", "psr/log": "^1|^2|^3", - "symfony/var-dumper": "^6.4|^7.0" + "symfony/polyfill-php85": "^1.32", + "symfony/var-dumper": "^6.4|^7.0|^8.0" }, "conflict": { "symfony/deprecation-contracts": "<2.5", "symfony/http-kernel": "<6.4" }, "require-dev": { - "symfony/console": "^6.4|^7.0", + "symfony/console": "^6.4|^7.0|^8.0", "symfony/deprecation-contracts": "^2.5|^3", - "symfony/http-kernel": "^6.4|^7.0", - "symfony/serializer": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/serializer": "^6.4|^7.0|^8.0", "symfony/webpack-encore-bundle": "^1.0|^2.0" }, "bin": [ @@ -5250,7 +5445,7 @@ "description": "Provides tools to manage errors and ease debugging PHP code", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/error-handler/tree/v7.3.4" + "source": "https://github.com/symfony/error-handler/tree/v7.4.0" }, "funding": [ { @@ -5270,28 +5465,28 @@ "type": "tidelift" } ], - "time": "2025-09-11T10:12:26+00:00" + "time": "2025-11-05T14:29:59+00:00" }, { "name": "symfony/event-dispatcher", - "version": "v7.3.3", + "version": "v8.0.0", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "b7dc69e71de420ac04bc9ab830cf3ffebba48191" + "reference": "573f95783a2ec6e38752979db139f09fec033f03" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/b7dc69e71de420ac04bc9ab830cf3ffebba48191", - "reference": "b7dc69e71de420ac04bc9ab830cf3ffebba48191", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/573f95783a2ec6e38752979db139f09fec033f03", + "reference": "573f95783a2ec6e38752979db139f09fec033f03", "shasum": "" }, "require": { - "php": ">=8.2", + "php": ">=8.4", "symfony/event-dispatcher-contracts": "^2.5|^3" }, "conflict": { - "symfony/dependency-injection": "<6.4", + "symfony/security-http": "<7.4", "symfony/service-contracts": "<2.5" }, "provide": { @@ -5300,13 +5495,14 @@ }, "require-dev": { "psr/log": "^1|^2|^3", - "symfony/config": "^6.4|^7.0", - "symfony/dependency-injection": "^6.4|^7.0", - "symfony/error-handler": "^6.4|^7.0", - "symfony/expression-language": "^6.4|^7.0", - "symfony/http-foundation": "^6.4|^7.0", + "symfony/config": "^7.4|^8.0", + "symfony/dependency-injection": "^7.4|^8.0", + "symfony/error-handler": "^7.4|^8.0", + "symfony/expression-language": "^7.4|^8.0", + "symfony/framework-bundle": "^7.4|^8.0", + "symfony/http-foundation": "^7.4|^8.0", "symfony/service-contracts": "^2.5|^3", - "symfony/stopwatch": "^6.4|^7.0" + "symfony/stopwatch": "^7.4|^8.0" }, "type": "library", "autoload": { @@ -5334,7 +5530,7 @@ "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/event-dispatcher/tree/v7.3.3" + "source": "https://github.com/symfony/event-dispatcher/tree/v8.0.0" }, "funding": [ { @@ -5354,7 +5550,7 @@ "type": "tidelift" } ], - "time": "2025-08-13T11:49:31+00:00" + "time": "2025-10-30T14:17:19+00:00" }, { "name": "symfony/event-dispatcher-contracts", @@ -5433,24 +5629,94 @@ "time": "2024-09-25T14:21:43+00:00" }, { - "name": "symfony/finder", - "version": "v7.3.5", + "name": "symfony/filesystem", + "version": "v8.0.1", "source": { "type": "git", - "url": "https://github.com/symfony/finder.git", - "reference": "9f696d2f1e340484b4683f7853b273abff94421f" + "url": "https://github.com/symfony/filesystem.git", + "reference": "d937d400b980523dc9ee946bb69972b5e619058d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/9f696d2f1e340484b4683f7853b273abff94421f", - "reference": "9f696d2f1e340484b4683f7853b273abff94421f", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/d937d400b980523dc9ee946bb69972b5e619058d", + "reference": "d937d400b980523dc9ee946bb69972b5e619058d", + "shasum": "" + }, + "require": { + "php": ">=8.4", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-mbstring": "~1.8" + }, + "require-dev": { + "symfony/process": "^7.4|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Filesystem\\": "" + }, + "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": "Provides basic utilities for the filesystem", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/filesystem/tree/v8.0.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-12-01T09:13:36+00:00" + }, + { + "name": "symfony/finder", + "version": "v7.4.3", + "source": { + "type": "git", + "url": "https://github.com/symfony/finder.git", + "reference": "fffe05569336549b20a1be64250b40516d6e8d06" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/finder/zipball/fffe05569336549b20a1be64250b40516d6e8d06", + "reference": "fffe05569336549b20a1be64250b40516d6e8d06", "shasum": "" }, "require": { "php": ">=8.2" }, "require-dev": { - "symfony/filesystem": "^6.4|^7.0" + "symfony/filesystem": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { @@ -5478,7 +5744,7 @@ "description": "Finds files and directories via an intuitive fluent interface", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/finder/tree/v7.3.5" + "source": "https://github.com/symfony/finder/tree/v7.4.3" }, "funding": [ { @@ -5498,27 +5764,26 @@ "type": "tidelift" } ], - "time": "2025-10-15T18:45:57+00:00" + "time": "2025-12-23T14:50:43+00:00" }, { "name": "symfony/http-foundation", - "version": "v7.3.5", + "version": "v7.4.3", "source": { "type": "git", "url": "https://github.com/symfony/http-foundation.git", - "reference": "ce31218c7cac92eab280762c4375fb70a6f4f897" + "reference": "a70c745d4cea48dbd609f4075e5f5cbce453bd52" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-foundation/zipball/ce31218c7cac92eab280762c4375fb70a6f4f897", - "reference": "ce31218c7cac92eab280762c4375fb70a6f4f897", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/a70c745d4cea48dbd609f4075e5f5cbce453bd52", + "reference": "a70c745d4cea48dbd609f4075e5f5cbce453bd52", "shasum": "" }, "require": { "php": ">=8.2", - "symfony/deprecation-contracts": "^2.5|^3.0", - "symfony/polyfill-mbstring": "~1.1", - "symfony/polyfill-php83": "^1.27" + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "^1.1" }, "conflict": { "doctrine/dbal": "<3.6", @@ -5527,13 +5792,13 @@ "require-dev": { "doctrine/dbal": "^3.6|^4", "predis/predis": "^1.1|^2.0", - "symfony/cache": "^6.4.12|^7.1.5", - "symfony/clock": "^6.4|^7.0", - "symfony/dependency-injection": "^6.4|^7.0", - "symfony/expression-language": "^6.4|^7.0", - "symfony/http-kernel": "^6.4|^7.0", - "symfony/mime": "^6.4|^7.0", - "symfony/rate-limiter": "^6.4|^7.0" + "symfony/cache": "^6.4.12|^7.1.5|^8.0", + "symfony/clock": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/expression-language": "^6.4|^7.0|^8.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/mime": "^6.4|^7.0|^8.0", + "symfony/rate-limiter": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { @@ -5561,7 +5826,7 @@ "description": "Defines an object-oriented layer for the HTTP specification", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-foundation/tree/v7.3.5" + "source": "https://github.com/symfony/http-foundation/tree/v7.4.3" }, "funding": [ { @@ -5581,29 +5846,29 @@ "type": "tidelift" } ], - "time": "2025-10-24T21:42:11+00:00" + "time": "2025-12-23T14:23:49+00:00" }, { "name": "symfony/http-kernel", - "version": "v7.3.5", + "version": "v7.4.3", "source": { "type": "git", "url": "https://github.com/symfony/http-kernel.git", - "reference": "24fd3f123532e26025f49f1abefcc01a69ef15ab" + "reference": "885211d4bed3f857b8c964011923528a55702aa5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-kernel/zipball/24fd3f123532e26025f49f1abefcc01a69ef15ab", - "reference": "24fd3f123532e26025f49f1abefcc01a69ef15ab", + "url": "https://api.github.com/repos/symfony/http-kernel/zipball/885211d4bed3f857b8c964011923528a55702aa5", + "reference": "885211d4bed3f857b8c964011923528a55702aa5", "shasum": "" }, "require": { "php": ">=8.2", "psr/log": "^1|^2|^3", "symfony/deprecation-contracts": "^2.5|^3", - "symfony/error-handler": "^6.4|^7.0", - "symfony/event-dispatcher": "^7.3", - "symfony/http-foundation": "^7.3", + "symfony/error-handler": "^6.4|^7.0|^8.0", + "symfony/event-dispatcher": "^7.3|^8.0", + "symfony/http-foundation": "^7.4|^8.0", "symfony/polyfill-ctype": "^1.8" }, "conflict": { @@ -5613,6 +5878,7 @@ "symfony/console": "<6.4", "symfony/dependency-injection": "<6.4", "symfony/doctrine-bridge": "<6.4", + "symfony/flex": "<2.10", "symfony/form": "<6.4", "symfony/http-client": "<6.4", "symfony/http-client-contracts": "<2.5", @@ -5630,27 +5896,27 @@ }, "require-dev": { "psr/cache": "^1.0|^2.0|^3.0", - "symfony/browser-kit": "^6.4|^7.0", - "symfony/clock": "^6.4|^7.0", - "symfony/config": "^6.4|^7.0", - "symfony/console": "^6.4|^7.0", - "symfony/css-selector": "^6.4|^7.0", - "symfony/dependency-injection": "^6.4|^7.0", - "symfony/dom-crawler": "^6.4|^7.0", - "symfony/expression-language": "^6.4|^7.0", - "symfony/finder": "^6.4|^7.0", + "symfony/browser-kit": "^6.4|^7.0|^8.0", + "symfony/clock": "^6.4|^7.0|^8.0", + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/console": "^6.4|^7.0|^8.0", + "symfony/css-selector": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/dom-crawler": "^6.4|^7.0|^8.0", + "symfony/expression-language": "^6.4|^7.0|^8.0", + "symfony/finder": "^6.4|^7.0|^8.0", "symfony/http-client-contracts": "^2.5|^3", - "symfony/process": "^6.4|^7.0", - "symfony/property-access": "^7.1", - "symfony/routing": "^6.4|^7.0", - "symfony/serializer": "^7.1", - "symfony/stopwatch": "^6.4|^7.0", - "symfony/translation": "^6.4|^7.0", + "symfony/process": "^6.4|^7.0|^8.0", + "symfony/property-access": "^7.1|^8.0", + "symfony/routing": "^6.4|^7.0|^8.0", + "symfony/serializer": "^7.1|^8.0", + "symfony/stopwatch": "^6.4|^7.0|^8.0", + "symfony/translation": "^6.4|^7.0|^8.0", "symfony/translation-contracts": "^2.5|^3", - "symfony/uid": "^6.4|^7.0", - "symfony/validator": "^6.4|^7.0", - "symfony/var-dumper": "^6.4|^7.0", - "symfony/var-exporter": "^6.4|^7.0", + "symfony/uid": "^6.4|^7.0|^8.0", + "symfony/validator": "^6.4|^7.0|^8.0", + "symfony/var-dumper": "^6.4|^7.0|^8.0", + "symfony/var-exporter": "^6.4|^7.0|^8.0", "twig/twig": "^3.12" }, "type": "library", @@ -5679,7 +5945,7 @@ "description": "Provides a structured process for converting a Request into a Response", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-kernel/tree/v7.3.5" + "source": "https://github.com/symfony/http-kernel/tree/v7.4.3" }, "funding": [ { @@ -5699,20 +5965,20 @@ "type": "tidelift" } ], - "time": "2025-10-28T10:19:01+00:00" + "time": "2025-12-31T08:43:57+00:00" }, { "name": "symfony/mailer", - "version": "v7.3.5", + "version": "v7.4.3", "source": { "type": "git", "url": "https://github.com/symfony/mailer.git", - "reference": "fd497c45ba9c10c37864e19466b090dcb60a50ba" + "reference": "e472d35e230108231ccb7f51eb6b2100cac02ee4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/mailer/zipball/fd497c45ba9c10c37864e19466b090dcb60a50ba", - "reference": "fd497c45ba9c10c37864e19466b090dcb60a50ba", + "url": "https://api.github.com/repos/symfony/mailer/zipball/e472d35e230108231ccb7f51eb6b2100cac02ee4", + "reference": "e472d35e230108231ccb7f51eb6b2100cac02ee4", "shasum": "" }, "require": { @@ -5720,8 +5986,8 @@ "php": ">=8.2", "psr/event-dispatcher": "^1", "psr/log": "^1|^2|^3", - "symfony/event-dispatcher": "^6.4|^7.0", - "symfony/mime": "^7.2", + "symfony/event-dispatcher": "^6.4|^7.0|^8.0", + "symfony/mime": "^7.2|^8.0", "symfony/service-contracts": "^2.5|^3" }, "conflict": { @@ -5732,10 +5998,10 @@ "symfony/twig-bridge": "<6.4" }, "require-dev": { - "symfony/console": "^6.4|^7.0", - "symfony/http-client": "^6.4|^7.0", - "symfony/messenger": "^6.4|^7.0", - "symfony/twig-bridge": "^6.4|^7.0" + "symfony/console": "^6.4|^7.0|^8.0", + "symfony/http-client": "^6.4|^7.0|^8.0", + "symfony/messenger": "^6.4|^7.0|^8.0", + "symfony/twig-bridge": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { @@ -5763,7 +6029,7 @@ "description": "Helps sending emails", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/mailer/tree/v7.3.5" + "source": "https://github.com/symfony/mailer/tree/v7.4.3" }, "funding": [ { @@ -5783,24 +6049,25 @@ "type": "tidelift" } ], - "time": "2025-10-24T14:27:20+00:00" + "time": "2025-12-16T08:02:06+00:00" }, { "name": "symfony/mime", - "version": "v7.3.4", + "version": "v7.4.0", "source": { "type": "git", "url": "https://github.com/symfony/mime.git", - "reference": "b1b828f69cbaf887fa835a091869e55df91d0e35" + "reference": "bdb02729471be5d047a3ac4a69068748f1a6be7a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/mime/zipball/b1b828f69cbaf887fa835a091869e55df91d0e35", - "reference": "b1b828f69cbaf887fa835a091869e55df91d0e35", + "url": "https://api.github.com/repos/symfony/mime/zipball/bdb02729471be5d047a3ac4a69068748f1a6be7a", + "reference": "bdb02729471be5d047a3ac4a69068748f1a6be7a", "shasum": "" }, "require": { "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", "symfony/polyfill-intl-idn": "^1.10", "symfony/polyfill-mbstring": "^1.0" }, @@ -5815,11 +6082,11 @@ "egulias/email-validator": "^2.1.10|^3.1|^4", "league/html-to-markdown": "^5.0", "phpdocumentor/reflection-docblock": "^3.0|^4.0|^5.0", - "symfony/dependency-injection": "^6.4|^7.0", - "symfony/process": "^6.4|^7.0", - "symfony/property-access": "^6.4|^7.0", - "symfony/property-info": "^6.4|^7.0", - "symfony/serializer": "^6.4.3|^7.0.3" + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/process": "^6.4|^7.0|^8.0", + "symfony/property-access": "^6.4|^7.0|^8.0", + "symfony/property-info": "^6.4|^7.0|^8.0", + "symfony/serializer": "^6.4.3|^7.0.3|^8.0" }, "type": "library", "autoload": { @@ -5851,7 +6118,7 @@ "mime-type" ], "support": { - "source": "https://github.com/symfony/mime/tree/v7.3.4" + "source": "https://github.com/symfony/mime/tree/v7.4.0" }, "funding": [ { @@ -5871,7 +6138,7 @@ "type": "tidelift" } ], - "time": "2025-09-16T08:38:17+00:00" + "time": "2025-11-16T10:14:42+00:00" }, { "name": "symfony/polyfill-ctype", @@ -6704,16 +6971,16 @@ }, { "name": "symfony/process", - "version": "v7.3.4", + "version": "v7.4.3", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "f24f8f316367b30810810d4eb30c543d7003ff3b" + "reference": "2f8e1a6cdf590ca63715da4d3a7a3327404a523f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/f24f8f316367b30810810d4eb30c543d7003ff3b", - "reference": "f24f8f316367b30810810d4eb30c543d7003ff3b", + "url": "https://api.github.com/repos/symfony/process/zipball/2f8e1a6cdf590ca63715da4d3a7a3327404a523f", + "reference": "2f8e1a6cdf590ca63715da4d3a7a3327404a523f", "shasum": "" }, "require": { @@ -6745,7 +7012,7 @@ "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/process/tree/v7.3.4" + "source": "https://github.com/symfony/process/tree/v7.4.3" }, "funding": [ { @@ -6765,20 +7032,20 @@ "type": "tidelift" } ], - "time": "2025-09-11T10:12:26+00:00" + "time": "2025-12-19T10:00:43+00:00" }, { "name": "symfony/routing", - "version": "v7.3.4", + "version": "v7.4.3", "source": { "type": "git", "url": "https://github.com/symfony/routing.git", - "reference": "8dc648e159e9bac02b703b9fbd937f19ba13d07c" + "reference": "5d3fd7adf8896c2fdb54e2f0f35b1bcbd9e45090" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/routing/zipball/8dc648e159e9bac02b703b9fbd937f19ba13d07c", - "reference": "8dc648e159e9bac02b703b9fbd937f19ba13d07c", + "url": "https://api.github.com/repos/symfony/routing/zipball/5d3fd7adf8896c2fdb54e2f0f35b1bcbd9e45090", + "reference": "5d3fd7adf8896c2fdb54e2f0f35b1bcbd9e45090", "shasum": "" }, "require": { @@ -6792,11 +7059,11 @@ }, "require-dev": { "psr/log": "^1|^2|^3", - "symfony/config": "^6.4|^7.0", - "symfony/dependency-injection": "^6.4|^7.0", - "symfony/expression-language": "^6.4|^7.0", - "symfony/http-foundation": "^6.4|^7.0", - "symfony/yaml": "^6.4|^7.0" + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/expression-language": "^6.4|^7.0|^8.0", + "symfony/http-foundation": "^6.4|^7.0|^8.0", + "symfony/yaml": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { @@ -6830,7 +7097,7 @@ "url" ], "support": { - "source": "https://github.com/symfony/routing/tree/v7.3.4" + "source": "https://github.com/symfony/routing/tree/v7.4.3" }, "funding": [ { @@ -6850,20 +7117,20 @@ "type": "tidelift" } ], - "time": "2025-09-11T10:12:26+00:00" + "time": "2025-12-19T10:00:43+00:00" }, { "name": "symfony/service-contracts", - "version": "v3.6.0", + "version": "v3.6.1", "source": { "type": "git", "url": "https://github.com/symfony/service-contracts.git", - "reference": "f021b05a130d35510bd6b25fe9053c2a8a15d5d4" + "reference": "45112560a3ba2d715666a509a0bc9521d10b6c43" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/service-contracts/zipball/f021b05a130d35510bd6b25fe9053c2a8a15d5d4", - "reference": "f021b05a130d35510bd6b25fe9053c2a8a15d5d4", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/45112560a3ba2d715666a509a0bc9521d10b6c43", + "reference": "45112560a3ba2d715666a509a0bc9521d10b6c43", "shasum": "" }, "require": { @@ -6917,7 +7184,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/service-contracts/tree/v3.6.0" + "source": "https://github.com/symfony/service-contracts/tree/v3.6.1" }, "funding": [ { @@ -6928,43 +7195,47 @@ "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-04-25T09:37:31+00:00" + "time": "2025-07-15T11:30:57+00:00" }, { "name": "symfony/string", - "version": "v7.3.4", + "version": "v8.0.1", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "f96476035142921000338bad71e5247fbc138872" + "reference": "ba65a969ac918ce0cc3edfac6cdde847eba231dc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/f96476035142921000338bad71e5247fbc138872", - "reference": "f96476035142921000338bad71e5247fbc138872", + "url": "https://api.github.com/repos/symfony/string/zipball/ba65a969ac918ce0cc3edfac6cdde847eba231dc", + "reference": "ba65a969ac918ce0cc3edfac6cdde847eba231dc", "shasum": "" }, "require": { - "php": ">=8.2", - "symfony/polyfill-ctype": "~1.8", - "symfony/polyfill-intl-grapheme": "~1.0", - "symfony/polyfill-intl-normalizer": "~1.0", - "symfony/polyfill-mbstring": "~1.0" + "php": ">=8.4", + "symfony/polyfill-ctype": "^1.8", + "symfony/polyfill-intl-grapheme": "^1.33", + "symfony/polyfill-intl-normalizer": "^1.0", + "symfony/polyfill-mbstring": "^1.0" }, "conflict": { "symfony/translation-contracts": "<2.5" }, "require-dev": { - "symfony/emoji": "^7.1", - "symfony/http-client": "^6.4|^7.0", - "symfony/intl": "^6.4|^7.0", + "symfony/emoji": "^7.4|^8.0", + "symfony/http-client": "^7.4|^8.0", + "symfony/intl": "^7.4|^8.0", "symfony/translation-contracts": "^2.5|^3.0", - "symfony/var-exporter": "^6.4|^7.0" + "symfony/var-exporter": "^7.4|^8.0" }, "type": "library", "autoload": { @@ -7003,7 +7274,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v7.3.4" + "source": "https://github.com/symfony/string/tree/v8.0.1" }, "funding": [ { @@ -7023,38 +7294,31 @@ "type": "tidelift" } ], - "time": "2025-09-11T14:36:48+00:00" + "time": "2025-12-01T09:13:36+00:00" }, { "name": "symfony/translation", - "version": "v7.3.4", + "version": "v8.0.3", "source": { "type": "git", "url": "https://github.com/symfony/translation.git", - "reference": "ec25870502d0c7072d086e8ffba1420c85965174" + "reference": "60a8f11f0e15c48f2cc47c4da53873bb5b62135d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/translation/zipball/ec25870502d0c7072d086e8ffba1420c85965174", - "reference": "ec25870502d0c7072d086e8ffba1420c85965174", + "url": "https://api.github.com/repos/symfony/translation/zipball/60a8f11f0e15c48f2cc47c4da53873bb5b62135d", + "reference": "60a8f11f0e15c48f2cc47c4da53873bb5b62135d", "shasum": "" }, "require": { - "php": ">=8.2", - "symfony/deprecation-contracts": "^2.5|^3", - "symfony/polyfill-mbstring": "~1.0", - "symfony/translation-contracts": "^2.5|^3.0" + "php": ">=8.4", + "symfony/polyfill-mbstring": "^1.0", + "symfony/translation-contracts": "^3.6.1" }, "conflict": { "nikic/php-parser": "<5.0", - "symfony/config": "<6.4", - "symfony/console": "<6.4", - "symfony/dependency-injection": "<6.4", "symfony/http-client-contracts": "<2.5", - "symfony/http-kernel": "<6.4", - "symfony/service-contracts": "<2.5", - "symfony/twig-bundle": "<6.4", - "symfony/yaml": "<6.4" + "symfony/service-contracts": "<2.5" }, "provide": { "symfony/translation-implementation": "2.3|3.0" @@ -7062,17 +7326,17 @@ "require-dev": { "nikic/php-parser": "^5.0", "psr/log": "^1|^2|^3", - "symfony/config": "^6.4|^7.0", - "symfony/console": "^6.4|^7.0", - "symfony/dependency-injection": "^6.4|^7.0", - "symfony/finder": "^6.4|^7.0", + "symfony/config": "^7.4|^8.0", + "symfony/console": "^7.4|^8.0", + "symfony/dependency-injection": "^7.4|^8.0", + "symfony/finder": "^7.4|^8.0", "symfony/http-client-contracts": "^2.5|^3.0", - "symfony/http-kernel": "^6.4|^7.0", - "symfony/intl": "^6.4|^7.0", + "symfony/http-kernel": "^7.4|^8.0", + "symfony/intl": "^7.4|^8.0", "symfony/polyfill-intl-icu": "^1.21", - "symfony/routing": "^6.4|^7.0", + "symfony/routing": "^7.4|^8.0", "symfony/service-contracts": "^2.5|^3", - "symfony/yaml": "^6.4|^7.0" + "symfony/yaml": "^7.4|^8.0" }, "type": "library", "autoload": { @@ -7103,7 +7367,7 @@ "description": "Provides tools to internationalize your application", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/translation/tree/v7.3.4" + "source": "https://github.com/symfony/translation/tree/v8.0.3" }, "funding": [ { @@ -7123,20 +7387,20 @@ "type": "tidelift" } ], - "time": "2025-09-07T11:39:36+00:00" + "time": "2025-12-21T10:59:45+00:00" }, { "name": "symfony/translation-contracts", - "version": "v3.6.0", + "version": "v3.6.1", "source": { "type": "git", "url": "https://github.com/symfony/translation-contracts.git", - "reference": "df210c7a2573f1913b2d17cc95f90f53a73d8f7d" + "reference": "65a8bc82080447fae78373aa10f8d13b38338977" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/translation-contracts/zipball/df210c7a2573f1913b2d17cc95f90f53a73d8f7d", - "reference": "df210c7a2573f1913b2d17cc95f90f53a73d8f7d", + "url": "https://api.github.com/repos/symfony/translation-contracts/zipball/65a8bc82080447fae78373aa10f8d13b38338977", + "reference": "65a8bc82080447fae78373aa10f8d13b38338977", "shasum": "" }, "require": { @@ -7185,7 +7449,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/translation-contracts/tree/v3.6.0" + "source": "https://github.com/symfony/translation-contracts/tree/v3.6.1" }, "funding": [ { @@ -7196,25 +7460,29 @@ "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-27T08:32:26+00:00" + "time": "2025-07-15T13:41:35+00:00" }, { "name": "symfony/uid", - "version": "v7.3.1", + "version": "v7.4.0", "source": { "type": "git", "url": "https://github.com/symfony/uid.git", - "reference": "a69f69f3159b852651a6bf45a9fdd149520525bb" + "reference": "2498e9f81b7baa206f44de583f2f48350b90142c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/uid/zipball/a69f69f3159b852651a6bf45a9fdd149520525bb", - "reference": "a69f69f3159b852651a6bf45a9fdd149520525bb", + "url": "https://api.github.com/repos/symfony/uid/zipball/2498e9f81b7baa206f44de583f2f48350b90142c", + "reference": "2498e9f81b7baa206f44de583f2f48350b90142c", "shasum": "" }, "require": { @@ -7222,7 +7490,7 @@ "symfony/polyfill-uuid": "^1.15" }, "require-dev": { - "symfony/console": "^6.4|^7.0" + "symfony/console": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { @@ -7259,7 +7527,7 @@ "uuid" ], "support": { - "source": "https://github.com/symfony/uid/tree/v7.3.1" + "source": "https://github.com/symfony/uid/tree/v7.4.0" }, "funding": [ { @@ -7270,25 +7538,29 @@ "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-06-27T19:55:54+00:00" + "time": "2025-09-25T11:02:55+00:00" }, { "name": "symfony/var-dumper", - "version": "v7.3.5", + "version": "v7.4.3", "source": { "type": "git", "url": "https://github.com/symfony/var-dumper.git", - "reference": "476c4ae17f43a9a36650c69879dcf5b1e6ae724d" + "reference": "7e99bebcb3f90d8721890f2963463280848cba92" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-dumper/zipball/476c4ae17f43a9a36650c69879dcf5b1e6ae724d", - "reference": "476c4ae17f43a9a36650c69879dcf5b1e6ae724d", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/7e99bebcb3f90d8721890f2963463280848cba92", + "reference": "7e99bebcb3f90d8721890f2963463280848cba92", "shasum": "" }, "require": { @@ -7300,10 +7572,10 @@ "symfony/console": "<6.4" }, "require-dev": { - "symfony/console": "^6.4|^7.0", - "symfony/http-kernel": "^6.4|^7.0", - "symfony/process": "^6.4|^7.0", - "symfony/uid": "^6.4|^7.0", + "symfony/console": "^6.4|^7.0|^8.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/process": "^6.4|^7.0|^8.0", + "symfony/uid": "^6.4|^7.0|^8.0", "twig/twig": "^3.12" }, "bin": [ @@ -7342,7 +7614,7 @@ "dump" ], "support": { - "source": "https://github.com/symfony/var-dumper/tree/v7.3.5" + "source": "https://github.com/symfony/var-dumper/tree/v7.4.3" }, "funding": [ { @@ -7362,20 +7634,20 @@ "type": "tidelift" } ], - "time": "2025-09-27T09:00:46+00:00" + "time": "2025-12-18T07:04:31+00:00" }, { "name": "symfony/var-exporter", - "version": "v7.3.4", + "version": "v7.4.0", "source": { "type": "git", "url": "https://github.com/symfony/var-exporter.git", - "reference": "0f020b544a30a7fe8ba972e53ee48a74c0bc87f4" + "reference": "03a60f169c79a28513a78c967316fbc8bf17816f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-exporter/zipball/0f020b544a30a7fe8ba972e53ee48a74c0bc87f4", - "reference": "0f020b544a30a7fe8ba972e53ee48a74c0bc87f4", + "url": "https://api.github.com/repos/symfony/var-exporter/zipball/03a60f169c79a28513a78c967316fbc8bf17816f", + "reference": "03a60f169c79a28513a78c967316fbc8bf17816f", "shasum": "" }, "require": { @@ -7383,9 +7655,9 @@ "symfony/deprecation-contracts": "^2.5|^3" }, "require-dev": { - "symfony/property-access": "^6.4|^7.0", - "symfony/serializer": "^6.4|^7.0", - "symfony/var-dumper": "^6.4|^7.0" + "symfony/property-access": "^6.4|^7.0|^8.0", + "symfony/serializer": "^6.4|^7.0|^8.0", + "symfony/var-dumper": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { @@ -7423,7 +7695,7 @@ "serialize" ], "support": { - "source": "https://github.com/symfony/var-exporter/tree/v7.3.4" + "source": "https://github.com/symfony/var-exporter/tree/v7.4.0" }, "funding": [ { @@ -7443,32 +7715,32 @@ "type": "tidelift" } ], - "time": "2025-09-11T10:12:26+00:00" + "time": "2025-09-11T10:15:23+00:00" }, { "name": "symfony/yaml", - "version": "v7.3.5", + "version": "v7.4.1", "source": { "type": "git", "url": "https://github.com/symfony/yaml.git", - "reference": "90208e2fc6f68f613eae7ca25a2458a931b1bacc" + "reference": "24dd4de28d2e3988b311751ac49e684d783e2345" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/90208e2fc6f68f613eae7ca25a2458a931b1bacc", - "reference": "90208e2fc6f68f613eae7ca25a2458a931b1bacc", + "url": "https://api.github.com/repos/symfony/yaml/zipball/24dd4de28d2e3988b311751ac49e684d783e2345", + "reference": "24dd4de28d2e3988b311751ac49e684d783e2345", "shasum": "" }, "require": { "php": ">=8.2", - "symfony/deprecation-contracts": "^2.5|^3.0", + "symfony/deprecation-contracts": "^2.5|^3", "symfony/polyfill-ctype": "^1.8" }, "conflict": { "symfony/console": "<6.4" }, "require-dev": { - "symfony/console": "^6.4|^7.0" + "symfony/console": "^6.4|^7.0|^8.0" }, "bin": [ "Resources/bin/yaml-lint" @@ -7499,7 +7771,7 @@ "description": "Loads and dumps YAML files", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/yaml/tree/v7.3.5" + "source": "https://github.com/symfony/yaml/tree/v7.4.1" }, "funding": [ { @@ -7519,27 +7791,27 @@ "type": "tidelift" } ], - "time": "2025-09-27T09:00:46+00:00" + "time": "2025-12-04T18:11:45+00:00" }, { "name": "tijsverkoyen/css-to-inline-styles", - "version": "v2.3.0", + "version": "v2.4.0", "source": { "type": "git", "url": "https://github.com/tijsverkoyen/CssToInlineStyles.git", - "reference": "0d72ac1c00084279c1816675284073c5a337c20d" + "reference": "f0292ccf0ec75843d65027214426b6b163b48b41" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/tijsverkoyen/CssToInlineStyles/zipball/0d72ac1c00084279c1816675284073c5a337c20d", - "reference": "0d72ac1c00084279c1816675284073c5a337c20d", + "url": "https://api.github.com/repos/tijsverkoyen/CssToInlineStyles/zipball/f0292ccf0ec75843d65027214426b6b163b48b41", + "reference": "f0292ccf0ec75843d65027214426b6b163b48b41", "shasum": "" }, "require": { "ext-dom": "*", "ext-libxml": "*", "php": "^7.4 || ^8.0", - "symfony/css-selector": "^5.4 || ^6.0 || ^7.0" + "symfony/css-selector": "^5.4 || ^6.0 || ^7.0 || ^8.0" }, "require-dev": { "phpstan/phpstan": "^2.0", @@ -7572,32 +7844,32 @@ "homepage": "https://github.com/tijsverkoyen/CssToInlineStyles", "support": { "issues": "https://github.com/tijsverkoyen/CssToInlineStyles/issues", - "source": "https://github.com/tijsverkoyen/CssToInlineStyles/tree/v2.3.0" + "source": "https://github.com/tijsverkoyen/CssToInlineStyles/tree/v2.4.0" }, - "time": "2024-12-21T16:25:41+00:00" + "time": "2025-12-02T11:56:42+00:00" }, { "name": "vlucas/phpdotenv", - "version": "v5.6.2", + "version": "v5.6.3", "source": { "type": "git", "url": "https://github.com/vlucas/phpdotenv.git", - "reference": "24ac4c74f91ee2c193fa1aaa5c249cb0822809af" + "reference": "955e7815d677a3eaa7075231212f2110983adecc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/24ac4c74f91ee2c193fa1aaa5c249cb0822809af", - "reference": "24ac4c74f91ee2c193fa1aaa5c249cb0822809af", + "url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/955e7815d677a3eaa7075231212f2110983adecc", + "reference": "955e7815d677a3eaa7075231212f2110983adecc", "shasum": "" }, "require": { "ext-pcre": "*", - "graham-campbell/result-type": "^1.1.3", + "graham-campbell/result-type": "^1.1.4", "php": "^7.2.5 || ^8.0", - "phpoption/phpoption": "^1.9.3", - "symfony/polyfill-ctype": "^1.24", - "symfony/polyfill-mbstring": "^1.24", - "symfony/polyfill-php80": "^1.24" + "phpoption/phpoption": "^1.9.5", + "symfony/polyfill-ctype": "^1.26", + "symfony/polyfill-mbstring": "^1.26", + "symfony/polyfill-php80": "^1.26" }, "require-dev": { "bamarni/composer-bin-plugin": "^1.8.2", @@ -7646,7 +7918,7 @@ ], "support": { "issues": "https://github.com/vlucas/phpdotenv/issues", - "source": "https://github.com/vlucas/phpdotenv/tree/v5.6.2" + "source": "https://github.com/vlucas/phpdotenv/tree/v5.6.3" }, "funding": [ { @@ -7658,7 +7930,7 @@ "type": "tidelift" } ], - "time": "2025-04-30T23:37:27+00:00" + "time": "2025-12-27T19:49:13+00:00" }, { "name": "voku/portable-ascii", @@ -7736,16 +8008,16 @@ }, { "name": "wnx/sidecar-browsershot", - "version": "v2.6.1", + "version": "v2.7.0", "source": { "type": "git", "url": "https://github.com/stefanzweifel/sidecar-browsershot.git", - "reference": "1f35d16bed18f766e918adc8e06ca83c274bcbdd" + "reference": "e42a996c6fab4357919cd5e3f3fab33f019cdd80" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/stefanzweifel/sidecar-browsershot/zipball/1f35d16bed18f766e918adc8e06ca83c274bcbdd", - "reference": "1f35d16bed18f766e918adc8e06ca83c274bcbdd", + "url": "https://api.github.com/repos/stefanzweifel/sidecar-browsershot/zipball/e42a996c6fab4357919cd5e3f3fab33f019cdd80", + "reference": "e42a996c6fab4357919cd5e3f3fab33f019cdd80", "shasum": "" }, "require": { @@ -7810,7 +8082,7 @@ ], "support": { "issues": "https://github.com/stefanzweifel/sidecar-browsershot/issues", - "source": "https://github.com/stefanzweifel/sidecar-browsershot/tree/v2.6.1" + "source": "https://github.com/stefanzweifel/sidecar-browsershot/tree/v2.7.0" }, "funding": [ { @@ -7818,22 +8090,22 @@ "type": "github" } ], - "time": "2025-09-23T09:28:01+00:00" + "time": "2025-11-22T08:49:08+00:00" } ], "packages-dev": [ { "name": "brianium/paratest", - "version": "v7.14.2", + "version": "v7.16.1", "source": { "type": "git", "url": "https://github.com/paratestphp/paratest.git", - "reference": "de06de1ae1203b11976c6ca01d6a9081c8b33d45" + "reference": "f0fdfd8e654e0d38bc2ba756a6cabe7be287390b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/paratestphp/paratest/zipball/de06de1ae1203b11976c6ca01d6a9081c8b33d45", - "reference": "de06de1ae1203b11976c6ca01d6a9081c8b33d45", + "url": "https://api.github.com/repos/paratestphp/paratest/zipball/f0fdfd8e654e0d38bc2ba756a6cabe7be287390b", + "reference": "f0fdfd8e654e0d38bc2ba756a6cabe7be287390b", "shasum": "" }, "require": { @@ -7844,24 +8116,24 @@ "fidry/cpu-core-counter": "^1.3.0", "jean85/pretty-package-versions": "^2.1.1", "php": "~8.3.0 || ~8.4.0 || ~8.5.0", - "phpunit/php-code-coverage": "^12.4.0", + "phpunit/php-code-coverage": "^12.5.2", "phpunit/php-file-iterator": "^6", "phpunit/php-timer": "^8", - "phpunit/phpunit": "^12.4.1", + "phpunit/phpunit": "^12.5.4", "sebastian/environment": "^8.0.3", - "symfony/console": "^6.4.20 || ^7.3.4", - "symfony/process": "^6.4.20 || ^7.3.4" + "symfony/console": "^7.3.4 || ^8.0.0", + "symfony/process": "^7.3.4 || ^8.0.0" }, "require-dev": { "doctrine/coding-standard": "^14.0.0", "ext-pcntl": "*", "ext-pcov": "*", "ext-posix": "*", - "phpstan/phpstan": "^2.1.31", + "phpstan/phpstan": "^2.1.33", "phpstan/phpstan-deprecation-rules": "^2.0.3", - "phpstan/phpstan-phpunit": "^2.0.7", + "phpstan/phpstan-phpunit": "^2.0.11", "phpstan/phpstan-strict-rules": "^2.0.7", - "symfony/filesystem": "^6.4.13 || ^7.3.2" + "symfony/filesystem": "^7.3.2 || ^8.0.0" }, "bin": [ "bin/paratest", @@ -7901,7 +8173,7 @@ ], "support": { "issues": "https://github.com/paratestphp/paratest/issues", - "source": "https://github.com/paratestphp/paratest/tree/v7.14.2" + "source": "https://github.com/paratestphp/paratest/tree/v7.16.1" }, "funding": [ { @@ -7913,7 +8185,7 @@ "type": "paypal" } ], - "time": "2025-10-24T07:20:53+00:00" + "time": "2026-01-08T07:23:06+00:00" }, { "name": "doctrine/deprecations", @@ -8312,16 +8584,16 @@ }, { "name": "larastan/larastan", - "version": "v3.8.0", + "version": "v3.8.1", "source": { "type": "git", "url": "https://github.com/larastan/larastan.git", - "reference": "d13ef96d652d1b2a8f34f1760ba6bf5b9c98112e" + "reference": "ff3725291bc4c7e6032b5a54776e3e5104c86db9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/larastan/larastan/zipball/d13ef96d652d1b2a8f34f1760ba6bf5b9c98112e", - "reference": "d13ef96d652d1b2a8f34f1760ba6bf5b9c98112e", + "url": "https://api.github.com/repos/larastan/larastan/zipball/ff3725291bc4c7e6032b5a54776e3e5104c86db9", + "reference": "ff3725291bc4c7e6032b5a54776e3e5104c86db9", "shasum": "" }, "require": { @@ -8335,7 +8607,7 @@ "illuminate/pipeline": "^11.44.2 || ^12.4.1", "illuminate/support": "^11.44.2 || ^12.4.1", "php": "^8.2", - "phpstan/phpstan": "^2.1.29" + "phpstan/phpstan": "^2.1.32" }, "require-dev": { "doctrine/coding-standard": "^13", @@ -8390,7 +8662,7 @@ ], "support": { "issues": "https://github.com/larastan/larastan/issues", - "source": "https://github.com/larastan/larastan/tree/v3.8.0" + "source": "https://github.com/larastan/larastan/tree/v3.8.1" }, "funding": [ { @@ -8398,38 +8670,38 @@ "type": "github" } ], - "time": "2025-10-27T23:09:14+00:00" + "time": "2025-12-11T16:37:35+00:00" }, { "name": "laravel/boost", - "version": "v1.7.1", + "version": "v1.8.9", "source": { "type": "git", "url": "https://github.com/laravel/boost.git", - "reference": "355f7c27952862aab3f61adec27773fd4d41a582" + "reference": "1f2c2d41b5216618170fb6730ec13bf894c5bffd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/boost/zipball/355f7c27952862aab3f61adec27773fd4d41a582", - "reference": "355f7c27952862aab3f61adec27773fd4d41a582", + "url": "https://api.github.com/repos/laravel/boost/zipball/1f2c2d41b5216618170fb6730ec13bf894c5bffd", + "reference": "1f2c2d41b5216618170fb6730ec13bf894c5bffd", "shasum": "" }, "require": { - "guzzlehttp/guzzle": "^7.10", - "illuminate/console": "^10.49.0|^11.45.3|^12.28.1", - "illuminate/contracts": "^10.49.0|^11.45.3|^12.28.1", - "illuminate/routing": "^10.49.0|^11.45.3|^12.28.1", - "illuminate/support": "^10.49.0|^11.45.3|^12.28.1", - "laravel/mcp": "^0.3.2", + "guzzlehttp/guzzle": "^7.9", + "illuminate/console": "^10.49.0|^11.45.3|^12.41.1", + "illuminate/contracts": "^10.49.0|^11.45.3|^12.41.1", + "illuminate/routing": "^10.49.0|^11.45.3|^12.41.1", + "illuminate/support": "^10.49.0|^11.45.3|^12.41.1", + "laravel/mcp": "^0.5.1", "laravel/prompts": "0.1.25|^0.3.6", "laravel/roster": "^0.2.9", "php": "^8.1" }, "require-dev": { - "laravel/pint": "1.20", + "laravel/pint": "^1.20.0", "mockery/mockery": "^1.6.12", "orchestra/testbench": "^8.36.0|^9.15.0|^10.6", - "pestphp/pest": "^2.36.0|^3.8.4", + "pestphp/pest": "^2.36.0|^3.8.4|^4.1.5", "phpstan/phpstan": "^2.1.27", "rector/rector": "^2.1" }, @@ -8464,38 +8736,38 @@ "issues": "https://github.com/laravel/boost/issues", "source": "https://github.com/laravel/boost" }, - "time": "2025-11-05T21:41:46+00:00" + "time": "2026-01-07T18:43:11+00:00" }, { "name": "laravel/mcp", - "version": "v0.3.2", + "version": "v0.5.2", "source": { "type": "git", "url": "https://github.com/laravel/mcp.git", - "reference": "dc722a4c388f172365dec70461f0413ac366f360" + "reference": "b9bdd8d6f8b547c8733fe6826b1819341597ba3c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/mcp/zipball/dc722a4c388f172365dec70461f0413ac366f360", - "reference": "dc722a4c388f172365dec70461f0413ac366f360", + "url": "https://api.github.com/repos/laravel/mcp/zipball/b9bdd8d6f8b547c8733fe6826b1819341597ba3c", + "reference": "b9bdd8d6f8b547c8733fe6826b1819341597ba3c", "shasum": "" }, "require": { "ext-json": "*", "ext-mbstring": "*", - "illuminate/console": "^10.49.0|^11.45.3|^12.28.1", - "illuminate/container": "^10.49.0|^11.45.3|^12.28.1", - "illuminate/contracts": "^10.49.0|^11.45.3|^12.28.1", - "illuminate/http": "^10.49.0|^11.45.3|^12.28.1", - "illuminate/json-schema": "^12.28.1", - "illuminate/routing": "^10.49.0|^11.45.3|^12.28.1", - "illuminate/support": "^10.49.0|^11.45.3|^12.28.1", - "illuminate/validation": "^10.49.0|^11.45.3|^12.28.1", + "illuminate/console": "^10.49.0|^11.45.3|^12.41.1", + "illuminate/container": "^10.49.0|^11.45.3|^12.41.1", + "illuminate/contracts": "^10.49.0|^11.45.3|^12.41.1", + "illuminate/http": "^10.49.0|^11.45.3|^12.41.1", + "illuminate/json-schema": "^12.41.1", + "illuminate/routing": "^10.49.0|^11.45.3|^12.41.1", + "illuminate/support": "^10.49.0|^11.45.3|^12.41.1", + "illuminate/validation": "^10.49.0|^11.45.3|^12.41.1", "php": "^8.1" }, "require-dev": { - "laravel/pint": "1.20.0", - "orchestra/testbench": "^8.36.0|^9.15.0|^10.6.0", + "laravel/pint": "^1.20", + "orchestra/testbench": "^8.36|^9.15|^10.8", "pestphp/pest": "^2.36.0|^3.8.4|^4.1.0", "phpstan/phpstan": "^2.1.27", "rector/rector": "^2.2.4" @@ -8537,20 +8809,20 @@ "issues": "https://github.com/laravel/mcp/issues", "source": "https://github.com/laravel/mcp" }, - "time": "2025-10-29T14:26:01+00:00" + "time": "2025-12-19T19:32:34+00:00" }, { "name": "laravel/pail", - "version": "v1.2.3", + "version": "v1.2.4", "source": { "type": "git", "url": "https://github.com/laravel/pail.git", - "reference": "8cc3d575c1f0e57eeb923f366a37528c50d2385a" + "reference": "49f92285ff5d6fc09816e976a004f8dec6a0ea30" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/pail/zipball/8cc3d575c1f0e57eeb923f366a37528c50d2385a", - "reference": "8cc3d575c1f0e57eeb923f366a37528c50d2385a", + "url": "https://api.github.com/repos/laravel/pail/zipball/49f92285ff5d6fc09816e976a004f8dec6a0ea30", + "reference": "49f92285ff5d6fc09816e976a004f8dec6a0ea30", "shasum": "" }, "require": { @@ -8567,9 +8839,9 @@ "require-dev": { "laravel/framework": "^10.24|^11.0|^12.0", "laravel/pint": "^1.13", - "orchestra/testbench-core": "^8.13|^9.0|^10.0", - "pestphp/pest": "^2.20|^3.0", - "pestphp/pest-plugin-type-coverage": "^2.3|^3.0", + "orchestra/testbench-core": "^8.13|^9.17|^10.8", + "pestphp/pest": "^2.20|^3.0|^4.0", + "pestphp/pest-plugin-type-coverage": "^2.3|^3.0|^4.0", "phpstan/phpstan": "^1.12.27", "symfony/var-dumper": "^6.3|^7.0" }, @@ -8616,20 +8888,20 @@ "issues": "https://github.com/laravel/pail/issues", "source": "https://github.com/laravel/pail" }, - "time": "2025-06-05T13:55:57+00:00" + "time": "2025-11-20T16:29:35+00:00" }, { "name": "laravel/pint", - "version": "v1.25.1", + "version": "v1.27.0", "source": { "type": "git", "url": "https://github.com/laravel/pint.git", - "reference": "5016e263f95d97670d71b9a987bd8996ade6d8d9" + "reference": "c67b4195b75491e4dfc6b00b1c78b68d86f54c90" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/pint/zipball/5016e263f95d97670d71b9a987bd8996ade6d8d9", - "reference": "5016e263f95d97670d71b9a987bd8996ade6d8d9", + "url": "https://api.github.com/repos/laravel/pint/zipball/c67b4195b75491e4dfc6b00b1c78b68d86f54c90", + "reference": "c67b4195b75491e4dfc6b00b1c78b68d86f54c90", "shasum": "" }, "require": { @@ -8640,13 +8912,13 @@ "php": "^8.2.0" }, "require-dev": { - "friendsofphp/php-cs-fixer": "^3.87.2", - "illuminate/view": "^11.46.0", - "larastan/larastan": "^3.7.1", - "laravel-zero/framework": "^11.45.0", + "friendsofphp/php-cs-fixer": "^3.92.4", + "illuminate/view": "^12.44.0", + "larastan/larastan": "^3.8.1", + "laravel-zero/framework": "^12.0.4", "mockery/mockery": "^1.6.12", - "nunomaduro/termwind": "^2.3.1", - "pestphp/pest": "^2.36.0" + "nunomaduro/termwind": "^2.3.3", + "pestphp/pest": "^3.8.4" }, "bin": [ "builds/pint" @@ -8672,6 +8944,7 @@ "description": "An opinionated code formatter for PHP.", "homepage": "https://laravel.com", "keywords": [ + "dev", "format", "formatter", "lint", @@ -8682,7 +8955,7 @@ "issues": "https://github.com/laravel/pint/issues", "source": "https://github.com/laravel/pint" }, - "time": "2025-09-19T02:57:12+00:00" + "time": "2026-01-05T16:49:17+00:00" }, { "name": "laravel/roster", @@ -8747,16 +9020,16 @@ }, { "name": "laravel/sail", - "version": "v1.47.0", + "version": "v1.52.0", "source": { "type": "git", "url": "https://github.com/laravel/sail.git", - "reference": "9a11e822238167ad8b791e4ea51155d25cf4d8f2" + "reference": "64ac7d8abb2dbcf2b76e61289451bae79066b0b3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/sail/zipball/9a11e822238167ad8b791e4ea51155d25cf4d8f2", - "reference": "9a11e822238167ad8b791e4ea51155d25cf4d8f2", + "url": "https://api.github.com/repos/laravel/sail/zipball/64ac7d8abb2dbcf2b76e61289451bae79066b0b3", + "reference": "64ac7d8abb2dbcf2b76e61289451bae79066b0b3", "shasum": "" }, "require": { @@ -8806,7 +9079,7 @@ "issues": "https://github.com/laravel/sail/issues", "source": "https://github.com/laravel/sail" }, - "time": "2025-10-28T13:55:29+00:00" + "time": "2026-01-01T02:46:03+00:00" }, { "name": "mockery/mockery", @@ -8953,16 +9226,16 @@ }, { "name": "nunomaduro/collision", - "version": "v8.8.2", + "version": "v8.8.3", "source": { "type": "git", "url": "https://github.com/nunomaduro/collision.git", - "reference": "60207965f9b7b7a4ce15a0f75d57f9dadb105bdb" + "reference": "1dc9e88d105699d0fee8bb18890f41b274f6b4c4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nunomaduro/collision/zipball/60207965f9b7b7a4ce15a0f75d57f9dadb105bdb", - "reference": "60207965f9b7b7a4ce15a0f75d57f9dadb105bdb", + "url": "https://api.github.com/repos/nunomaduro/collision/zipball/1dc9e88d105699d0fee8bb18890f41b274f6b4c4", + "reference": "1dc9e88d105699d0fee8bb18890f41b274f6b4c4", "shasum": "" }, "require": { @@ -8984,7 +9257,7 @@ "laravel/sanctum": "^4.1.1", "laravel/tinker": "^2.10.1", "orchestra/testbench-core": "^9.12.0 || ^10.4", - "pestphp/pest": "^3.8.2", + "pestphp/pest": "^3.8.2 || ^4.0.0", "sebastian/environment": "^7.2.1 || ^8.0" }, "type": "library", @@ -9048,45 +9321,45 @@ "type": "patreon" } ], - "time": "2025-06-25T02:12:12+00:00" + "time": "2025-11-20T02:55:25+00:00" }, { "name": "pestphp/pest", - "version": "v4.1.3", + "version": "v4.3.1", "source": { "type": "git", "url": "https://github.com/pestphp/pest.git", - "reference": "477d20a54fd9329ddfb0f8d4eb90dca7bc81b027" + "reference": "bc57a84e77afd4544ff9643a6858f68d05aeab96" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/pestphp/pest/zipball/477d20a54fd9329ddfb0f8d4eb90dca7bc81b027", - "reference": "477d20a54fd9329ddfb0f8d4eb90dca7bc81b027", + "url": "https://api.github.com/repos/pestphp/pest/zipball/bc57a84e77afd4544ff9643a6858f68d05aeab96", + "reference": "bc57a84e77afd4544ff9643a6858f68d05aeab96", "shasum": "" }, "require": { - "brianium/paratest": "^7.14.0", - "nunomaduro/collision": "^8.8.2", - "nunomaduro/termwind": "^2.3.1", + "brianium/paratest": "^7.16.0", + "nunomaduro/collision": "^8.8.3", + "nunomaduro/termwind": "^2.3.3", "pestphp/pest-plugin": "^4.0.0", "pestphp/pest-plugin-arch": "^4.0.0", "pestphp/pest-plugin-mutate": "^4.0.1", - "pestphp/pest-plugin-profanity": "^4.1.0", + "pestphp/pest-plugin-profanity": "^4.2.1", "php": "^8.3.0", - "phpunit/phpunit": "^12.4.1", - "symfony/process": "^7.3.4" + "phpunit/phpunit": "^12.5.4", + "symfony/process": "^7.4.3|^8.0.0" }, "conflict": { "filp/whoops": "<2.18.3", - "phpunit/phpunit": ">12.4.1", + "phpunit/phpunit": ">12.5.4", "sebastian/exporter": "<7.0.0", "webmozart/assert": "<1.11.0" }, "require-dev": { "pestphp/pest-dev-tools": "^4.0.0", "pestphp/pest-plugin-browser": "^4.1.1", - "pestphp/pest-plugin-type-coverage": "^4.0.2", - "psy/psysh": "^0.12.12" + "pestphp/pest-plugin-type-coverage": "^4.0.3", + "psy/psysh": "^0.12.18" }, "bin": [ "bin/pest" @@ -9152,7 +9425,7 @@ ], "support": { "issues": "https://github.com/pestphp/pest/issues", - "source": "https://github.com/pestphp/pest/tree/v4.1.3" + "source": "https://github.com/pestphp/pest/tree/v4.3.1" }, "funding": [ { @@ -9164,7 +9437,7 @@ "type": "github" } ], - "time": "2025-10-29T22:45:27+00:00" + "time": "2026-01-04T16:29:59+00:00" }, { "name": "pestphp/pest-plugin", @@ -9527,16 +9800,16 @@ }, { "name": "pestphp/pest-plugin-profanity", - "version": "v4.2.0", + "version": "v4.2.1", "source": { "type": "git", "url": "https://github.com/pestphp/pest-plugin-profanity.git", - "reference": "c37e5e2c7136ee4eae12082e7952332bc1c6600a" + "reference": "343cfa6f3564b7e35df0ebb77b7fa97039f72b27" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/pestphp/pest-plugin-profanity/zipball/c37e5e2c7136ee4eae12082e7952332bc1c6600a", - "reference": "c37e5e2c7136ee4eae12082e7952332bc1c6600a", + "url": "https://api.github.com/repos/pestphp/pest-plugin-profanity/zipball/343cfa6f3564b7e35df0ebb77b7fa97039f72b27", + "reference": "343cfa6f3564b7e35df0ebb77b7fa97039f72b27", "shasum": "" }, "require": { @@ -9577,9 +9850,9 @@ "unit" ], "support": { - "source": "https://github.com/pestphp/pest-plugin-profanity/tree/v4.2.0" + "source": "https://github.com/pestphp/pest-plugin-profanity/tree/v4.2.1" }, - "time": "2025-10-28T23:14:11+00:00" + "time": "2025-12-08T00:13:17+00:00" }, { "name": "phar-io/manifest", @@ -9754,16 +10027,16 @@ }, { "name": "phpdocumentor/reflection-docblock", - "version": "5.6.3", + "version": "5.6.6", "source": { "type": "git", "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", - "reference": "94f8051919d1b0369a6bcc7931d679a511c03fe9" + "reference": "5cee1d3dfc2d2aa6599834520911d246f656bcb8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/94f8051919d1b0369a6bcc7931d679a511c03fe9", - "reference": "94f8051919d1b0369a6bcc7931d679a511c03fe9", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/5cee1d3dfc2d2aa6599834520911d246f656bcb8", + "reference": "5cee1d3dfc2d2aa6599834520911d246f656bcb8", "shasum": "" }, "require": { @@ -9773,7 +10046,7 @@ "phpdocumentor/reflection-common": "^2.2", "phpdocumentor/type-resolver": "^1.7", "phpstan/phpdoc-parser": "^1.7|^2.0", - "webmozart/assert": "^1.9.1" + "webmozart/assert": "^1.9.1 || ^2" }, "require-dev": { "mockery/mockery": "~1.3.5 || ~1.6.0", @@ -9812,22 +10085,22 @@ "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", "support": { "issues": "https://github.com/phpDocumentor/ReflectionDocBlock/issues", - "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/5.6.3" + "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/5.6.6" }, - "time": "2025-08-01T19:43:32+00:00" + "time": "2025-12-22T21:13:58+00:00" }, { "name": "phpdocumentor/type-resolver", - "version": "1.10.0", + "version": "1.12.0", "source": { "type": "git", "url": "https://github.com/phpDocumentor/TypeResolver.git", - "reference": "679e3ce485b99e84c775d28e2e96fade9a7fb50a" + "reference": "92a98ada2b93d9b201a613cb5a33584dde25f195" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/679e3ce485b99e84c775d28e2e96fade9a7fb50a", - "reference": "679e3ce485b99e84c775d28e2e96fade9a7fb50a", + "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/92a98ada2b93d9b201a613cb5a33584dde25f195", + "reference": "92a98ada2b93d9b201a613cb5a33584dde25f195", "shasum": "" }, "require": { @@ -9870,9 +10143,9 @@ "description": "A PSR-5 based resolver of Class names, Types and Structural Element Names", "support": { "issues": "https://github.com/phpDocumentor/TypeResolver/issues", - "source": "https://github.com/phpDocumentor/TypeResolver/tree/1.10.0" + "source": "https://github.com/phpDocumentor/TypeResolver/tree/1.12.0" }, - "time": "2024-11-09T15:12:26+00:00" + "time": "2025-11-21T15:09:14+00:00" }, { "name": "phpstan/phpdoc-parser", @@ -9923,11 +10196,11 @@ }, { "name": "phpstan/phpstan", - "version": "2.1.31", + "version": "2.1.33", "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/ead89849d879fe203ce9292c6ef5e7e76f867b96", - "reference": "ead89849d879fe203ce9292c6ef5e7e76f867b96", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/9e800e6bee7d5bd02784d4c6069b48032d16224f", + "reference": "9e800e6bee7d5bd02784d4c6069b48032d16224f", "shasum": "" }, "require": { @@ -9972,27 +10245,27 @@ "type": "github" } ], - "time": "2025-10-10T14:14:11+00:00" + "time": "2025-12-05T10:24:31+00:00" }, { "name": "phpunit/php-code-coverage", - "version": "12.4.0", + "version": "12.5.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "67e8aed88f93d0e6e1cb7effe1a2dfc2fee6022c" + "reference": "4a9739b51cbcb355f6e95659612f92e282a7077b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/67e8aed88f93d0e6e1cb7effe1a2dfc2fee6022c", - "reference": "67e8aed88f93d0e6e1cb7effe1a2dfc2fee6022c", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/4a9739b51cbcb355f6e95659612f92e282a7077b", + "reference": "4a9739b51cbcb355f6e95659612f92e282a7077b", "shasum": "" }, "require": { "ext-dom": "*", "ext-libxml": "*", "ext-xmlwriter": "*", - "nikic/php-parser": "^5.6.1", + "nikic/php-parser": "^5.7.0", "php": ">=8.3", "phpunit/php-file-iterator": "^6.0", "phpunit/php-text-template": "^5.0", @@ -10000,10 +10273,10 @@ "sebastian/environment": "^8.0.3", "sebastian/lines-of-code": "^4.0", "sebastian/version": "^6.0", - "theseer/tokenizer": "^1.2.3" + "theseer/tokenizer": "^2.0.1" }, "require-dev": { - "phpunit/phpunit": "^12.3.7" + "phpunit/phpunit": "^12.5.1" }, "suggest": { "ext-pcov": "PHP extension that provides line coverage", @@ -10012,7 +10285,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "12.4.x-dev" + "dev-main": "12.5.x-dev" } }, "autoload": { @@ -10041,7 +10314,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", - "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/12.4.0" + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/12.5.2" }, "funding": [ { @@ -10061,7 +10334,7 @@ "type": "tidelift" } ], - "time": "2025-09-24T13:44:41+00:00" + "time": "2025-12-24T07:03:04+00:00" }, { "name": "phpunit/php-file-iterator", @@ -10310,16 +10583,16 @@ }, { "name": "phpunit/phpunit", - "version": "12.4.1", + "version": "12.5.4", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "fc5413a2e6d240d2f6d9317bdf7f0a24e73de194" + "reference": "4ba0e923f9d3fc655de22f9547c01d15a41fc93a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/fc5413a2e6d240d2f6d9317bdf7f0a24e73de194", - "reference": "fc5413a2e6d240d2f6d9317bdf7f0a24e73de194", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/4ba0e923f9d3fc655de22f9547c01d15a41fc93a", + "reference": "4ba0e923f9d3fc655de22f9547c01d15a41fc93a", "shasum": "" }, "require": { @@ -10333,7 +10606,7 @@ "phar-io/manifest": "^2.0.4", "phar-io/version": "^3.2.1", "php": ">=8.3", - "phpunit/php-code-coverage": "^12.4.0", + "phpunit/php-code-coverage": "^12.5.1", "phpunit/php-file-iterator": "^6.0.0", "phpunit/php-invoker": "^6.0.0", "phpunit/php-text-template": "^5.0.0", @@ -10355,7 +10628,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "12.4-dev" + "dev-main": "12.5-dev" } }, "autoload": { @@ -10387,7 +10660,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/12.4.1" + "source": "https://github.com/sebastianbergmann/phpunit/tree/12.5.4" }, "funding": [ { @@ -10411,25 +10684,25 @@ "type": "tidelift" } ], - "time": "2025-10-09T14:08:29+00:00" + "time": "2025-12-15T06:05:34+00:00" }, { "name": "rector/rector", - "version": "2.2.7", + "version": "2.3.0", "source": { "type": "git", "url": "https://github.com/rectorphp/rector.git", - "reference": "022038537838bc8a4e526af86c2d6e38eaeff7ef" + "reference": "f7166355dcf47482f27be59169b0825995f51c7d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/rectorphp/rector/zipball/022038537838bc8a4e526af86c2d6e38eaeff7ef", - "reference": "022038537838bc8a4e526af86c2d6e38eaeff7ef", + "url": "https://api.github.com/repos/rectorphp/rector/zipball/f7166355dcf47482f27be59169b0825995f51c7d", + "reference": "f7166355dcf47482f27be59169b0825995f51c7d", "shasum": "" }, "require": { "php": "^7.4|^8.0", - "phpstan/phpstan": "^2.1.26" + "phpstan/phpstan": "^2.1.33" }, "conflict": { "rector/rector-doctrine": "*", @@ -10463,7 +10736,7 @@ ], "support": { "issues": "https://github.com/rectorphp/rector/issues", - "source": "https://github.com/rectorphp/rector/tree/2.2.7" + "source": "https://github.com/rectorphp/rector/tree/2.3.0" }, "funding": [ { @@ -10471,7 +10744,7 @@ "type": "github" } ], - "time": "2025-10-29T15:46:12+00:00" + "time": "2025-12-25T22:00:18+00:00" }, { "name": "sebastian/cli-parser", @@ -11483,23 +11756,23 @@ }, { "name": "theseer/tokenizer", - "version": "1.2.3", + "version": "2.0.1", "source": { "type": "git", "url": "https://github.com/theseer/tokenizer.git", - "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2" + "reference": "7989e43bf381af0eac72e4f0ca5bcbfa81658be4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/theseer/tokenizer/zipball/737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", - "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/7989e43bf381af0eac72e4f0ca5bcbfa81658be4", + "reference": "7989e43bf381af0eac72e4f0ca5bcbfa81658be4", "shasum": "" }, "require": { "ext-dom": "*", "ext-tokenizer": "*", "ext-xmlwriter": "*", - "php": "^7.2 || ^8.0" + "php": "^8.1" }, "type": "library", "autoload": { @@ -11521,7 +11794,7 @@ "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", "support": { "issues": "https://github.com/theseer/tokenizer/issues", - "source": "https://github.com/theseer/tokenizer/tree/1.2.3" + "source": "https://github.com/theseer/tokenizer/tree/2.0.1" }, "funding": [ { @@ -11529,27 +11802,27 @@ "type": "github" } ], - "time": "2024-03-03T12:36:25+00:00" + "time": "2025-12-08T11:19:18+00:00" }, { "name": "webmozart/assert", - "version": "1.12.1", + "version": "2.1.1", "source": { "type": "git", "url": "https://github.com/webmozarts/assert.git", - "reference": "9be6926d8b485f55b9229203f962b51ed377ba68" + "reference": "bdbabc199a7ba9965484e4725d66170e5711323b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/webmozarts/assert/zipball/9be6926d8b485f55b9229203f962b51ed377ba68", - "reference": "9be6926d8b485f55b9229203f962b51ed377ba68", + "url": "https://api.github.com/repos/webmozarts/assert/zipball/bdbabc199a7ba9965484e4725d66170e5711323b", + "reference": "bdbabc199a7ba9965484e4725d66170e5711323b", "shasum": "" }, "require": { "ext-ctype": "*", "ext-date": "*", "ext-filter": "*", - "php": "^7.2 || ^8.0" + "php": "^8.2" }, "suggest": { "ext-intl": "", @@ -11559,7 +11832,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.10-dev" + "dev-feature/2-0": "2.0-dev" } }, "autoload": { @@ -11575,6 +11848,10 @@ { "name": "Bernhard Schussek", "email": "bschussek@gmail.com" + }, + { + "name": "Woody Gilk", + "email": "woody.gilk@gmail.com" } ], "description": "Assertions to validate method input/output with nice error messages.", @@ -11585,9 +11862,9 @@ ], "support": { "issues": "https://github.com/webmozarts/assert/issues", - "source": "https://github.com/webmozarts/assert/tree/1.12.1" + "source": "https://github.com/webmozarts/assert/tree/2.1.1" }, - "time": "2025-10-29T15:56:20+00:00" + "time": "2026-01-08T11:28:40+00:00" } ], "aliases": [], @@ -11602,5 +11879,5 @@ "ext-zip": "*" }, "platform-dev": {}, - "plugin-api-version": "2.6.0" + "plugin-api-version": "2.9.0" } diff --git a/config/app.php b/config/app.php index 73bcaaf..c7cb051 100644 --- a/config/app.php +++ b/config/app.php @@ -130,7 +130,7 @@ return [ 'force_https' => env('FORCE_HTTPS', false), 'puppeteer_docker' => env('PUPPETEER_DOCKER', false), 'puppeteer_mode' => env('PUPPETEER_MODE', 'local'), - 'puppeteer_wait_for_network_idle' => env('PUPPETEER_WAIT_FOR_NETWORK_IDLE', false), + 'puppeteer_wait_for_network_idle' => env('PUPPETEER_WAIT_FOR_NETWORK_IDLE', true), 'puppeteer_window_size_strategy' => env('PUPPETEER_WINDOW_SIZE_STRATEGY', null), 'notifications' => [ diff --git a/config/services.php b/config/services.php index 5cb8a74..d97255a 100644 --- a/config/services.php +++ b/config/services.php @@ -41,6 +41,8 @@ return [ 'proxy_refresh_cron' => env('TRMNL_PROXY_REFRESH_CRON'), 'override_orig_icon' => env('TRMNL_OVERRIDE_ORIG_ICON', false), 'image_url_timeout' => env('TRMNL_IMAGE_URL_TIMEOUT', 30), // 30 seconds; increase on low-powered devices + 'liquid_enabled' => env('TRMNL_LIQUID_ENABLED', false), + 'liquid_path' => env('TRMNL_LIQUID_PATH', '/usr/local/bin/trmnl-liquid-cli'), ], 'webhook' => [ diff --git a/config/trustedproxy.php b/config/trustedproxy.php new file mode 100644 index 0000000..8557288 --- /dev/null +++ b/config/trustedproxy.php @@ -0,0 +1,6 @@ + ($proxies = env('TRUSTED_PROXIES', '')) === '*' ? '*' : array_filter(explode(',', $proxies)), +]; diff --git a/database/factories/DevicePaletteFactory.php b/database/factories/DevicePaletteFactory.php new file mode 100644 index 0000000..1d7ed2d --- /dev/null +++ b/database/factories/DevicePaletteFactory.php @@ -0,0 +1,38 @@ + + */ +class DevicePaletteFactory extends Factory +{ + protected $model = DevicePalette::class; + + /** + * Define the model's default state. + * + * @return array + */ + public function definition(): array + { + return [ + 'id' => 'test-'.$this->faker->unique()->slug(), + 'name' => $this->faker->words(3, true), + 'grays' => $this->faker->randomElement([2, 4, 16, 256]), + 'colors' => $this->faker->optional()->passthrough([ + '#FF0000', + '#00FF00', + '#0000FF', + '#FFFF00', + '#000000', + '#FFFFFF', + ]), + 'framework_class' => null, + 'source' => 'api', + ]; + } +} diff --git a/database/factories/PluginFactory.php b/database/factories/PluginFactory.php index a2d2e65..10a1580 100644 --- a/database/factories/PluginFactory.php +++ b/database/factories/PluginFactory.php @@ -29,8 +29,24 @@ class PluginFactory extends Factory 'icon_url' => null, 'flux_icon_name' => null, 'author_name' => $this->faker->name(), + 'plugin_type' => 'recipe', 'created_at' => Carbon::now(), 'updated_at' => Carbon::now(), ]; } + + /** + * Indicate that the plugin is an image webhook plugin. + */ + public function imageWebhook(): static + { + return $this->state(fn (array $attributes): array => [ + 'plugin_type' => 'image_webhook', + 'data_strategy' => 'static', + 'data_stale_minutes' => 60, + 'polling_url' => null, + 'polling_verb' => 'get', + 'name' => $this->faker->randomElement(['Camera Feed', 'Security Camera', 'Webcam', 'Image Stream']), + ]); + } } diff --git a/database/migrations/2025_08_04_064514_add_oidc_sub_to_users_table.php b/database/migrations/2025_08_04_064514_add_oidc_sub_to_users_table.php index d8dba38..7ec1374 100644 --- a/database/migrations/2025_08_04_064514_add_oidc_sub_to_users_table.php +++ b/database/migrations/2025_08_04_064514_add_oidc_sub_to_users_table.php @@ -22,6 +22,7 @@ return new class extends Migration public function down(): void { Schema::table('users', function (Blueprint $table) { + $table->dropUnique(['oidc_sub']); $table->dropColumn('oidc_sub'); }); } diff --git a/database/migrations/2025_11_03_213452_add_preferred_renderer_to_plugins_table.php b/database/migrations/2025_11_03_213452_add_preferred_renderer_to_plugins_table.php new file mode 100644 index 0000000..a998420 --- /dev/null +++ b/database/migrations/2025_11_03_213452_add_preferred_renderer_to_plugins_table.php @@ -0,0 +1,28 @@ +string('preferred_renderer')->nullable()->after('markup_language'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('plugins', function (Blueprint $table) { + $table->dropColumn('preferred_renderer'); + }); + } +}; diff --git a/database/migrations/2025_11_22_084119_create_device_palettes_table.php b/database/migrations/2025_11_22_084119_create_device_palettes_table.php new file mode 100644 index 0000000..9262dac --- /dev/null +++ b/database/migrations/2025_11_22_084119_create_device_palettes_table.php @@ -0,0 +1,33 @@ +id(); + $table->string('name')->unique(); + $table->string('description')->nullable(); + $table->integer('grays'); + $table->json('colors')->nullable(); + $table->string('framework_class')->default(''); + $table->string('source')->default('api'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('device_palettes'); + } +}; diff --git a/database/migrations/2025_11_22_084208_add_palette_id_to_device_models_table.php b/database/migrations/2025_11_22_084208_add_palette_id_to_device_models_table.php new file mode 100644 index 0000000..1993fcf --- /dev/null +++ b/database/migrations/2025_11_22_084208_add_palette_id_to_device_models_table.php @@ -0,0 +1,29 @@ +foreignId('palette_id')->nullable()->after('source')->constrained('device_palettes')->onDelete('set null'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('device_models', function (Blueprint $table) { + $table->dropForeign(['palette_id']); + $table->dropColumn('palette_id'); + }); + } +}; diff --git a/database/migrations/2025_11_22_084211_add_palette_id_to_devices_table.php b/database/migrations/2025_11_22_084211_add_palette_id_to_devices_table.php new file mode 100644 index 0000000..3a47afe --- /dev/null +++ b/database/migrations/2025_11_22_084211_add_palette_id_to_devices_table.php @@ -0,0 +1,29 @@ +foreignId('palette_id')->nullable()->constrained('device_palettes')->onDelete('set null'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('devices', function (Blueprint $table) { + $table->dropForeign(['palette_id']); + $table->dropColumn('palette_id'); + }); + } +}; diff --git a/database/migrations/2025_11_22_084425_seed_palettes_and_relationships.php b/database/migrations/2025_11_22_084425_seed_palettes_and_relationships.php new file mode 100644 index 0000000..c198d81 --- /dev/null +++ b/database/migrations/2025_11_22_084425_seed_palettes_and_relationships.php @@ -0,0 +1,124 @@ + 'bw', + 'description' => 'Black & White', + 'grays' => 2, + 'colors' => null, + 'framework_class' => 'screen--1bit', + 'source' => 'api', + ], + [ + 'name' => 'gray-4', + 'description' => '4 Grays', + 'grays' => 4, + 'colors' => null, + 'framework_class' => 'screen--2bit', + 'source' => 'api', + ], + [ + 'name' => 'gray-16', + 'description' => '16 Grays', + 'grays' => 16, + 'colors' => null, + 'framework_class' => 'screen--4bit', + 'source' => 'api', + ], + [ + 'name' => 'gray-256', + 'description' => '256 Grays', + 'grays' => 256, + 'colors' => null, + 'framework_class' => 'screen--4bit', + 'source' => 'api', + ], + [ + 'name' => 'color-6a', + 'description' => '6 Colors', + 'grays' => 2, + 'colors' => json_encode(['#FF0000', '#00FF00', '#0000FF', '#FFFF00', '#000000', '#FFFFFF']), + 'framework_class' => '', + 'source' => 'api', + ], + [ + 'name' => 'color-7a', + 'description' => '7 Colors', + 'grays' => 2, + 'colors' => json_encode(['#000000', '#FFFFFF', '#FF0000', '#00FF00', '#0000FF', '#FFFF00', '#FFA500']), + 'framework_class' => '', + 'source' => 'api', + ], + ]; + + $now = now(); + $paletteIdMap = []; + + foreach ($palettes as $paletteData) { + $paletteName = $paletteData['name']; + $paletteData['created_at'] = $now; + $paletteData['updated_at'] = $now; + + DB::table('device_palettes')->updateOrInsert( + ['name' => $paletteName], + $paletteData + ); + + // Get the ID of the palette (either newly created or existing) + $paletteRecord = DB::table('device_palettes')->where('name', $paletteName)->first(); + $paletteIdMap[$paletteName] = $paletteRecord->id; + } + + // Set default palette_id on DeviceModel based on first palette_ids entry + $models = [ + ['name' => 'og_png', 'palette_name' => 'bw'], + ['name' => 'og_plus', 'palette_name' => 'gray-4'], + ['name' => 'amazon_kindle_2024', 'palette_name' => 'gray-256'], + ['name' => 'amazon_kindle_paperwhite_6th_gen', 'palette_name' => 'gray-256'], + ['name' => 'amazon_kindle_paperwhite_7th_gen', 'palette_name' => 'gray-256'], + ['name' => 'inkplate_10', 'palette_name' => 'gray-4'], + ['name' => 'amazon_kindle_7', 'palette_name' => 'gray-256'], + ['name' => 'inky_impression_7_3', 'palette_name' => 'color-7a'], + ['name' => 'kobo_libra_2', 'palette_name' => 'gray-16'], + ['name' => 'amazon_kindle_oasis_2', 'palette_name' => 'gray-256'], + ['name' => 'kobo_aura_one', 'palette_name' => 'gray-16'], + ['name' => 'kobo_aura_hd', 'palette_name' => 'gray-16'], + ['name' => 'inky_impression_13_3', 'palette_name' => 'color-6a'], + ['name' => 'm5_paper_s3', 'palette_name' => 'gray-16'], + ['name' => 'amazon_kindle_scribe', 'palette_name' => 'gray-256'], + ['name' => 'seeed_e1001', 'palette_name' => 'gray-4'], + ['name' => 'seeed_e1002', 'palette_name' => 'gray-4'], + ['name' => 'waveshare_4_26', 'palette_name' => 'gray-4'], + ['name' => 'waveshare_7_5_bw', 'palette_name' => 'bw'], + ]; + + foreach ($models as $modelData) { + $deviceModel = DeviceModel::where('name', $modelData['name'])->first(); + if ($deviceModel && ! $deviceModel->palette_id && isset($paletteIdMap[$modelData['palette_name']])) { + $deviceModel->update(['palette_id' => $paletteIdMap[$modelData['palette_name']]]); + } + } + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + // Remove palette_id from device models but keep palettes + DeviceModel::query()->update(['palette_id' => null]); + } +}; diff --git a/database/migrations/2025_12_02_154228_add_timezone_to_users_table.php b/database/migrations/2025_12_02_154228_add_timezone_to_users_table.php new file mode 100644 index 0000000..8a92627 --- /dev/null +++ b/database/migrations/2025_12_02_154228_add_timezone_to_users_table.php @@ -0,0 +1,28 @@ +string('timezone')->nullable()->after('oidc_sub'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('users', function (Blueprint $table) { + $table->dropColumn('timezone'); + }); + } +}; diff --git a/database/migrations/2026_01_05_153321_add_plugin_type_to_plugins_table.php b/database/migrations/2026_01_05_153321_add_plugin_type_to_plugins_table.php new file mode 100644 index 0000000..558fe2c --- /dev/null +++ b/database/migrations/2026_01_05_153321_add_plugin_type_to_plugins_table.php @@ -0,0 +1,28 @@ +string('plugin_type')->default('recipe')->after('uuid'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('plugins', function (Blueprint $table): void { + $table->dropColumn('plugin_type'); + }); + } +}; diff --git a/database/migrations/2026_01_08_173521_add_kind_to_device_models_table.php b/database/migrations/2026_01_08_173521_add_kind_to_device_models_table.php new file mode 100644 index 0000000..d230657 --- /dev/null +++ b/database/migrations/2026_01_08_173521_add_kind_to_device_models_table.php @@ -0,0 +1,33 @@ +string('kind')->nullable()->index(); + }); + + // Set existing og_png and og_plus to kind "trmnl" + DeviceModel::whereIn('name', ['og_png', 'og_plus'])->update(['kind' => 'trmnl']); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('device_models', function (Blueprint $table) { + $table->dropIndex(['kind']); + $table->dropColumn('kind'); + }); + } +}; diff --git a/database/migrations/2026_01_11_121809_make_trmnlp_id_unique_in_plugins_table.php b/database/migrations/2026_01_11_121809_make_trmnlp_id_unique_in_plugins_table.php new file mode 100644 index 0000000..9769505 --- /dev/null +++ b/database/migrations/2026_01_11_121809_make_trmnlp_id_unique_in_plugins_table.php @@ -0,0 +1,60 @@ +select('user_id', 'trmnlp_id', DB::raw('COUNT(*) as count')) + ->whereNotNull('trmnlp_id') + ->groupBy('user_id', 'trmnlp_id') + ->having('count', '>', 1) + ->get(); + + // For each duplicate combination, keep the first one (by id) and set others to null + foreach ($duplicates as $duplicate) { + $plugins = DB::table('plugins') + ->where('user_id', $duplicate->user_id) + ->where('trmnlp_id', $duplicate->trmnlp_id) + ->orderBy('id') + ->get(); + + // Keep the first one, set the rest to null + $keepFirst = true; + foreach ($plugins as $plugin) { + if ($keepFirst) { + $keepFirst = false; + + continue; + } + + DB::table('plugins') + ->where('id', $plugin->id) + ->update(['trmnlp_id' => null]); + } + } + + Schema::table('plugins', function (Blueprint $table) { + $table->unique(['user_id', 'trmnlp_id']); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('plugins', function (Blueprint $table) { + $table->dropUnique(['user_id', 'trmnlp_id']); + }); + } +}; diff --git a/database/migrations/2026_01_11_173757_add_alias_to_plugins_table.php b/database/migrations/2026_01_11_173757_add_alias_to_plugins_table.php new file mode 100644 index 0000000..0a527d7 --- /dev/null +++ b/database/migrations/2026_01_11_173757_add_alias_to_plugins_table.php @@ -0,0 +1,28 @@ +boolean('alias')->default(false); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('plugins', function (Blueprint $table) { + $table->dropColumn('alias'); + }); + } +}; diff --git a/database/seeders/ExampleRecipesSeeder.php b/database/seeders/ExampleRecipesSeeder.php index 9d8e9bb..5474615 100644 --- a/database/seeders/ExampleRecipesSeeder.php +++ b/database/seeders/ExampleRecipesSeeder.php @@ -144,5 +144,42 @@ class ExampleRecipesSeeder extends Seeder 'flux_icon_name' => 'flower', ] ); + + Plugin::updateOrCreate( + [ + 'uuid' => '1d98bca4-837d-4b01-b1a1-e3b6e56eca90', + 'name' => 'Holidays (iCal)', + 'user_id' => $user_id, + 'data_payload' => null, + 'data_stale_minutes' => 720, + 'data_strategy' => 'polling', + 'configuration_template' => [ + 'custom_fields' => [ + [ + 'keyname' => 'calendar', + 'field_type' => 'select', + 'name' => 'Public Holidays Calendar', + 'options' => [ + ['USA' => 'usa'], + ['Austria' => 'austria'], + ['Australia' => 'australia'], + ['Canada' => 'canada'], + ['Germany' => 'germany'], + ['UK' => 'united-kingdom'], + ], + ], + ], + ], + 'configuration' => ['calendar' => 'usa'], + 'polling_url' => 'https://www.officeholidays.com/ics-clean/{{calendar}}', + 'polling_verb' => 'get', + 'polling_header' => null, + 'render_markup' => null, + 'render_markup_view' => 'recipes.holidays-ical', + 'detail_view_route' => null, + 'icon_url' => null, + 'flux_icon_name' => 'calendar', + ] + ); } } diff --git a/package-lock.json b/package-lock.json index 27ee26a..e722432 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,5 +1,5 @@ { - "name": "laravel-trmnl-server", + "name": "laravel", "lockfileVersion": 3, "requires": true, "packages": { @@ -23,7 +23,7 @@ "codemirror": "^6.0.2", "concurrently": "^9.0.1", "laravel-vite-plugin": "^2.0", - "puppeteer": "24.17.0", + "puppeteer": "24.30.0", "tailwindcss": "^4.0.7", "vite": "^7.0.4" }, @@ -48,18 +48,18 @@ } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", - "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@codemirror/autocomplete": { - "version": "6.19.0", - "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.19.0.tgz", - "integrity": "sha512-61Hfv3cF07XvUxNeC3E7jhG8XNi1Yom1G0lRC936oLnlF+jrbrv8rc/J98XlYzcsAoTVupfsf5fLej1aI8kyIg==", + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.20.0.tgz", + "integrity": "sha512-bOwvTOIJcG5FVo5gUUupiwYh8MioPLQ4UcqbcRf7UQ98X90tCa9E1kZ3Z7tqwpZxYyOvh1YTYbmZE9RTfTp5hg==", "license": "MIT", "dependencies": { "@codemirror/language": "^6.0.0", @@ -69,9 +69,9 @@ } }, "node_modules/@codemirror/commands": { - "version": "6.9.0", - "resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.9.0.tgz", - "integrity": "sha512-454TVgjhO6cMufsyyGN70rGIfJxJEjcqjBG2x2Y03Y/+Fm99d3O/Kv1QDYWuG6hvxsgmjXmBuATikIIYvERX+w==", + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.10.0.tgz", + "integrity": "sha512-2xUIc5mHXQzT16JnyOFkh8PvfeXuIut3pslWGfsGOhxP/lpgRm9HOl/mpzLErgt5mXDovqA0d11P21gofRLb9w==", "license": "MIT", "dependencies": { "@codemirror/language": "^6.0.0", @@ -156,7 +156,6 @@ "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.11.3.tgz", "integrity": "sha512-9HBM2XnwDj7fnu0551HkGdrUrrqmYq/WC5iv6nbY2WdicXdGbhR/gfbZOH73Aqj4351alY1+aoG9rCNfiwS1RA==", "license": "MIT", - "peer": true, "dependencies": { "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.23.0", @@ -167,9 +166,9 @@ } }, "node_modules/@codemirror/lint": { - "version": "6.9.0", - "resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.9.0.tgz", - "integrity": "sha512-wZxW+9XDytH3SKvS8cQzMyQCaaazH8XL1EMHleHe00wVzsv7NBQKVW2yzEHrRhmM7ZOhVdItPbvlRBvMp9ej7A==", + "version": "6.9.2", + "resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.9.2.tgz", + "integrity": "sha512-sv3DylBiIyi+xKwRCJAAsBZZZWo82shJ/RTMymLabAdtbkV5cSKwWDeCgtUq3v8flTaXS2y1kKkICuRYtUswyQ==", "license": "MIT", "dependencies": { "@codemirror/state": "^6.0.0", @@ -193,7 +192,6 @@ "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.2.tgz", "integrity": "sha512-FVqsPqtPWKVVL3dPSxy8wEF/ymIEuVzF1PK3VbUgrxXpJUSHQWWZz4JMToquRxnkw+36LTamCZG2iua2Ptq0fA==", "license": "MIT", - "peer": true, "dependencies": { "@marijn/find-cluster-break": "^1.0.0" } @@ -211,11 +209,10 @@ } }, "node_modules/@codemirror/view": { - "version": "6.38.5", - "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.38.5.tgz", - "integrity": "sha512-SFVsNAgsAoou+BjRewMqN+m9jaztB9wCWN9RSRgePqUbq8UVlvJfku5zB2KVhLPgH/h0RLk38tvd4tGeAhygnw==", + "version": "6.38.8", + "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.38.8.tgz", + "integrity": "sha512-XcE9fcnkHCbWkjeKyi0lllwXmBLtyYb5dt89dJyx23I9+LSh5vZDIuk7OLG4VM1lgrXZQcY6cxyZyk5WVPRv/A==", "license": "MIT", - "peer": true, "dependencies": { "@codemirror/state": "^6.5.0", "crelt": "^1.0.6", @@ -224,9 +221,9 @@ } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.10.tgz", - "integrity": "sha512-0NFWnA+7l41irNuaSVlLfgNT12caWJVLzp5eAVhZ0z1qpxbockccEt3s+149rE64VUI3Ml2zt8Nv5JVc4QXTsw==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", "cpu": [ "ppc64" ], @@ -240,9 +237,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.10.tgz", - "integrity": "sha512-dQAxF1dW1C3zpeCDc5KqIYuZ1tgAdRXNoZP7vkBIRtKZPYe2xVr/d3SkirklCHudW1B45tGiUlz2pUWDfbDD4w==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", "cpu": [ "arm" ], @@ -256,9 +253,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.10.tgz", - "integrity": "sha512-LSQa7eDahypv/VO6WKohZGPSJDq5OVOo3UoFR1E4t4Gj1W7zEQMUhI+lo81H+DtB+kP+tDgBp+M4oNCwp6kffg==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", "cpu": [ "arm64" ], @@ -272,9 +269,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.10.tgz", - "integrity": "sha512-MiC9CWdPrfhibcXwr39p9ha1x0lZJ9KaVfvzA0Wxwz9ETX4v5CHfF09bx935nHlhi+MxhA63dKRRQLiVgSUtEg==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", "cpu": [ "x64" ], @@ -288,9 +285,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.10.tgz", - "integrity": "sha512-JC74bdXcQEpW9KkV326WpZZjLguSZ3DfS8wrrvPMHgQOIEIG/sPXEN/V8IssoJhbefLRcRqw6RQH2NnpdprtMA==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", "cpu": [ "arm64" ], @@ -304,9 +301,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.10.tgz", - "integrity": "sha512-tguWg1olF6DGqzws97pKZ8G2L7Ig1vjDmGTwcTuYHbuU6TTjJe5FXbgs5C1BBzHbJ2bo1m3WkQDbWO2PvamRcg==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", "cpu": [ "x64" ], @@ -320,9 +317,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.10.tgz", - "integrity": "sha512-3ZioSQSg1HT2N05YxeJWYR+Libe3bREVSdWhEEgExWaDtyFbbXWb49QgPvFH8u03vUPX10JhJPcz7s9t9+boWg==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", "cpu": [ "arm64" ], @@ -336,9 +333,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.10.tgz", - "integrity": "sha512-LLgJfHJk014Aa4anGDbh8bmI5Lk+QidDmGzuC2D+vP7mv/GeSN+H39zOf7pN5N8p059FcOfs2bVlrRr4SK9WxA==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", "cpu": [ "x64" ], @@ -352,9 +349,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.10.tgz", - "integrity": "sha512-oR31GtBTFYCqEBALI9r6WxoU/ZofZl962pouZRTEYECvNF/dtXKku8YXcJkhgK/beU+zedXfIzHijSRapJY3vg==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", "cpu": [ "arm" ], @@ -368,9 +365,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.10.tgz", - "integrity": "sha512-5luJWN6YKBsawd5f9i4+c+geYiVEw20FVW5x0v1kEMWNq8UctFjDiMATBxLvmmHA4bf7F6hTRaJgtghFr9iziQ==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", "cpu": [ "arm64" ], @@ -384,9 +381,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.10.tgz", - "integrity": "sha512-NrSCx2Kim3EnnWgS4Txn0QGt0Xipoumb6z6sUtl5bOEZIVKhzfyp/Lyw4C1DIYvzeW/5mWYPBFJU3a/8Yr75DQ==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", "cpu": [ "ia32" ], @@ -400,9 +397,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.10.tgz", - "integrity": "sha512-xoSphrd4AZda8+rUDDfD9J6FUMjrkTz8itpTITM4/xgerAZZcFW7Dv+sun7333IfKxGG8gAq+3NbfEMJfiY+Eg==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", "cpu": [ "loong64" ], @@ -416,9 +413,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.10.tgz", - "integrity": "sha512-ab6eiuCwoMmYDyTnyptoKkVS3k8fy/1Uvq7Dj5czXI6DF2GqD2ToInBI0SHOp5/X1BdZ26RKc5+qjQNGRBelRA==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", "cpu": [ "mips64el" ], @@ -432,9 +429,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.10.tgz", - "integrity": "sha512-NLinzzOgZQsGpsTkEbdJTCanwA5/wozN9dSgEl12haXJBzMTpssebuXR42bthOF3z7zXFWH1AmvWunUCkBE4EA==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", "cpu": [ "ppc64" ], @@ -448,9 +445,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.10.tgz", - "integrity": "sha512-FE557XdZDrtX8NMIeA8LBJX3dC2M8VGXwfrQWU7LB5SLOajfJIxmSdyL/gU1m64Zs9CBKvm4UAuBp5aJ8OgnrA==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", "cpu": [ "riscv64" ], @@ -464,9 +461,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.10.tgz", - "integrity": "sha512-3BBSbgzuB9ajLoVZk0mGu+EHlBwkusRmeNYdqmznmMc9zGASFjSsxgkNsqmXugpPk00gJ0JNKh/97nxmjctdew==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", "cpu": [ "s390x" ], @@ -480,9 +477,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.10.tgz", - "integrity": "sha512-QSX81KhFoZGwenVyPoberggdW1nrQZSvfVDAIUXr3WqLRZGZqWk/P4T8p2SP+de2Sr5HPcvjhcJzEiulKgnxtA==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", "cpu": [ "x64" ], @@ -496,9 +493,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.10.tgz", - "integrity": "sha512-AKQM3gfYfSW8XRk8DdMCzaLUFB15dTrZfnX8WXQoOUpUBQ+NaAFCP1kPS/ykbbGYz7rxn0WS48/81l9hFl3u4A==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", "cpu": [ "arm64" ], @@ -512,9 +509,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.10.tgz", - "integrity": "sha512-7RTytDPGU6fek/hWuN9qQpeGPBZFfB4zZgcz2VK2Z5VpdUxEI8JKYsg3JfO0n/Z1E/6l05n0unDCNc4HnhQGig==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", "cpu": [ "x64" ], @@ -528,9 +525,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.10.tgz", - "integrity": "sha512-5Se0VM9Wtq797YFn+dLimf2Zx6McttsH2olUBsDml+lm0GOCRVebRWUvDtkY4BWYv/3NgzS8b/UM3jQNh5hYyw==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", "cpu": [ "arm64" ], @@ -544,9 +541,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.10.tgz", - "integrity": "sha512-XkA4frq1TLj4bEMB+2HnI0+4RnjbuGZfet2gs/LNs5Hc7D89ZQBHQ0gL2ND6Lzu1+QVkjp3x1gIcPKzRNP8bXw==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", "cpu": [ "x64" ], @@ -560,9 +557,9 @@ } }, "node_modules/@esbuild/openharmony-arm64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.10.tgz", - "integrity": "sha512-AVTSBhTX8Y/Fz6OmIVBip9tJzZEUcY8WLh7I59+upa5/GPhh2/aM6bvOMQySspnCCHvFi79kMtdJS1w0DXAeag==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", "cpu": [ "arm64" ], @@ -576,9 +573,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.10.tgz", - "integrity": "sha512-fswk3XT0Uf2pGJmOpDB7yknqhVkJQkAQOcW/ccVOtfx05LkbWOaRAtn5SaqXypeKQra1QaEa841PgrSL9ubSPQ==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", "cpu": [ "x64" ], @@ -592,9 +589,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.10.tgz", - "integrity": "sha512-ah+9b59KDTSfpaCg6VdJoOQvKjI33nTaQr4UluQwW7aEwZQsbMCfTmfEO4VyewOxx4RaDT/xCy9ra2GPWmO7Kw==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", "cpu": [ "arm64" ], @@ -608,9 +605,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.10.tgz", - "integrity": "sha512-QHPDbKkrGO8/cz9LKVnJU22HOi4pxZnZhhA2HYHez5Pz4JeffhDjf85E57Oyco163GnzNCVkZK0b/n4Y0UHcSw==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", "cpu": [ "ia32" ], @@ -624,9 +621,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.10.tgz", - "integrity": "sha512-9KpxSVFCu0iK1owoez6aC/s/EdUQLDN3adTxGCqxMVhrPDj6bt5dbrHDXUuq+Bs2vATFBBrQS5vdQ/Ed2P+nbw==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", "cpu": [ "x64" ], @@ -640,9 +637,9 @@ } }, "node_modules/@fsegurai/codemirror-theme-github-light": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/@fsegurai/codemirror-theme-github-light/-/codemirror-theme-github-light-6.2.2.tgz", - "integrity": "sha512-YQr5MbhMlhRlAQcSCSbet4NDDkMvd5sbUyk9JmM0vfZhQbatvw4c56gNG/54JKGM0kWY5zRWzgLtFuz6D7yEsw==", + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/@fsegurai/codemirror-theme-github-light/-/codemirror-theme-github-light-6.2.3.tgz", + "integrity": "sha512-vbwyznBoTrLQdWvQ6/vjIpoDojd7VIMK+sQnMXkKOjXbm5cGul6A3mqM2RSt9Z5NhIRikmxKkApflvWOJrDuWA==", "license": "MIT", "peerDependencies": { "@codemirror/language": "^6.0.0", @@ -651,18 +648,6 @@ "@lezer/highlight": "^1.0.0" } }, - "node_modules/@isaacs/fs-minipass": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", - "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", - "license": "ISC", - "dependencies": { - "minipass": "^7.0.4" - }, - "engines": { - "node": ">=18.0.0" - } - }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", @@ -709,9 +694,9 @@ } }, "node_modules/@lezer/common": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.2.3.tgz", - "integrity": "sha512-w7ojc8ejBqr2REPsWxJjrMFsA/ysDCFICn8zEOR9mrqzOu2amhITYuLD8ag6XZf0CFXDrhKqw7+tW8cX66NaDA==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.4.0.tgz", + "integrity": "sha512-DVeMRoGrgn/k45oQNu189BoW4SZwgZFzJ1+1TV5j2NJ/KFC83oa/enRqZSGshyeMk5cPWMhsKs9nx+8o0unwGg==", "license": "MIT" }, "node_modules/@lezer/css": { @@ -726,13 +711,12 @@ } }, "node_modules/@lezer/highlight": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.1.tgz", - "integrity": "sha512-Z5duk4RN/3zuVO7Jq0pGLJ3qynpxUVsh7IbUbGj88+uV2ApSAn6kWg2au3iJb+0Zi7kKtqffIESgNcRXWZWmSA==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.3.tgz", + "integrity": "sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g==", "license": "MIT", - "peer": true, "dependencies": { - "@lezer/common": "^1.0.0" + "@lezer/common": "^1.3.0" } }, "node_modules/@lezer/html": { @@ -769,9 +753,9 @@ } }, "node_modules/@lezer/lr": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.2.tgz", - "integrity": "sha512-pu0K1jCIdnQ12aWNaAVU5bzi7Bd1w54J3ECgANPmYLtQKP0HBj2cE/5coBD66MT10xbtIuUr7tg0Shbsvk0mDA==", + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.4.tgz", + "integrity": "sha512-LHL17Mq0OcFXm1pGQssuGTQFPPdxARjKM8f7GA5+sGtHi0K3R84YaSbmche0+RKWHnCsx9asEe5OWOI4FHfe4A==", "license": "MIT", "dependencies": { "@lezer/common": "^1.0.0" @@ -784,17 +768,17 @@ "license": "MIT" }, "node_modules/@puppeteer/browsers": { - "version": "2.10.7", - "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.10.7.tgz", - "integrity": "sha512-wHWLkQWBjHtajZeqCB74nsa/X70KheyOhySYBRmVQDJiNj0zjZR/naPCvdWjMhcG1LmjaMV/9WtTo5mpe8qWLw==", + "version": "2.10.13", + "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.10.13.tgz", + "integrity": "sha512-a9Ruw3j3qlnB5a/zHRTkruppynxqaeE4H9WNj5eYGRWqw0ZauZ23f4W2ARf3hghF5doozyD+CRtt7XSYuYRI/Q==", "license": "Apache-2.0", "dependencies": { - "debug": "^4.4.1", + "debug": "^4.4.3", "extract-zip": "^2.0.1", "progress": "^2.0.3", "proxy-agent": "^6.5.0", - "semver": "^7.7.2", - "tar-fs": "^3.1.0", + "semver": "^7.7.3", + "tar-fs": "^3.1.1", "yargs": "^17.7.2" }, "bin": { @@ -805,9 +789,9 @@ } }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.52.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.52.4.tgz", - "integrity": "sha512-BTm2qKNnWIQ5auf4deoetINJm2JzvihvGb9R6K/ETwKLql/Bb3Eg2H1FBp1gUb4YGbydMA3jcmQTR73q7J+GAA==", + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.53.3.tgz", + "integrity": "sha512-mRSi+4cBjrRLoaal2PnqH82Wqyb+d3HsPUN/W+WslCXsZsyHa9ZeQQX/pQsZaVIWDkPcpV6jJ+3KLbTbgnwv8w==", "cpu": [ "arm" ], @@ -818,9 +802,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.52.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.52.4.tgz", - "integrity": "sha512-P9LDQiC5vpgGFgz7GSM6dKPCiqR3XYN1WwJKA4/BUVDjHpYsf3iBEmVz62uyq20NGYbiGPR5cNHI7T1HqxNs2w==", + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.53.3.tgz", + "integrity": "sha512-CbDGaMpdE9sh7sCmTrTUyllhrg65t6SwhjlMJsLr+J8YjFuPmCEjbBSx4Z/e4SmDyH3aB5hGaJUP2ltV/vcs4w==", "cpu": [ "arm64" ], @@ -831,9 +815,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.52.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.52.4.tgz", - "integrity": "sha512-QRWSW+bVccAvZF6cbNZBJwAehmvG9NwfWHwMy4GbWi/BQIA/laTIktebT2ipVjNncqE6GLPxOok5hsECgAxGZg==", + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.53.3.tgz", + "integrity": "sha512-Nr7SlQeqIBpOV6BHHGZgYBuSdanCXuw09hon14MGOLGmXAFYjx1wNvquVPmpZnl0tLjg25dEdr4IQ6GgyToCUA==", "cpu": [ "arm64" ], @@ -844,9 +828,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.52.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.52.4.tgz", - "integrity": "sha512-hZgP05pResAkRJxL1b+7yxCnXPGsXU0fG9Yfd6dUaoGk+FhdPKCJ5L1Sumyxn8kvw8Qi5PvQ8ulenUbRjzeCTw==", + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.53.3.tgz", + "integrity": "sha512-DZ8N4CSNfl965CmPktJ8oBnfYr3F8dTTNBQkRlffnUarJ2ohudQD17sZBa097J8xhQ26AwhHJ5mvUyQW8ddTsQ==", "cpu": [ "x64" ], @@ -857,9 +841,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.52.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.52.4.tgz", - "integrity": "sha512-xmc30VshuBNUd58Xk4TKAEcRZHaXlV+tCxIXELiE9sQuK3kG8ZFgSPi57UBJt8/ogfhAF5Oz4ZSUBN77weM+mQ==", + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.53.3.tgz", + "integrity": "sha512-yMTrCrK92aGyi7GuDNtGn2sNW+Gdb4vErx4t3Gv/Tr+1zRb8ax4z8GWVRfr3Jw8zJWvpGHNpss3vVlbF58DZ4w==", "cpu": [ "arm64" ], @@ -870,9 +854,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.52.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.52.4.tgz", - "integrity": "sha512-WdSLpZFjOEqNZGmHflxyifolwAiZmDQzuOzIq9L27ButpCVpD7KzTRtEG1I0wMPFyiyUdOO+4t8GvrnBLQSwpw==", + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.53.3.tgz", + "integrity": "sha512-lMfF8X7QhdQzseM6XaX0vbno2m3hlyZFhwcndRMw8fbAGUGL3WFMBdK0hbUBIUYcEcMhVLr1SIamDeuLBnXS+Q==", "cpu": [ "x64" ], @@ -883,9 +867,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.52.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.52.4.tgz", - "integrity": "sha512-xRiOu9Of1FZ4SxVbB0iEDXc4ddIcjCv2aj03dmW8UrZIW7aIQ9jVJdLBIhxBI+MaTnGAKyvMwPwQnoOEvP7FgQ==", + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.53.3.tgz", + "integrity": "sha512-k9oD15soC/Ln6d2Wv/JOFPzZXIAIFLp6B+i14KhxAfnq76ajt0EhYc5YPeX6W1xJkAdItcVT+JhKl1QZh44/qw==", "cpu": [ "arm" ], @@ -896,9 +880,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.52.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.52.4.tgz", - "integrity": "sha512-FbhM2p9TJAmEIEhIgzR4soUcsW49e9veAQCziwbR+XWB2zqJ12b4i/+hel9yLiD8pLncDH4fKIPIbt5238341Q==", + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.53.3.tgz", + "integrity": "sha512-vTNlKq+N6CK/8UktsrFuc+/7NlEYVxgaEgRXVUVK258Z5ymho29skzW1sutgYjqNnquGwVUObAaxae8rZ6YMhg==", "cpu": [ "arm" ], @@ -909,9 +893,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.52.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.52.4.tgz", - "integrity": "sha512-4n4gVwhPHR9q/g8lKCyz0yuaD0MvDf7dV4f9tHt0C73Mp8h38UCtSCSE6R9iBlTbXlmA8CjpsZoujhszefqueg==", + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.53.3.tgz", + "integrity": "sha512-RGrFLWgMhSxRs/EWJMIFM1O5Mzuz3Xy3/mnxJp/5cVhZ2XoCAxJnmNsEyeMJtpK+wu0FJFWz+QF4mjCA7AUQ3w==", "cpu": [ "arm64" ], @@ -922,9 +906,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.52.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.52.4.tgz", - "integrity": "sha512-u0n17nGA0nvi/11gcZKsjkLj1QIpAuPFQbR48Subo7SmZJnGxDpspyw2kbpuoQnyK+9pwf3pAoEXerJs/8Mi9g==", + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.53.3.tgz", + "integrity": "sha512-kASyvfBEWYPEwe0Qv4nfu6pNkITLTb32p4yTgzFCocHnJLAHs+9LjUu9ONIhvfT/5lv4YS5muBHyuV84epBo/A==", "cpu": [ "arm64" ], @@ -935,9 +919,9 @@ ] }, "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.52.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.52.4.tgz", - "integrity": "sha512-0G2c2lpYtbTuXo8KEJkDkClE/+/2AFPdPAbmaHoE870foRFs4pBrDehilMcrSScrN/fB/1HTaWO4bqw+ewBzMQ==", + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.53.3.tgz", + "integrity": "sha512-JiuKcp2teLJwQ7vkJ95EwESWkNRFJD7TQgYmCnrPtlu50b4XvT5MOmurWNrCj3IFdyjBQ5p9vnrX4JM6I8OE7g==", "cpu": [ "loong64" ], @@ -948,9 +932,9 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.52.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.52.4.tgz", - "integrity": "sha512-teSACug1GyZHmPDv14VNbvZFX779UqWTsd7KtTM9JIZRDI5NUwYSIS30kzI8m06gOPB//jtpqlhmraQ68b5X2g==", + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.53.3.tgz", + "integrity": "sha512-EoGSa8nd6d3T7zLuqdojxC20oBfNT8nexBbB/rkxgKj5T5vhpAQKKnD+h3UkoMuTyXkP5jTjK/ccNRmQrPNDuw==", "cpu": [ "ppc64" ], @@ -961,9 +945,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.52.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.52.4.tgz", - "integrity": "sha512-/MOEW3aHjjs1p4Pw1Xk4+3egRevx8Ji9N6HUIA1Ifh8Q+cg9dremvFCUbOX2Zebz80BwJIgCBUemjqhU5XI5Eg==", + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.53.3.tgz", + "integrity": "sha512-4s+Wped2IHXHPnAEbIB0YWBv7SDohqxobiiPA1FIWZpX+w9o2i4LezzH/NkFUl8LRci/8udci6cLq+jJQlh+0g==", "cpu": [ "riscv64" ], @@ -974,9 +958,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.52.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.52.4.tgz", - "integrity": "sha512-1HHmsRyh845QDpEWzOFtMCph5Ts+9+yllCrREuBR/vg2RogAQGGBRC8lDPrPOMnrdOJ+mt1WLMOC2Kao/UwcvA==", + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.53.3.tgz", + "integrity": "sha512-68k2g7+0vs2u9CxDt5ktXTngsxOQkSEV/xBbwlqYcUrAVh6P9EgMZvFsnHy4SEiUl46Xf0IObWVbMvPrr2gw8A==", "cpu": [ "riscv64" ], @@ -987,9 +971,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.52.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.52.4.tgz", - "integrity": "sha512-seoeZp4L/6D1MUyjWkOMRU6/iLmCU2EjbMTyAG4oIOs1/I82Y5lTeaxW0KBfkUdHAWN7j25bpkt0rjnOgAcQcA==", + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.53.3.tgz", + "integrity": "sha512-VYsFMpULAz87ZW6BVYw3I6sWesGpsP9OPcyKe8ofdg9LHxSbRMd7zrVrr5xi/3kMZtpWL/wC+UIJWJYVX5uTKg==", "cpu": [ "s390x" ], @@ -1013,9 +997,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.52.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.52.4.tgz", - "integrity": "sha512-dtBZYjDmCQ9hW+WgEkaffvRRCKm767wWhxsFW3Lw86VXz/uJRuD438/XvbZT//B96Vs8oTA8Q4A0AfHbrxP9zw==", + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.53.3.tgz", + "integrity": "sha512-eoROhjcc6HbZCJr+tvVT8X4fW3/5g/WkGvvmwz/88sDtSJzO7r/blvoBDgISDiCjDRZmHpwud7h+6Q9JxFwq1Q==", "cpu": [ "x64" ], @@ -1026,9 +1010,9 @@ ] }, "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.52.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.52.4.tgz", - "integrity": "sha512-1ox+GqgRWqaB1RnyZXL8PD6E5f7YyRUJYnCqKpNzxzP0TkaUh112NDrR9Tt+C8rJ4x5G9Mk8PQR3o7Ku2RKqKA==", + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.53.3.tgz", + "integrity": "sha512-OueLAWgrNSPGAdUdIjSWXw+u/02BRTcnfw9PN41D2vq/JSEPnJnVuBgw18VkN8wcd4fjUs+jFHVM4t9+kBSNLw==", "cpu": [ "arm64" ], @@ -1039,9 +1023,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.52.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.52.4.tgz", - "integrity": "sha512-8GKr640PdFNXwzIE0IrkMWUNUomILLkfeHjXBi/nUvFlpZP+FA8BKGKpacjW6OUUHaNI6sUURxR2U2g78FOHWQ==", + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.53.3.tgz", + "integrity": "sha512-GOFuKpsxR/whszbF/bzydebLiXIHSgsEUp6M0JI8dWvi+fFa1TD6YQa4aSZHtpmh2/uAlj/Dy+nmby3TJ3pkTw==", "cpu": [ "arm64" ], @@ -1052,9 +1036,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.52.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.52.4.tgz", - "integrity": "sha512-AIy/jdJ7WtJ/F6EcfOb2GjR9UweO0n43jNObQMb6oGxkYTfLcnN7vYYpG+CN3lLxrQkzWnMOoNSHTW54pgbVxw==", + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.53.3.tgz", + "integrity": "sha512-iah+THLcBJdpfZ1TstDFbKNznlzoxa8fmnFYK4V67HvmuNYkVdAywJSoteUszvBQ9/HqN2+9AZghbajMsFT+oA==", "cpu": [ "ia32" ], @@ -1065,9 +1049,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.52.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.52.4.tgz", - "integrity": "sha512-UF9KfsH9yEam0UjTwAgdK0anlQ7c8/pWPU2yVjyWcF1I1thABt6WXE47cI71pGiZ8wGvxohBoLnxM04L/wj8mQ==", + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.53.3.tgz", + "integrity": "sha512-J9QDiOIZlZLdcot5NXEepDkstocktoVjkaKUtqzgzpt2yWjGlbYiKyp05rWwk4nypbYUNoFAztEgixoLaSETkg==", "cpu": [ "x64" ], @@ -1078,9 +1062,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.52.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.52.4.tgz", - "integrity": "sha512-bf9PtUa0u8IXDVxzRToFQKsNCRz9qLYfR/MpECxl4mRoWYjAeFjgxj1XdZr2M/GNVpT05p+LgQOHopYDlUu6/w==", + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.53.3.tgz", + "integrity": "sha512-UhTd8u31dXadv0MopwGgNOBpUVROFKWVQgAg5N1ESyCz8AuBcMqm4AuTjrwgQKGDfoFuz02EuMRHQIw/frmYKQ==", "cpu": [ "x64" ], @@ -1091,52 +1075,47 @@ ] }, "node_modules/@tailwindcss/node": { - "version": "4.1.14", - "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.14.tgz", - "integrity": "sha512-hpz+8vFk3Ic2xssIA3e01R6jkmsAhvkQdXlEbRTk6S10xDAtiQiM3FyvZVGsucefq764euO/b8WUW9ysLdThHw==", + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.17.tgz", + "integrity": "sha512-csIkHIgLb3JisEFQ0vxr2Y57GUNYh447C8xzwj89U/8fdW8LhProdxvnVH6U8M2Y73QKiTIH+LWbK3V2BBZsAg==", "license": "MIT", "dependencies": { "@jridgewell/remapping": "^2.3.4", "enhanced-resolve": "^5.18.3", - "jiti": "^2.6.0", - "lightningcss": "1.30.1", - "magic-string": "^0.30.19", + "jiti": "^2.6.1", + "lightningcss": "1.30.2", + "magic-string": "^0.30.21", "source-map-js": "^1.2.1", - "tailwindcss": "4.1.14" + "tailwindcss": "4.1.17" } }, "node_modules/@tailwindcss/oxide": { - "version": "4.1.14", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.14.tgz", - "integrity": "sha512-23yx+VUbBwCg2x5XWdB8+1lkPajzLmALEfMb51zZUBYaYVPDQvBSD/WYDqiVyBIo2BZFa3yw1Rpy3G2Jp+K0dw==", - "hasInstallScript": true, + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.17.tgz", + "integrity": "sha512-F0F7d01fmkQhsTjXezGBLdrl1KresJTcI3DB8EkScCldyKp3Msz4hub4uyYaVnk88BAS1g5DQjjF6F5qczheLA==", "license": "MIT", - "dependencies": { - "detect-libc": "^2.0.4", - "tar": "^7.5.1" - }, "engines": { "node": ">= 10" }, "optionalDependencies": { - "@tailwindcss/oxide-android-arm64": "4.1.14", - "@tailwindcss/oxide-darwin-arm64": "4.1.14", - "@tailwindcss/oxide-darwin-x64": "4.1.14", - "@tailwindcss/oxide-freebsd-x64": "4.1.14", - "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.14", - "@tailwindcss/oxide-linux-arm64-gnu": "4.1.14", - "@tailwindcss/oxide-linux-arm64-musl": "4.1.14", - "@tailwindcss/oxide-linux-x64-gnu": "4.1.14", - "@tailwindcss/oxide-linux-x64-musl": "4.1.14", - "@tailwindcss/oxide-wasm32-wasi": "4.1.14", - "@tailwindcss/oxide-win32-arm64-msvc": "4.1.14", - "@tailwindcss/oxide-win32-x64-msvc": "4.1.14" + "@tailwindcss/oxide-android-arm64": "4.1.17", + "@tailwindcss/oxide-darwin-arm64": "4.1.17", + "@tailwindcss/oxide-darwin-x64": "4.1.17", + "@tailwindcss/oxide-freebsd-x64": "4.1.17", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.17", + "@tailwindcss/oxide-linux-arm64-gnu": "4.1.17", + "@tailwindcss/oxide-linux-arm64-musl": "4.1.17", + "@tailwindcss/oxide-linux-x64-gnu": "4.1.17", + "@tailwindcss/oxide-linux-x64-musl": "4.1.17", + "@tailwindcss/oxide-wasm32-wasi": "4.1.17", + "@tailwindcss/oxide-win32-arm64-msvc": "4.1.17", + "@tailwindcss/oxide-win32-x64-msvc": "4.1.17" } }, "node_modules/@tailwindcss/oxide-android-arm64": { - "version": "4.1.14", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.14.tgz", - "integrity": "sha512-a94ifZrGwMvbdeAxWoSuGcIl6/DOP5cdxagid7xJv6bwFp3oebp7y2ImYsnZBMTwjn5Ev5xESvS3FFYUGgPODQ==", + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.17.tgz", + "integrity": "sha512-BMqpkJHgOZ5z78qqiGE6ZIRExyaHyuxjgrJ6eBO5+hfrfGkuya0lYfw8fRHG77gdTjWkNWEEm+qeG2cDMxArLQ==", "cpu": [ "arm64" ], @@ -1150,9 +1129,9 @@ } }, "node_modules/@tailwindcss/oxide-darwin-arm64": { - "version": "4.1.14", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.14.tgz", - "integrity": "sha512-HkFP/CqfSh09xCnrPJA7jud7hij5ahKyWomrC3oiO2U9i0UjP17o9pJbxUN0IJ471GTQQmzwhp0DEcpbp4MZTA==", + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.17.tgz", + "integrity": "sha512-EquyumkQweUBNk1zGEU/wfZo2qkp/nQKRZM8bUYO0J+Lums5+wl2CcG1f9BgAjn/u9pJzdYddHWBiFXJTcxmOg==", "cpu": [ "arm64" ], @@ -1166,9 +1145,9 @@ } }, "node_modules/@tailwindcss/oxide-darwin-x64": { - "version": "4.1.14", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.14.tgz", - "integrity": "sha512-eVNaWmCgdLf5iv6Qd3s7JI5SEFBFRtfm6W0mphJYXgvnDEAZ5sZzqmI06bK6xo0IErDHdTA5/t7d4eTfWbWOFw==", + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.17.tgz", + "integrity": "sha512-gdhEPLzke2Pog8s12oADwYu0IAw04Y2tlmgVzIN0+046ytcgx8uZmCzEg4VcQh+AHKiS7xaL8kGo/QTiNEGRog==", "cpu": [ "x64" ], @@ -1182,9 +1161,9 @@ } }, "node_modules/@tailwindcss/oxide-freebsd-x64": { - "version": "4.1.14", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.14.tgz", - "integrity": "sha512-QWLoRXNikEuqtNb0dhQN6wsSVVjX6dmUFzuuiL09ZeXju25dsei2uIPl71y2Ic6QbNBsB4scwBoFnlBfabHkEw==", + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.17.tgz", + "integrity": "sha512-hxGS81KskMxML9DXsaXT1H0DyA+ZBIbyG/sSAjWNe2EDl7TkPOBI42GBV3u38itzGUOmFfCzk1iAjDXds8Oh0g==", "cpu": [ "x64" ], @@ -1198,9 +1177,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { - "version": "4.1.14", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.14.tgz", - "integrity": "sha512-VB4gjQni9+F0VCASU+L8zSIyjrLLsy03sjcR3bM0V2g4SNamo0FakZFKyUQ96ZVwGK4CaJsc9zd/obQy74o0Fw==", + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.17.tgz", + "integrity": "sha512-k7jWk5E3ldAdw0cNglhjSgv501u7yrMf8oeZ0cElhxU6Y2o7f8yqelOp3fhf7evjIS6ujTI3U8pKUXV2I4iXHQ==", "cpu": [ "arm" ], @@ -1214,9 +1193,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { - "version": "4.1.14", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.14.tgz", - "integrity": "sha512-qaEy0dIZ6d9vyLnmeg24yzA8XuEAD9WjpM5nIM1sUgQ/Zv7cVkharPDQcmm/t/TvXoKo/0knI3me3AGfdx6w1w==", + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.17.tgz", + "integrity": "sha512-HVDOm/mxK6+TbARwdW17WrgDYEGzmoYayrCgmLEw7FxTPLcp/glBisuyWkFz/jb7ZfiAXAXUACfyItn+nTgsdQ==", "cpu": [ "arm64" ], @@ -1230,9 +1209,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-arm64-musl": { - "version": "4.1.14", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.14.tgz", - "integrity": "sha512-ISZjT44s59O8xKsPEIesiIydMG/sCXoMBCqsphDm/WcbnuWLxxb+GcvSIIA5NjUw6F8Tex7s5/LM2yDy8RqYBQ==", + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.17.tgz", + "integrity": "sha512-HvZLfGr42i5anKtIeQzxdkw/wPqIbpeZqe7vd3V9vI3RQxe3xU1fLjss0TjyhxWcBaipk7NYwSrwTwK1hJARMg==", "cpu": [ "arm64" ], @@ -1246,9 +1225,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-x64-gnu": { - "version": "4.1.14", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.14.tgz", - "integrity": "sha512-02c6JhLPJj10L2caH4U0zF8Hji4dOeahmuMl23stk0MU1wfd1OraE7rOloidSF8W5JTHkFdVo/O7uRUJJnUAJg==", + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.17.tgz", + "integrity": "sha512-M3XZuORCGB7VPOEDH+nzpJ21XPvK5PyjlkSFkFziNHGLc5d6g3di2McAAblmaSUNl8IOmzYwLx9NsE7bplNkwQ==", "cpu": [ "x64" ], @@ -1262,9 +1241,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-x64-musl": { - "version": "4.1.14", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.14.tgz", - "integrity": "sha512-TNGeLiN1XS66kQhxHG/7wMeQDOoL0S33x9BgmydbrWAb9Qw0KYdd8o1ifx4HOGDWhVmJ+Ul+JQ7lyknQFilO3Q==", + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.17.tgz", + "integrity": "sha512-k7f+pf9eXLEey4pBlw+8dgfJHY4PZ5qOUFDyNf7SI6lHjQ9Zt7+NcscjpwdCEbYi6FI5c2KDTDWyf2iHcCSyyQ==", "cpu": [ "x64" ], @@ -1278,9 +1257,9 @@ } }, "node_modules/@tailwindcss/oxide-wasm32-wasi": { - "version": "4.1.14", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.14.tgz", - "integrity": "sha512-uZYAsaW/jS/IYkd6EWPJKW/NlPNSkWkBlaeVBi/WsFQNP05/bzkebUL8FH1pdsqx4f2fH/bWFcUABOM9nfiJkQ==", + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.17.tgz", + "integrity": "sha512-cEytGqSSoy7zK4JRWiTCx43FsKP/zGr0CsuMawhH67ONlH+T79VteQeJQRO/X7L0juEUA8ZyuYikcRBf0vsxhg==", "bundleDependencies": [ "@napi-rs/wasm-runtime", "@emnapi/core", @@ -1295,10 +1274,10 @@ "license": "MIT", "optional": true, "dependencies": { - "@emnapi/core": "^1.5.0", - "@emnapi/runtime": "^1.5.0", + "@emnapi/core": "^1.6.0", + "@emnapi/runtime": "^1.6.0", "@emnapi/wasi-threads": "^1.1.0", - "@napi-rs/wasm-runtime": "^1.0.5", + "@napi-rs/wasm-runtime": "^1.0.7", "@tybys/wasm-util": "^0.10.1", "tslib": "^2.4.0" }, @@ -1307,9 +1286,9 @@ } }, "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { - "version": "4.1.14", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.14.tgz", - "integrity": "sha512-Az0RnnkcvRqsuoLH2Z4n3JfAef0wElgzHD5Aky/e+0tBUxUhIeIqFBTMNQvmMRSP15fWwmvjBxZ3Q8RhsDnxAA==", + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.17.tgz", + "integrity": "sha512-JU5AHr7gKbZlOGvMdb4722/0aYbU+tN6lv1kONx0JK2cGsh7g148zVWLM0IKR3NeKLv+L90chBVYcJ8uJWbC9A==", "cpu": [ "arm64" ], @@ -1323,9 +1302,9 @@ } }, "node_modules/@tailwindcss/oxide-win32-x64-msvc": { - "version": "4.1.14", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.14.tgz", - "integrity": "sha512-ttblVGHgf68kEE4om1n/n44I0yGPkCPbLsqzjvybhpwa6mKKtgFfAzy6btc3HRmuW7nHe0OOrSeNP9sQmmH9XA==", + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.17.tgz", + "integrity": "sha512-SKWM4waLuqx0IH+FMDUw6R66Hu4OuTALFgnleKbqhgGU30DY20NORZMZUKgLRjQXNN2TLzKvh48QXTig4h4bGw==", "cpu": [ "x64" ], @@ -1339,14 +1318,14 @@ } }, "node_modules/@tailwindcss/vite": { - "version": "4.1.14", - "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.1.14.tgz", - "integrity": "sha512-BoFUoU0XqgCUS1UXWhmDJroKKhNXeDzD7/XwabjkDIAbMnc4ULn5e2FuEuBbhZ6ENZoSYzKlzvZ44Yr6EUDUSA==", + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.1.17.tgz", + "integrity": "sha512-4+9w8ZHOiGnpcGI6z1TVVfWaX/koK7fKeSYF3qlYg2xpBtbteP2ddBxiarL+HVgfSJGeK5RIxRQmKm4rTJJAwA==", "license": "MIT", "dependencies": { - "@tailwindcss/node": "4.1.14", - "@tailwindcss/oxide": "4.1.14", - "tailwindcss": "4.1.14" + "@tailwindcss/node": "4.1.17", + "@tailwindcss/oxide": "4.1.17", + "tailwindcss": "4.1.17" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7" @@ -1365,13 +1344,13 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "24.7.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.7.0.tgz", - "integrity": "sha512-IbKooQVqUBrlzWTi79E8Fw78l8k1RNtlDDNWsFZs7XonuQSJ8oNYfEeclhprUldXISRMLzBpILuKgPlIxm+/Yw==", + "version": "24.10.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.1.tgz", + "integrity": "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==", "license": "MIT", "optional": true, "dependencies": { - "undici-types": "~7.14.0" + "undici-types": "~7.16.0" } }, "node_modules/@types/yauzl": { @@ -1442,9 +1421,9 @@ "license": "MIT" }, "node_modules/autoprefixer": { - "version": "10.4.21", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.21.tgz", - "integrity": "sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ==", + "version": "10.4.22", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.22.tgz", + "integrity": "sha512-ARe0v/t9gO28Bznv6GgqARmVqcWOV3mfgUPn9becPHMiD3o9BwlRgaeccZnwTpZ7Zwqrm+c1sUSsMxIzQzc8Xg==", "funding": [ { "type": "opencollective", @@ -1461,9 +1440,9 @@ ], "license": "MIT", "dependencies": { - "browserslist": "^4.24.4", - "caniuse-lite": "^1.0.30001702", - "fraction.js": "^4.3.7", + "browserslist": "^4.27.0", + "caniuse-lite": "^1.0.30001754", + "fraction.js": "^5.3.4", "normalize-range": "^0.1.2", "picocolors": "^1.1.1", "postcss-value-parser": "^4.2.0" @@ -1479,9 +1458,9 @@ } }, "node_modules/axios": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.12.2.tgz", - "integrity": "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz", + "integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==", "license": "MIT", "dependencies": { "follow-redirects": "^1.15.6", @@ -1504,15 +1483,23 @@ } }, "node_modules/bare-events": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.7.0.tgz", - "integrity": "sha512-b3N5eTW1g7vXkw+0CXh/HazGTcO5KYuu/RCNaJbDMPI6LHDi+7qe8EmxKUVe1sUbY2KZOVZFyj62x0OEz9qyAA==", - "license": "Apache-2.0" + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.8.2.tgz", + "integrity": "sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==", + "license": "Apache-2.0", + "peerDependencies": { + "bare-abort-controller": "*" + }, + "peerDependenciesMeta": { + "bare-abort-controller": { + "optional": true + } + } }, "node_modules/bare-fs": { - "version": "4.4.5", - "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.4.5.tgz", - "integrity": "sha512-TCtu93KGLu6/aiGWzMr12TmSRS6nKdfhAnzTQRbXoSWxkbb9eRd53jQ51jG7g1gYjjtto3hbBrrhzg6djcgiKg==", + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.5.2.tgz", + "integrity": "sha512-veTnRzkb6aPHOvSKIOy60KzURfBdUflr5VReI+NSaPL6xf+XLdONQgZgpYvUuZLVQ8dCqxpBAudaOM1+KpAUxw==", "license": "Apache-2.0", "optional": true, "dependencies": { @@ -1577,9 +1564,9 @@ } }, "node_modules/bare-url": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.2.2.tgz", - "integrity": "sha512-g+ueNGKkrjMazDG3elZO1pNs3HY5+mMmOet1jtKyhOaCnkLzitxf26z7hoAEkDNgdNmnc1KIlt/dw6Po6xZMpA==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.3.2.tgz", + "integrity": "sha512-ZMq4gd9ngV5aTMa5p9+UfY0b3skwhHELaDkhEHetMdX0LRkW9kzaym4oo/Eh+Ghm0CCDuMTsRIGM/ytUc1ZYmw==", "license": "Apache-2.0", "optional": true, "dependencies": { @@ -1587,9 +1574,9 @@ } }, "node_modules/baseline-browser-mapping": { - "version": "2.8.13", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.13.tgz", - "integrity": "sha512-7s16KR8io8nIBWQyCYhmFhd+ebIzb9VKTzki+wOJXHTxTnV6+mFGH3+Jwn1zoKaY9/H9T/0BcKCZnzXljPnpSQ==", + "version": "2.9.3", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.3.tgz", + "integrity": "sha512-8QdH6czo+G7uBsNo0GiUfouPN1lRzKdJTGnKXwe12gkFbnnOUaUKGN55dMkfy+mnxmvjwl9zcI4VncczcVXDhA==", "license": "Apache-2.0", "bin": { "baseline-browser-mapping": "dist/cli.js" @@ -1605,9 +1592,9 @@ } }, "node_modules/browserslist": { - "version": "4.26.3", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.26.3.tgz", - "integrity": "sha512-lAUU+02RFBuCKQPj/P6NgjlbCnLBMp4UtgTx7vNHd3XSIJF87s9a5rA3aH2yw3GS9DqZAUbOtZdCCiZeVRqt0w==", + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", "funding": [ { "type": "opencollective", @@ -1623,13 +1610,12 @@ } ], "license": "MIT", - "peer": true, "dependencies": { - "baseline-browser-mapping": "^2.8.9", - "caniuse-lite": "^1.0.30001746", - "electron-to-chromium": "^1.5.227", - "node-releases": "^2.0.21", - "update-browserslist-db": "^1.1.3" + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" }, "bin": { "browserslist": "cli.js" @@ -1670,9 +1656,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001748", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001748.tgz", - "integrity": "sha512-5P5UgAr0+aBmNiplks08JLw+AW/XG/SurlgZLgB1dDLfAw7EfRGxIwzPHxdSCGY/BTKDqIVyJL87cCN6s0ZR0w==", + "version": "1.0.30001759", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001759.tgz", + "integrity": "sha512-Pzfx9fOKoKvevQf8oCXoyNRQ5QyxJj+3O0Rqx2V5oxT61KGx8+n6hV/IUyJeifUci2clnmmKVpvtiqRzgiWjSw==", "funding": [ { "type": "opencollective", @@ -1717,19 +1703,10 @@ "node": ">=8" } }, - "node_modules/chownr": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", - "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=18" - } - }, "node_modules/chromium-bidi": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-8.0.0.tgz", - "integrity": "sha512-d1VmE0FD7lxZQHzcDUCKZSNRtRwISXDsdg4HjdTR5+Ll5nQ/vzU12JeNmupD6VWffrPSlrnGhEWlLESKH3VO+g==", + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-11.0.0.tgz", + "integrity": "sha512-cM3DI+OOb89T3wO8cpPSro80Q9eKYJ7hGVXoGS3GkDPxnYSqiv+6xwpIf6XERyJ9Tdsl09hmNmY94BkgZdVekw==", "license": "Apache-2.0", "dependencies": { "mitt": "^3.0.1", @@ -1913,11 +1890,10 @@ } }, "node_modules/devtools-protocol": { - "version": "0.0.1475386", - "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1475386.tgz", - "integrity": "sha512-RQ809ykTfJ+dgj9bftdeL2vRVxASAuGU+I9LEx9Ij5TXU5HrgAQVmzi72VA+mkzscE12uzlRv5/tWWv9R9J1SA==", - "license": "BSD-3-Clause", - "peer": true + "version": "0.0.1521046", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1521046.tgz", + "integrity": "sha512-vhE6eymDQSKWUXwwA37NtTTVEzjtGVfDr3pRbsWEQ5onH/Snp2c+2xZHWJJawG/0hCCJLRGt4xVtEVUVILol4w==", + "license": "BSD-3-Clause" }, "node_modules/dunder-proto": { "version": "1.0.1", @@ -1934,9 +1910,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.232", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.232.tgz", - "integrity": "sha512-ENirSe7wf8WzyPCibqKUG1Cg43cPaxH4wRR7AJsX7MCABCHBIOFqvaYODSLKUuZdraxUTHRE/0A2Aq8BYKEHOg==", + "version": "1.5.266", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.266.tgz", + "integrity": "sha512-kgWEglXvkEfMH7rxP5OSZZwnaDWT7J9EoZCujhnpLbfi0bbNtRkgdX2E3gt0Uer11c61qCYktB3hwkAS325sJg==", "license": "ISC" }, "node_modules/emoji-regex": { @@ -2031,9 +2007,9 @@ } }, "node_modules/esbuild": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.10.tgz", - "integrity": "sha512-9RiGKvCwaqxO2owP61uQ4BgNborAQskMR6QusfWzQqv7AZOg5oGehdY2pRJMTKuwxd1IDBP4rSbI5lHzU7SMsQ==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", "hasInstallScript": true, "license": "MIT", "bin": { @@ -2043,32 +2019,32 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.10", - "@esbuild/android-arm": "0.25.10", - "@esbuild/android-arm64": "0.25.10", - "@esbuild/android-x64": "0.25.10", - "@esbuild/darwin-arm64": "0.25.10", - "@esbuild/darwin-x64": "0.25.10", - "@esbuild/freebsd-arm64": "0.25.10", - "@esbuild/freebsd-x64": "0.25.10", - "@esbuild/linux-arm": "0.25.10", - "@esbuild/linux-arm64": "0.25.10", - "@esbuild/linux-ia32": "0.25.10", - "@esbuild/linux-loong64": "0.25.10", - "@esbuild/linux-mips64el": "0.25.10", - "@esbuild/linux-ppc64": "0.25.10", - "@esbuild/linux-riscv64": "0.25.10", - "@esbuild/linux-s390x": "0.25.10", - "@esbuild/linux-x64": "0.25.10", - "@esbuild/netbsd-arm64": "0.25.10", - "@esbuild/netbsd-x64": "0.25.10", - "@esbuild/openbsd-arm64": "0.25.10", - "@esbuild/openbsd-x64": "0.25.10", - "@esbuild/openharmony-arm64": "0.25.10", - "@esbuild/sunos-x64": "0.25.10", - "@esbuild/win32-arm64": "0.25.10", - "@esbuild/win32-ia32": "0.25.10", - "@esbuild/win32-x64": "0.25.10" + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" } }, "node_modules/escalade": { @@ -2214,9 +2190,9 @@ } }, "node_modules/form-data": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", - "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", "license": "MIT", "dependencies": { "asynckit": "^0.4.0", @@ -2230,15 +2206,15 @@ } }, "node_modules/fraction.js": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", - "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", + "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==", "license": "MIT", "engines": { "node": "*" }, "funding": { - "type": "patreon", + "type": "github", "url": "https://github.com/sponsors/rawify" } }, @@ -2449,9 +2425,9 @@ } }, "node_modules/ip-address": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.0.1.tgz", - "integrity": "sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==", + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz", + "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==", "license": "MIT", "engines": { "node": ">= 12" @@ -2488,9 +2464,9 @@ "license": "MIT" }, "node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "license": "MIT", "dependencies": { "argparse": "^2.0.1" @@ -2525,9 +2501,9 @@ } }, "node_modules/lightningcss": { - "version": "1.30.1", - "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.1.tgz", - "integrity": "sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==", + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.2.tgz", + "integrity": "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==", "license": "MPL-2.0", "dependencies": { "detect-libc": "^2.0.3" @@ -2540,22 +2516,43 @@ "url": "https://opencollective.com/parcel" }, "optionalDependencies": { - "lightningcss-darwin-arm64": "1.30.1", - "lightningcss-darwin-x64": "1.30.1", - "lightningcss-freebsd-x64": "1.30.1", - "lightningcss-linux-arm-gnueabihf": "1.30.1", - "lightningcss-linux-arm64-gnu": "1.30.1", - "lightningcss-linux-arm64-musl": "1.30.1", - "lightningcss-linux-x64-gnu": "1.30.1", - "lightningcss-linux-x64-musl": "1.30.1", - "lightningcss-win32-arm64-msvc": "1.30.1", - "lightningcss-win32-x64-msvc": "1.30.1" + "lightningcss-android-arm64": "1.30.2", + "lightningcss-darwin-arm64": "1.30.2", + "lightningcss-darwin-x64": "1.30.2", + "lightningcss-freebsd-x64": "1.30.2", + "lightningcss-linux-arm-gnueabihf": "1.30.2", + "lightningcss-linux-arm64-gnu": "1.30.2", + "lightningcss-linux-arm64-musl": "1.30.2", + "lightningcss-linux-x64-gnu": "1.30.2", + "lightningcss-linux-x64-musl": "1.30.2", + "lightningcss-win32-arm64-msvc": "1.30.2", + "lightningcss-win32-x64-msvc": "1.30.2" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.30.2.tgz", + "integrity": "sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==", + "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.30.1", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.1.tgz", - "integrity": "sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ==", + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.2.tgz", + "integrity": "sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA==", "cpu": [ "arm64" ], @@ -2573,9 +2570,9 @@ } }, "node_modules/lightningcss-darwin-x64": { - "version": "1.30.1", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.1.tgz", - "integrity": "sha512-k1EvjakfumAQoTfcXUcHQZhSpLlkAuEkdMBsI/ivWw9hL+7FtilQc0Cy3hrx0AAQrVtQAbMI7YjCgYgvn37PzA==", + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.2.tgz", + "integrity": "sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ==", "cpu": [ "x64" ], @@ -2593,9 +2590,9 @@ } }, "node_modules/lightningcss-freebsd-x64": { - "version": "1.30.1", - "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.1.tgz", - "integrity": "sha512-kmW6UGCGg2PcyUE59K5r0kWfKPAVy4SltVeut+umLCFoJ53RdCUWxcRDzO1eTaxf/7Q2H7LTquFHPL5R+Gjyig==", + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.2.tgz", + "integrity": "sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA==", "cpu": [ "x64" ], @@ -2613,9 +2610,9 @@ } }, "node_modules/lightningcss-linux-arm-gnueabihf": { - "version": "1.30.1", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.1.tgz", - "integrity": "sha512-MjxUShl1v8pit+6D/zSPq9S9dQ2NPFSQwGvxBCYaBYLPlCWuPh9/t1MRS8iUaR8i+a6w7aps+B4N0S1TYP/R+Q==", + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.2.tgz", + "integrity": "sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA==", "cpu": [ "arm" ], @@ -2633,9 +2630,9 @@ } }, "node_modules/lightningcss-linux-arm64-gnu": { - "version": "1.30.1", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.1.tgz", - "integrity": "sha512-gB72maP8rmrKsnKYy8XUuXi/4OctJiuQjcuqWNlJQ6jZiWqtPvqFziskH3hnajfvKB27ynbVCucKSm2rkQp4Bw==", + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.2.tgz", + "integrity": "sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==", "cpu": [ "arm64" ], @@ -2653,9 +2650,9 @@ } }, "node_modules/lightningcss-linux-arm64-musl": { - "version": "1.30.1", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.1.tgz", - "integrity": "sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ==", + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.2.tgz", + "integrity": "sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==", "cpu": [ "arm64" ], @@ -2693,9 +2690,9 @@ } }, "node_modules/lightningcss-linux-x64-musl": { - "version": "1.30.1", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.1.tgz", - "integrity": "sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ==", + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.2.tgz", + "integrity": "sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==", "cpu": [ "x64" ], @@ -2713,9 +2710,9 @@ } }, "node_modules/lightningcss-win32-arm64-msvc": { - "version": "1.30.1", - "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.1.tgz", - "integrity": "sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA==", + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.2.tgz", + "integrity": "sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==", "cpu": [ "arm64" ], @@ -2733,9 +2730,9 @@ } }, "node_modules/lightningcss-win32-x64-msvc": { - "version": "1.30.1", - "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.1.tgz", - "integrity": "sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg==", + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.2.tgz", + "integrity": "sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw==", "cpu": [ "x64" ], @@ -2752,26 +2749,6 @@ "url": "https://opencollective.com/parcel" } }, - "node_modules/lightningcss/node_modules/lightningcss-linux-x64-gnu": { - "version": "1.30.1", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.1.tgz", - "integrity": "sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw==", - "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/lines-and-columns": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", @@ -2788,9 +2765,9 @@ } }, "node_modules/magic-string": { - "version": "0.30.19", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.19.tgz", - "integrity": "sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==", + "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" @@ -2826,27 +2803,6 @@ "node": ">= 0.6" } }, - "node_modules/minipass": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", - "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", - "license": "ISC", - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, - "node_modules/minizlib": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", - "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", - "license": "MIT", - "dependencies": { - "minipass": "^7.1.2" - }, - "engines": { - "node": ">= 18" - } - }, "node_modules/mitt": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", @@ -2887,9 +2843,9 @@ } }, "node_modules/node-releases": { - "version": "2.0.23", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.23.tgz", - "integrity": "sha512-cCmFDMSm26S6tQSDpBCg/NR8NENrVPhAJSf+XbxBG4rPFaaonlEoE9wHQmun+cls499TQGSb7ZyPBRlzgKfpeg==", + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", "license": "MIT" }, "node_modules/normalize-range": { @@ -2989,7 +2945,6 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -3016,7 +2971,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -3077,17 +3031,17 @@ } }, "node_modules/puppeteer": { - "version": "24.17.0", - "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-24.17.0.tgz", - "integrity": "sha512-CGrmJ8WgilK3nyE73k+pbxHggETPpEvL6AQ9H5JSK1RgZRGMQVJ+iO3MocGm9yBQXQJ9U5xijyLvkYXFeb0/+g==", + "version": "24.30.0", + "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-24.30.0.tgz", + "integrity": "sha512-A5OtCi9WpiXBQgJ2vQiZHSyrAzQmO/WDsvghqlN4kgw21PhxA5knHUaUQq/N3EMt8CcvSS0RM+kmYLJmedR3TQ==", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { - "@puppeteer/browsers": "2.10.7", - "chromium-bidi": "8.0.0", + "@puppeteer/browsers": "2.10.13", + "chromium-bidi": "11.0.0", "cosmiconfig": "^9.0.0", - "devtools-protocol": "0.0.1475386", - "puppeteer-core": "24.17.0", + "devtools-protocol": "0.0.1521046", + "puppeteer-core": "24.30.0", "typed-query-selector": "^2.12.0" }, "bin": { @@ -3098,16 +3052,17 @@ } }, "node_modules/puppeteer-core": { - "version": "24.17.0", - "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-24.17.0.tgz", - "integrity": "sha512-RYOBKFiF+3RdwIZTEacqNpD567gaFcBAOKTT7742FdB1icXudrPI7BlZbYTYWK2wgGQUXt9Zi1Yn+D5PmCs4CA==", + "version": "24.30.0", + "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-24.30.0.tgz", + "integrity": "sha512-2S3Smy0t0W4wJnNvDe7W0bE7wDmZjfZ3ljfMgJd6hn2Hq/f0jgN+x9PULZo2U3fu5UUIJ+JP8cNUGllu8P91Pg==", "license": "Apache-2.0", "dependencies": { - "@puppeteer/browsers": "2.10.7", - "chromium-bidi": "8.0.0", - "debug": "^4.4.1", - "devtools-protocol": "0.0.1475386", + "@puppeteer/browsers": "2.10.13", + "chromium-bidi": "11.0.0", + "debug": "^4.4.3", + "devtools-protocol": "0.0.1521046", "typed-query-selector": "^2.12.0", + "webdriver-bidi-protocol": "0.3.8", "ws": "^8.18.3" }, "engines": { @@ -3133,9 +3088,9 @@ } }, "node_modules/rollup": { - "version": "4.52.4", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.52.4.tgz", - "integrity": "sha512-CLEVl+MnPAiKh5pl4dEWSyMTpuflgNQiLGhMv8ezD5W/qP8AKvmYpCOKRRNOh7oRKnauBZ4SyeYkMS+1VSyKwQ==", + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.53.3.tgz", + "integrity": "sha512-w8GmOxZfBmKknvdXU1sdM9NHcoQejwF/4mNgj2JuEEdRaHwwF12K7e9eXn1nLZ07ad+du76mkVsyeb2rKGllsA==", "license": "MIT", "dependencies": { "@types/estree": "1.0.8" @@ -3148,35 +3103,35 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.52.4", - "@rollup/rollup-android-arm64": "4.52.4", - "@rollup/rollup-darwin-arm64": "4.52.4", - "@rollup/rollup-darwin-x64": "4.52.4", - "@rollup/rollup-freebsd-arm64": "4.52.4", - "@rollup/rollup-freebsd-x64": "4.52.4", - "@rollup/rollup-linux-arm-gnueabihf": "4.52.4", - "@rollup/rollup-linux-arm-musleabihf": "4.52.4", - "@rollup/rollup-linux-arm64-gnu": "4.52.4", - "@rollup/rollup-linux-arm64-musl": "4.52.4", - "@rollup/rollup-linux-loong64-gnu": "4.52.4", - "@rollup/rollup-linux-ppc64-gnu": "4.52.4", - "@rollup/rollup-linux-riscv64-gnu": "4.52.4", - "@rollup/rollup-linux-riscv64-musl": "4.52.4", - "@rollup/rollup-linux-s390x-gnu": "4.52.4", - "@rollup/rollup-linux-x64-gnu": "4.52.4", - "@rollup/rollup-linux-x64-musl": "4.52.4", - "@rollup/rollup-openharmony-arm64": "4.52.4", - "@rollup/rollup-win32-arm64-msvc": "4.52.4", - "@rollup/rollup-win32-ia32-msvc": "4.52.4", - "@rollup/rollup-win32-x64-gnu": "4.52.4", - "@rollup/rollup-win32-x64-msvc": "4.52.4", + "@rollup/rollup-android-arm-eabi": "4.53.3", + "@rollup/rollup-android-arm64": "4.53.3", + "@rollup/rollup-darwin-arm64": "4.53.3", + "@rollup/rollup-darwin-x64": "4.53.3", + "@rollup/rollup-freebsd-arm64": "4.53.3", + "@rollup/rollup-freebsd-x64": "4.53.3", + "@rollup/rollup-linux-arm-gnueabihf": "4.53.3", + "@rollup/rollup-linux-arm-musleabihf": "4.53.3", + "@rollup/rollup-linux-arm64-gnu": "4.53.3", + "@rollup/rollup-linux-arm64-musl": "4.53.3", + "@rollup/rollup-linux-loong64-gnu": "4.53.3", + "@rollup/rollup-linux-ppc64-gnu": "4.53.3", + "@rollup/rollup-linux-riscv64-gnu": "4.53.3", + "@rollup/rollup-linux-riscv64-musl": "4.53.3", + "@rollup/rollup-linux-s390x-gnu": "4.53.3", + "@rollup/rollup-linux-x64-gnu": "4.53.3", + "@rollup/rollup-linux-x64-musl": "4.53.3", + "@rollup/rollup-openharmony-arm64": "4.53.3", + "@rollup/rollup-win32-arm64-msvc": "4.53.3", + "@rollup/rollup-win32-ia32-msvc": "4.53.3", + "@rollup/rollup-win32-x64-gnu": "4.53.3", + "@rollup/rollup-win32-x64-msvc": "4.53.3", "fsevents": "~2.3.2" } }, "node_modules/rollup/node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.52.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.52.4.tgz", - "integrity": "sha512-Wi6AXf0k0L7E2gteNsNHUs7UMwCIhsCTs6+tqQ5GPwVRWMaflqGec4Sd8n6+FNFDw9vGcReqk2KzBDhCa1DLYg==", + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.53.3.tgz", + "integrity": "sha512-3EhFi1FU6YL8HTUJZ51imGJWEX//ajQPfqWLI3BQq4TlvHy4X0MOr5q3D2Zof/ka0d5FNdPwZXm3Yyib/UEd+w==", "cpu": [ "x64" ], @@ -3314,9 +3269,9 @@ } }, "node_modules/style-mod": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.2.tgz", - "integrity": "sha512-wnD1HyVqpJUI2+eKZ+eo1UwghftP6yuFheBqqe+bWCotBjC2K1YnteJILRMs3SM4V/0dLEW1SC27MWP5y+mwmw==", + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.3.tgz", + "integrity": "sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==", "license": "MIT" }, "node_modules/supports-color": { @@ -3335,9 +3290,9 @@ } }, "node_modules/tailwindcss": { - "version": "4.1.14", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.14.tgz", - "integrity": "sha512-b7pCxjGO98LnxVkKjaZSDeNuljC4ueKUddjENJOADtubtdo8llTaJy7HwBMeLNSSo2N5QIAgklslK1+Ir8r6CA==", + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.17.tgz", + "integrity": "sha512-j9Ee2YjuQqYT9bbRTfTZht9W/ytp5H+jJpZKiYdP/bpnXARAuELt9ofP0lPnmHjbga7SNQIxdTAXCmtKVYjN+Q==", "license": "MIT" }, "node_modules/tapable": { @@ -3353,22 +3308,6 @@ "url": "https://opencollective.com/webpack" } }, - "node_modules/tar": { - "version": "7.5.2", - "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.2.tgz", - "integrity": "sha512-7NyxrTE4Anh8km8iEy7o0QYPs+0JKBTj5ZaqHg6B39erLg0qYXN3BijtShwbsNSvQ+LN75+KV+C4QR/f6Gwnpg==", - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/fs-minipass": "^4.0.0", - "chownr": "^3.0.0", - "minipass": "^7.1.2", - "minizlib": "^3.1.0", - "yallist": "^5.0.0" - }, - "engines": { - "node": ">=18" - } - }, "node_modules/tar-fs": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.1.tgz", @@ -3441,16 +3380,16 @@ "license": "MIT" }, "node_modules/undici-types": { - "version": "7.14.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.14.0.tgz", - "integrity": "sha512-QQiYxHuyZ9gQUIrmPo3IA+hUl4KYk8uSA7cHrcKd/l3p1OTpZcM0Tbp9x7FAtXdAYhlasd60ncPpgu6ihG6TOA==", + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", "license": "MIT", "optional": true }, "node_modules/update-browserslist-db": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", - "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.2.tgz", + "integrity": "sha512-E85pfNzMQ9jpKkA7+TJAi4TJN+tBCuWh5rUcS/sv6cFi+1q9LYDwDI5dpUL0u/73EElyQ8d3TEaeW4sPedBqYA==", "funding": [ { "type": "opencollective", @@ -3478,11 +3417,10 @@ } }, "node_modules/vite": { - "version": "7.1.11", - "resolved": "https://registry.npmjs.org/vite/-/vite-7.1.11.tgz", - "integrity": "sha512-uzcxnSDVjAopEUjljkWh8EIrg6tlzrjFUfMcR1EVsRDGwf/ccef0qQPRyOrROwhrTDaApueq+ja+KLPlzR/zdg==", + "version": "7.2.6", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.2.6.tgz", + "integrity": "sha512-tI2l/nFHC5rLh7+5+o7QjKjSR04ivXDF4jcgV0f/bTQ+OJiITy5S6gaynVsEM+7RqzufMnVbIon6Sr5x1SDYaQ==", "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", @@ -3580,6 +3518,12 @@ "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==", "license": "MIT" }, + "node_modules/webdriver-bidi-protocol": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/webdriver-bidi-protocol/-/webdriver-bidi-protocol-0.3.8.tgz", + "integrity": "sha512-21Yi2GhGntMc671vNBCjiAeEVknXjVRoyu+k+9xOMShu+ZQfpGQwnBqbNz/Sv4GXZ6JmutlPAi2nIJcrymAWuQ==", + "license": "Apache-2.0" + }, "node_modules/wrap-ansi": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", @@ -3633,15 +3577,6 @@ "node": ">=10" } }, - "node_modules/yallist": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", - "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=18" - } - }, "node_modules/yargs": { "version": "17.7.2", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", diff --git a/package.json b/package.json index 4190067..7262ad1 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,7 @@ "codemirror": "^6.0.2", "concurrently": "^9.0.1", "laravel-vite-plugin": "^2.0", - "puppeteer": "24.17.0", + "puppeteer": "24.30.0", "tailwindcss": "^4.0.7", "vite": "^7.0.4" }, diff --git a/public/mirror/assets/apple-touch-icon-120x120.png b/public/mirror/assets/apple-touch-icon-120x120.png new file mode 100644 index 0000000..5e51318 Binary files /dev/null and b/public/mirror/assets/apple-touch-icon-120x120.png differ diff --git a/public/mirror/assets/apple-touch-icon-152x152.png b/public/mirror/assets/apple-touch-icon-152x152.png new file mode 100644 index 0000000..9f8d9e3 Binary files /dev/null and b/public/mirror/assets/apple-touch-icon-152x152.png differ diff --git a/public/mirror/assets/apple-touch-icon-167x167.png b/public/mirror/assets/apple-touch-icon-167x167.png new file mode 100644 index 0000000..79d1211 Binary files /dev/null and b/public/mirror/assets/apple-touch-icon-167x167.png differ diff --git a/public/mirror/assets/apple-touch-icon-180x180.png b/public/mirror/assets/apple-touch-icon-180x180.png new file mode 100644 index 0000000..0499ff4 Binary files /dev/null and b/public/mirror/assets/apple-touch-icon-180x180.png differ diff --git a/public/mirror/assets/apple-touch-icon-76x76.png b/public/mirror/assets/apple-touch-icon-76x76.png new file mode 100644 index 0000000..df3943a Binary files /dev/null and b/public/mirror/assets/apple-touch-icon-76x76.png differ diff --git a/public/mirror/assets/favicon-16x16.png b/public/mirror/assets/favicon-16x16.png new file mode 100644 index 0000000..b36f23b Binary files /dev/null and b/public/mirror/assets/favicon-16x16.png differ diff --git a/public/mirror/assets/favicon-32x32.png b/public/mirror/assets/favicon-32x32.png new file mode 100644 index 0000000..ae12e60 Binary files /dev/null and b/public/mirror/assets/favicon-32x32.png differ diff --git a/public/mirror/assets/favicon.ico b/public/mirror/assets/favicon.ico new file mode 100644 index 0000000..da17cd5 Binary files /dev/null and b/public/mirror/assets/favicon.ico differ diff --git a/public/mirror/assets/logo--brand.svg b/public/mirror/assets/logo--brand.svg new file mode 100644 index 0000000..1b84f50 --- /dev/null +++ b/public/mirror/assets/logo--brand.svg @@ -0,0 +1,139 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/mirror/index.html b/public/mirror/index.html new file mode 100644 index 0000000..64746fe --- /dev/null +++ b/public/mirror/index.html @@ -0,0 +1,521 @@ + + + + + + TRMNL BYOS Laravel Mirror + + + + + + + + + + + + + + + + + + + +
+
+ + + +
+ + + \ No newline at end of file diff --git a/public/mirror/manifest.json b/public/mirror/manifest.json new file mode 100644 index 0000000..4d44e44 --- /dev/null +++ b/public/mirror/manifest.json @@ -0,0 +1,7 @@ +{ + "name": "TRMNL BYOS Laravel Mirror", + "short_name": "TRMNL BYOS", + "display": "standalone", + "background_color": "#ffffff", + "theme_color": "#ffffff" +} diff --git a/resources/css/app.css b/resources/css/app.css index 46b9ca1..de95b81 100644 --- a/resources/css/app.css +++ b/resources/css/app.css @@ -59,6 +59,10 @@ @apply !mb-0 !leading-tight; } +[data-flux-description] a { + @apply text-accent underline hover:opacity-80; +} + input:focus[data-flux-control], textarea:focus[data-flux-control], select:focus[data-flux-control] { @@ -68,3 +72,39 @@ select:focus[data-flux-control] { /* \[:where(&)\]:size-4 { @apply size-4; } */ + +@layer components { + /* standard container for app */ + .styled-container, + .tab-button { + @apply rounded-xl border bg-white dark:bg-stone-950 dark:border-zinc-700 text-stone-800 shadow-xs; + } + + .tab-button { + @apply flex items-center gap-2 px-4 py-2 text-sm font-medium; + @apply rounded-b-none shadow-none bg-inherit; + + /* This makes the button sit slightly over the box border */ + margin-bottom: -1px; + position: relative; + z-index: 1; + } + + .tab-button.is-active { + @apply text-zinc-700 dark:text-zinc-300; + @apply border-b-white dark:border-b-zinc-800; + + /* Z-index 10 ensures the bottom border of the tab hides the top border of the box */ + z-index: 10; + } + + .tab-button:not(.is-active) { + @apply text-zinc-500 border-transparent; + } + + .tab-button:not(.is-active):hover { + @apply text-zinc-700 dark:text-zinc-300; + @apply border-zinc-300 dark:border-zinc-700; + cursor: pointer; + } +} diff --git a/resources/js/codemirror-core.js b/resources/js/codemirror-core.js index c77bf3d..f23389f 100644 --- a/resources/js/codemirror-core.js +++ b/resources/js/codemirror-core.js @@ -1,8 +1,9 @@ import { EditorView, lineNumbers, keymap } from '@codemirror/view'; import { ViewPlugin } from '@codemirror/view'; -import { indentWithTab } from '@codemirror/commands'; +import { indentWithTab, selectAll } from '@codemirror/commands'; import { foldGutter, foldKeymap } from '@codemirror/language'; import { history, historyKeymap } from '@codemirror/commands'; +import { searchKeymap } from '@codemirror/search'; import { html } from '@codemirror/lang-html'; import { javascript } from '@codemirror/lang-javascript'; import { json } from '@codemirror/lang-json'; @@ -154,7 +155,16 @@ export function createCodeMirror(element, options = {}) { createResizePlugin(), ...(Array.isArray(languageSupport) ? languageSupport : [languageSupport]), ...themeSupport, - keymap.of([indentWithTab, ...foldKeymap, ...historyKeymap]), + keymap.of([ + indentWithTab, + ...foldKeymap, + ...historyKeymap, + ...searchKeymap, + { + key: 'Mod-a', + run: selectAll, + }, + ]), EditorView.theme({ '&': { fontSize: '14px', diff --git a/resources/views/components/layouts/auth/card.blade.php b/resources/views/components/layouts/auth/card.blade.php index 1a316ef..b5a62c6 100644 --- a/resources/views/components/layouts/auth/card.blade.php +++ b/resources/views/components/layouts/auth/card.blade.php @@ -15,7 +15,7 @@
-
+
{{ $slot }}
diff --git a/resources/views/default-screens/error.blade.php b/resources/views/default-screens/error.blade.php new file mode 100644 index 0000000..be8063a --- /dev/null +++ b/resources/views/default-screens/error.blade.php @@ -0,0 +1,23 @@ +@props([ + 'noBleed' => false, + 'darkMode' => false, + 'deviceVariant' => 'og', + 'deviceOrientation' => null, + 'colorDepth' => '1bit', + 'scaleLevel' => null, + 'pluginName' => 'Recipe', +]) + + + + + + Error on {{ $pluginName }} + Unable to render content. Please check server logs. + + + + + diff --git a/resources/views/livewire/catalog/index.blade.php b/resources/views/livewire/catalog/index.blade.php index 5bdae10..fdf7f34 100644 --- a/resources/views/livewire/catalog/index.blade.php +++ b/resources/views/livewire/catalog/index.blade.php @@ -1,29 +1,52 @@ loadCatalogPlugins(); } + public function placeholder() + { + return <<<'HTML' +
+
+
+ + Loading recipes... +
+
+
+ HTML; + } + private function loadCatalogPlugins(): void { $catalogUrl = config('app.catalog_url'); $this->catalogPlugins = Cache::remember('catalog_plugins', 43200, function () use ($catalogUrl) { try { - $response = Http::get($catalogUrl); + $response = Http::timeout(10)->get($catalogUrl); $catalogContent = $response->body(); $catalog = Yaml::parse($catalogContent); @@ -32,7 +55,7 @@ new class extends Component { return collect($catalog) ->filter(function ($plugin) use ($currentVersion) { // Check if Laravel compatibility is true - if (!Arr::get($plugin, 'byos.byos_laravel.compatibility', false)) { + if (! Arr::get($plugin, 'byos.byos_laravel.compatibility', false)) { return false; } @@ -62,8 +85,9 @@ new class extends Component { }) ->sortBy('name') ->toArray(); - } catch (\Exception $e) { - Log::error('Failed to load catalog from URL: ' . $e->getMessage()); + } catch (Exception $e) { + Log::error('Failed to load catalog from URL: '.$e->getMessage()); + return []; } }); @@ -75,31 +99,59 @@ new class extends Component { $plugin = collect($this->catalogPlugins)->firstWhere('id', $pluginId); - if (!$plugin || !$plugin['zip_url']) { + if (! $plugin || ! $plugin['zip_url']) { $this->addError('installation', 'Plugin not found or no download URL available.'); + return; } $this->installingPlugin = $pluginId; try { - $importedPlugin = $pluginImportService->importFromUrl($plugin['zip_url'], auth()->user(), $plugin['zip_entry_path'] ?? null); + $importedPlugin = $pluginImportService->importFromUrl( + $plugin['zip_url'], + auth()->user(), + $plugin['zip_entry_path'] ?? null, + null, + $plugin['logo_url'] ?? null, + allowDuplicate: true + ); $this->dispatch('plugin-installed'); Flux::modal('import-from-catalog')->close(); - } catch (\Exception $e) { - $this->addError('installation', 'Error installing plugin: ' . $e->getMessage()); + } catch (Exception $e) { + $this->addError('installation', 'Error installing plugin: '.$e->getMessage()); } finally { $this->installingPlugin = ''; } } + + public function previewPlugin(string $pluginId): void + { + $plugin = collect($this->catalogPlugins)->firstWhere('id', $pluginId); + + if (! $plugin) { + $this->addError('preview', 'Plugin not found.'); + + return; + } + + $this->previewingPlugin = $pluginId; + $this->previewData = $plugin; + } + + public function closePreview(): void + { + $this->previewingPlugin = ''; + $this->previewData = []; + } }; ?>
@if(empty($catalogPlugins))
- + No plugins available Catalog is empty
@@ -110,30 +162,30 @@ new class extends Component { @enderror @foreach($catalogPlugins as $plugin) -
+
@if($plugin['logo_url']) - {{ $plugin['name'] }} + {{ $plugin['name'] }} @else -
- +
+
@endif
-

{{ $plugin['name'] }}

+ {{ $plugin['name'] }} @if ($plugin['github']) -

by {{ $plugin['github'] }}

+ by {{ $plugin['github'] }} @endif
@if($plugin['license']) - {{ $plugin['license'] }} + {{ $plugin['license'] }} @endif @if($plugin['repo_url']) - + @endif @@ -141,7 +193,7 @@ new class extends Component {
@if($plugin['description']) -

{{ $plugin['description'] }}

+ {{ $plugin['description'] }} @endif
@@ -151,6 +203,19 @@ new class extends Component { Install + @if($plugin['screenshot_url']) + + + Preview + + + @endif + + + @if($plugin['learn_more_url']) @endif + + + + @if($previewingPlugin && !empty($previewData)) +
+ Preview {{ $previewData['name'] ?? 'Plugin' }} +
+ +
+
+ Preview of {{ $previewData['name'] }} +
+ + @if($previewData['description']) +
+ Description + {{ $previewData['description'] }} +
+ @endif + +
+ + + Install Plugin + + +
+
+ @endif +
diff --git a/resources/views/livewire/catalog/trmnl.blade.php b/resources/views/livewire/catalog/trmnl.blade.php new file mode 100644 index 0000000..cc8b070 --- /dev/null +++ b/resources/views/livewire/catalog/trmnl.blade.php @@ -0,0 +1,407 @@ +loadNewest(); + } + + public function placeholder() + { + return <<<'HTML' +
+
+
+ + Loading recipes... +
+
+
+ HTML; + } + + private function loadNewest(): void + { + try { + $cacheKey = 'trmnl_recipes_newest_page_'.$this->page; + $response = Cache::remember($cacheKey, 43200, function () { + $response = Http::timeout(10)->get('https://usetrmnl.com/recipes.json', [ + 'sort-by' => 'newest', + 'page' => $this->page, + ]); + + if (! $response->successful()) { + throw new RuntimeException('Failed to fetch TRMNL recipes'); + } + + return $response->json(); + }); + + $data = $response['data'] ?? []; + $mapped = $this->mapRecipes($data); + + if ($this->page === 1) { + $this->recipes = $mapped; + } else { + $this->recipes = array_merge($this->recipes, $mapped); + } + + $this->hasMore = ! empty($response['next_page_url']); + } catch (Throwable $e) { + Log::error('TRMNL catalog load error: '.$e->getMessage()); + if ($this->page === 1) { + $this->recipes = []; + } + $this->hasMore = false; + } + } + + private function searchRecipes(string $term): void + { + $this->isSearching = true; + try { + $cacheKey = 'trmnl_recipes_search_'.md5($term).'_page_'.$this->page; + $response = Cache::remember($cacheKey, 300, function () use ($term) { + $response = Http::get('https://usetrmnl.com/recipes.json', [ + 'search' => $term, + 'sort-by' => 'newest', + 'page' => $this->page, + ]); + + if (! $response->successful()) { + throw new RuntimeException('Failed to search TRMNL recipes'); + } + + return $response->json(); + }); + + $data = $response['data'] ?? []; + $mapped = $this->mapRecipes($data); + + if ($this->page === 1) { + $this->recipes = $mapped; + } else { + $this->recipes = array_merge($this->recipes, $mapped); + } + + $this->hasMore = ! empty($response['next_page_url']); + } catch (Throwable $e) { + Log::error('TRMNL catalog search error: '.$e->getMessage()); + if ($this->page === 1) { + $this->recipes = []; + } + $this->hasMore = false; + } finally { + $this->isSearching = false; + } + } + + public function loadMore(): void + { + $this->page++; + + $term = mb_trim($this->search); + if ($term === '' || mb_strlen($term) < 2) { + $this->loadNewest(); + } else { + $this->searchRecipes($term); + } + } + + public function updatedSearch(): void + { + $this->page = 1; + $term = mb_trim($this->search); + if ($term === '') { + $this->loadNewest(); + + return; + } + + if (mb_strlen($term) < 2) { + // Require at least 2 chars to avoid noisy calls + return; + } + + $this->searchRecipes($term); + } + + public function installPlugin(string $recipeId, PluginImportService $pluginImportService): void + { + abort_unless(auth()->user() !== null, 403); + + try { + $zipUrl = "https://usetrmnl.com/api/plugin_settings/{$recipeId}/archive"; + + $recipe = collect($this->recipes)->firstWhere('id', $recipeId); + + $plugin = $pluginImportService->importFromUrl( + $zipUrl, + auth()->user(), + null, + config('services.trmnl.liquid_enabled') ? 'trmnl-liquid' : null, + $recipe['icon_url'] ?? null, + allowDuplicate: true + ); + + $this->dispatch('plugin-installed'); + Flux::modal('import-from-trmnl-catalog')->close(); + + } catch (Exception $e) { + Log::error('Plugin installation failed: '.$e->getMessage()); + $this->addError('installation', 'Error installing plugin: '.$e->getMessage()); + } + } + + public function previewRecipe(string $recipeId): void + { + $this->previewingRecipe = $recipeId; + $this->previewData = []; + + try { + $response = Http::timeout(10)->get("https://usetrmnl.com/recipes/{$recipeId}.json"); + + if ($response->successful()) { + $item = $response->json()['data'] ?? []; + $this->previewData = $this->mapRecipe($item); + } else { + // Fallback to searching for the specific recipe if single endpoint doesn't exist + $response = Http::timeout(10)->get('https://usetrmnl.com/recipes.json', [ + 'search' => $recipeId, + ]); + + if ($response->successful()) { + $data = $response->json()['data'] ?? []; + $item = collect($data)->firstWhere('id', $recipeId); + if ($item) { + $this->previewData = $this->mapRecipe($item); + } + } + } + } catch (Throwable $e) { + Log::error('TRMNL catalog preview fetch error: '.$e->getMessage()); + } + + if (empty($this->previewData)) { + $this->previewData = collect($this->recipes)->firstWhere('id', $recipeId) ?? []; + } + } + + /** + * @param array> $items + * @return array> + */ + private function mapRecipes(array $items): array + { + return collect($items) + ->map(fn (array $item) => $this->mapRecipe($item)) + ->toArray(); + } + + /** + * @param array $item + * @return array + */ + private function mapRecipe(array $item): array + { + return [ + 'id' => $item['id'] ?? null, + 'name' => $item['name'] ?? 'Untitled', + 'icon_url' => $item['icon_url'] ?? null, + 'screenshot_url' => $item['screenshot_url'] ?? null, + 'author_bio' => is_array($item['author_bio'] ?? null) + ? strip_tags($item['author_bio']['description'] ?? null) + : null, + 'stats' => [ + 'installs' => data_get($item, 'stats.installs'), + 'forks' => data_get($item, 'stats.forks'), + ], + 'detail_url' => isset($item['id']) ? ('https://usetrmnl.com/recipes/'.$item['id']) : null, + ]; + } +}; ?> + +
+
+
+ +
+ Newest +
+ + @error('installation') + + @enderror + + @if(empty($recipes)) +
+ + No recipes found + Try a different search term +
+ @else +
+ @foreach($recipes as $recipe) +
+
+
+ @php($thumb = $recipe['icon_url'] ?? $recipe['screenshot_url']) + @if($thumb) + {{ $recipe['name'] }} + @else +
+ +
+ @endif + +
+
+
+ {{ $recipe['name'] }} + @if(data_get($recipe, 'stats.installs')) + Installs: {{ data_get($recipe, 'stats.installs') }} ยท Forks: {{ data_get($recipe, 'stats.forks') }} + @endif +
+
+ @if($recipe['detail_url']) + + + + @endif +
+
+ + @if($recipe['author_bio']) + {{ $recipe['author_bio'] }} + @endif + +
+ @if($recipe['id']) + + Install + + @endif + + @if($recipe['id'] && ($recipe['screenshot_url'] ?? null)) + + + Preview + + + @endif +
+
+
+
+
+ @endforeach +
+ + @if($hasMore) +
+ + Load next page + Loading... + +
+ @endif + @endif + + + +
+
+ + Fetching recipe details... +
+
+ +
+ @if($previewingRecipe && !empty($previewData)) +
+ Preview {{ $previewData['name'] ?? 'Recipe' }} +
+ +
+
+ Preview of {{ $previewData['name'] }} +
+ + @if($previewData['author_bio']) +
+
+ Description + {{ $previewData['author_bio'] }} +
+
+ @endif + + @if(data_get($previewData, 'stats.installs')) +
+
+ Statistics + + Installs: {{ data_get($previewData, 'stats.installs') }} ยท + Forks: {{ data_get($previewData, 'stats.forks') }} + +
+
+ @endif + +
+ @if($previewData['detail_url']) + + View on TRMNL + + @endif + + + Install Recipe + + +
+
+ @endif +
+
+
diff --git a/resources/views/livewire/device-dashboard.blade.php b/resources/views/livewire/device-dashboard.blade.php index 5db65d1..7fd48a8 100644 --- a/resources/views/livewire/device-dashboard.blade.php +++ b/resources/views/livewire/device-dashboard.blade.php @@ -16,7 +16,7 @@ new class extends Component { @if($devices->isEmpty())
+ class="styled-container">

Add your first device

+ class="styled-container">
@php $current_image_uuid =$device->current_screen_image; diff --git a/resources/views/livewire/device-models/index.blade.php b/resources/views/livewire/device-models/index.blade.php index a78f2a2..a57085b 100644 --- a/resources/views/livewire/device-models/index.blade.php +++ b/resources/views/livewire/device-models/index.blade.php @@ -1,26 +1,43 @@ 'required|string|max:255|unique:device_models,name', 'label' => 'required|string|max:255', @@ -40,62 +57,58 @@ new class extends Component { public function mount() { $this->deviceModels = DeviceModel::all(); + $this->devicePalettes = DevicePalette::all(); + return view('livewire.device-models.index'); } - public function createDeviceModel(): void - { - $this->validate(); - - DeviceModel::create([ - 'name' => $this->name, - 'label' => $this->label, - 'description' => $this->description, - 'width' => $this->width, - 'height' => $this->height, - 'colors' => $this->colors, - 'bit_depth' => $this->bit_depth, - 'scale_factor' => $this->scale_factor, - 'rotation' => $this->rotation, - 'mime_type' => $this->mime_type, - 'offset_x' => $this->offset_x, - 'offset_y' => $this->offset_y, - 'published_at' => $this->published_at, - ]); - - $this->reset(['name', 'label', 'description', 'width', 'height', 'colors', 'bit_depth', 'scale_factor', 'rotation', 'mime_type', 'offset_x', 'offset_y', 'published_at']); - \Flux::modal('create-device-model')->close(); - - $this->deviceModels = DeviceModel::all(); - session()->flash('message', 'Device model created successfully.'); - } - public $editingDeviceModelId; - public function editDeviceModel(DeviceModel $deviceModel): void + public $viewingDeviceModelId; + + public function openDeviceModelModal(?string $deviceModelId = null, bool $viewOnly = false): void { - $this->editingDeviceModelId = $deviceModel->id; - $this->name = $deviceModel->name; - $this->label = $deviceModel->label; - $this->description = $deviceModel->description; - $this->width = $deviceModel->width; - $this->height = $deviceModel->height; - $this->colors = $deviceModel->colors; - $this->bit_depth = $deviceModel->bit_depth; - $this->scale_factor = $deviceModel->scale_factor; - $this->rotation = $deviceModel->rotation; - $this->mime_type = $deviceModel->mime_type; - $this->offset_x = $deviceModel->offset_x; - $this->offset_y = $deviceModel->offset_y; - $this->published_at = $deviceModel->published_at?->format('Y-m-d\TH:i'); + if ($deviceModelId) { + $deviceModel = DeviceModel::findOrFail($deviceModelId); + + if ($viewOnly) { + $this->viewingDeviceModelId = $deviceModel->id; + $this->editingDeviceModelId = null; + } else { + $this->editingDeviceModelId = $deviceModel->id; + $this->viewingDeviceModelId = null; + } + + $this->name = $deviceModel->name; + $this->label = $deviceModel->label; + $this->description = $deviceModel->description; + $this->width = $deviceModel->width; + $this->height = $deviceModel->height; + $this->colors = $deviceModel->colors; + $this->bit_depth = $deviceModel->bit_depth; + $this->scale_factor = $deviceModel->scale_factor; + $this->rotation = $deviceModel->rotation; + $this->mime_type = $deviceModel->mime_type; + $this->offset_x = $deviceModel->offset_x; + $this->offset_y = $deviceModel->offset_y; + $this->published_at = $deviceModel->published_at?->format('Y-m-d\TH:i'); + $this->palette_id = $deviceModel->palette_id; + } else { + $this->editingDeviceModelId = null; + $this->viewingDeviceModelId = null; + $this->reset(['name', 'label', 'description', 'width', 'height', 'colors', 'bit_depth', 'scale_factor', 'rotation', 'mime_type', 'offset_x', 'offset_y', 'published_at', 'palette_id']); + $this->mime_type = 'image/png'; + $this->scale_factor = 1.0; + $this->rotation = 0; + $this->offset_x = 0; + $this->offset_y = 0; + } } - public function updateDeviceModel(): void + public function saveDeviceModel(): void { - $deviceModel = DeviceModel::findOrFail($this->editingDeviceModelId); - - $this->validate([ - 'name' => 'required|string|max:255|unique:device_models,name,' . $deviceModel->id, + $rules = [ + 'name' => 'required|string|max:255', 'label' => 'required|string|max:255', 'description' => 'required|string', 'width' => 'required|integer|min:1', @@ -108,38 +121,96 @@ new class extends Component { 'offset_x' => 'required|integer', 'offset_y' => 'required|integer', 'published_at' => 'nullable|date', - ]); + 'palette_id' => 'nullable|exists:device_palettes,id', + ]; - $deviceModel->update([ - 'name' => $this->name, - 'label' => $this->label, - 'description' => $this->description, - 'width' => $this->width, - 'height' => $this->height, - 'colors' => $this->colors, - 'bit_depth' => $this->bit_depth, - 'scale_factor' => $this->scale_factor, - 'rotation' => $this->rotation, - 'mime_type' => $this->mime_type, - 'offset_x' => $this->offset_x, - 'offset_y' => $this->offset_y, - 'published_at' => $this->published_at, - ]); + if ($this->editingDeviceModelId) { + $rules['name'] = 'required|string|max:255|unique:device_models,name,'.$this->editingDeviceModelId; + } else { + $rules['name'] = 'required|string|max:255|unique:device_models,name'; + } - $this->reset(['name', 'label', 'description', 'width', 'height', 'colors', 'bit_depth', 'scale_factor', 'rotation', 'mime_type', 'offset_x', 'offset_y', 'published_at', 'editingDeviceModelId']); - \Flux::modal('edit-device-model-' . $deviceModel->id)->close(); + $this->validate($rules); + + if ($this->editingDeviceModelId) { + $deviceModel = DeviceModel::findOrFail($this->editingDeviceModelId); + $deviceModel->update([ + 'name' => $this->name, + 'label' => $this->label, + 'description' => $this->description, + 'width' => $this->width, + 'height' => $this->height, + 'colors' => $this->colors, + 'bit_depth' => $this->bit_depth, + 'scale_factor' => $this->scale_factor, + 'rotation' => $this->rotation, + 'mime_type' => $this->mime_type, + 'offset_x' => $this->offset_x, + 'offset_y' => $this->offset_y, + 'published_at' => $this->published_at, + 'palette_id' => $this->palette_id ?: null, + ]); + $message = 'Device model updated successfully.'; + } else { + DeviceModel::create([ + 'name' => $this->name, + 'label' => $this->label, + 'description' => $this->description, + 'width' => $this->width, + 'height' => $this->height, + 'colors' => $this->colors, + 'bit_depth' => $this->bit_depth, + 'scale_factor' => $this->scale_factor, + 'rotation' => $this->rotation, + 'mime_type' => $this->mime_type, + 'offset_x' => $this->offset_x, + 'offset_y' => $this->offset_y, + 'published_at' => $this->published_at, + 'palette_id' => $this->palette_id ?: null, + 'source' => 'manual', + ]); + $message = 'Device model created successfully.'; + } + + $this->reset(['name', 'label', 'description', 'width', 'height', 'colors', 'bit_depth', 'scale_factor', 'rotation', 'mime_type', 'offset_x', 'offset_y', 'published_at', 'palette_id', 'editingDeviceModelId', 'viewingDeviceModelId']); + Flux::modal('device-model-modal')->close(); $this->deviceModels = DeviceModel::all(); - session()->flash('message', 'Device model updated successfully.'); + session()->flash('message', $message); } - public function deleteDeviceModel(DeviceModel $deviceModel): void + public function deleteDeviceModel(string $deviceModelId): void { + $deviceModel = DeviceModel::findOrFail($deviceModelId); $deviceModel->delete(); $this->deviceModels = DeviceModel::all(); session()->flash('message', 'Device model deleted successfully.'); } + + public function duplicateDeviceModel(string $deviceModelId): void + { + $deviceModel = DeviceModel::findOrFail($deviceModelId); + + $this->editingDeviceModelId = null; + $this->viewingDeviceModelId = null; + $this->name = $deviceModel->name.' (Copy)'; + $this->label = $deviceModel->label; + $this->description = $deviceModel->description; + $this->width = $deviceModel->width; + $this->height = $deviceModel->height; + $this->colors = $deviceModel->colors; + $this->bit_depth = $deviceModel->bit_depth; + $this->scale_factor = $deviceModel->scale_factor; + $this->rotation = $deviceModel->rotation; + $this->mime_type = $deviceModel->mime_type; + $this->offset_x = $deviceModel->offset_x; + $this->offset_y = $deviceModel->offset_y; + $this->published_at = $deviceModel->published_at?->format('Y-m-d\TH:i'); + $this->palette_id = $deviceModel->palette_id; + + $this->js('Flux.modal("device-model-modal").show()'); + } } ?> @@ -148,10 +219,19 @@ new class extends Component {
-

Device Models

- {{-- --}} - {{-- Add Device Model--}} - {{-- --}} +
+

Device Models

+ + + + Devices + Device Palettes + + +
+ + Add Device Model +
@if (session()->has('message'))
@@ -164,157 +244,104 @@ new class extends Component {
@endif - +
- Add Device Model + + @if ($viewingDeviceModelId) + View Device Model + @elseif ($editingDeviceModelId) + Edit Device Model + @else + Add Device Model + @endif +
-
+
+ name="name" autofocus :disabled="(bool) $viewingDeviceModelId"/>
+ name="label" :disabled="(bool) $viewingDeviceModelId"/>
+ class="block mt-1 w-full" name="description" :disabled="(bool) $viewingDeviceModelId"/>
+ name="width" :disabled="(bool) $viewingDeviceModelId"/> + name="height" :disabled="(bool) $viewingDeviceModelId"/>
+ name="colors" :disabled="(bool) $viewingDeviceModelId"/> + name="bit_depth" :disabled="(bool) $viewingDeviceModelId"/>
+ name="scale_factor" step="0.1" :disabled="(bool) $viewingDeviceModelId"/> + name="rotation" :disabled="(bool) $viewingDeviceModelId"/>
- + + image/png + image/bmp +
+ name="offset_x" :disabled="(bool) $viewingDeviceModelId"/> + name="offset_y" :disabled="(bool) $viewingDeviceModelId"/>
-
- - Create Device Model +
+ + None + @foreach ($devicePalettes as $palette) + {{ $palette->description ?? $palette->name }} ({{ $palette->name }}) + @endforeach +
+ + @if (!$viewingDeviceModelId) +
+ + {{ $editingDeviceModelId ? 'Update' : 'Create' }} Device Model +
+ @else +
+ + Duplicate +
+ @endif
- @foreach ($deviceModels as $deviceModel) - -
-
- Edit Device Model -
- -
-
- -
- -
- -
- -
- -
- -
- - -
- -
- - -
- -
- - -
- -
- - image/png - image/bmp - - -
- -
- - -
- -
- - Update Device Model -
-
-
-
- @endforeach - @@ -369,14 +396,25 @@ new class extends Component { >
- - source === 'api') + + + + + - - - + @else + + + + + + + @endif
diff --git a/resources/views/livewire/device-palettes/index.blade.php b/resources/views/livewire/device-palettes/index.blade.php new file mode 100644 index 0000000..28f99c9 --- /dev/null +++ b/resources/views/livewire/device-palettes/index.blade.php @@ -0,0 +1,384 @@ + 'required|string|max:255|unique:device_palettes,name', + 'description' => 'nullable|string|max:255', + 'grays' => 'required|integer|min:1|max:256', + 'colors' => 'nullable|array', + 'colors.*' => 'string|regex:/^#[0-9A-Fa-f]{6}$/', + 'framework_class' => 'nullable|string|max:255', + ]; + + public function mount() + { + $this->devicePalettes = DevicePalette::all(); + + return view('livewire.device-palettes.index'); + } + + public function addColor(): void + { + $this->validate(['colorInput' => 'required|string|regex:/^#[0-9A-Fa-f]{6}$/'], [ + 'colorInput.regex' => 'Color must be a valid hex color (e.g., #FF0000)', + ]); + + if (! in_array($this->colorInput, $this->colors)) { + $this->colors[] = $this->colorInput; + } + + $this->colorInput = ''; + } + + public function removeColor(int $index): void + { + unset($this->colors[$index]); + $this->colors = array_values($this->colors); + } + + public $editingDevicePaletteId; + + public $viewingDevicePaletteId; + + public function openDevicePaletteModal(?string $devicePaletteId = null, bool $viewOnly = false): void + { + if ($devicePaletteId) { + $devicePalette = DevicePalette::findOrFail($devicePaletteId); + + if ($viewOnly) { + $this->viewingDevicePaletteId = $devicePalette->id; + $this->editingDevicePaletteId = null; + } else { + $this->editingDevicePaletteId = $devicePalette->id; + $this->viewingDevicePaletteId = null; + } + + $this->name = $devicePalette->name; + $this->description = $devicePalette->description; + $this->grays = $devicePalette->grays; + + // Ensure colors is always an array and properly decoded + // The model cast should handle JSON decoding, but we'll be explicit + $colors = $devicePalette->getAttribute('colors'); + + if ($colors === null) { + $this->colors = []; + } elseif (is_string($colors)) { + $decoded = json_decode($colors, true); + $this->colors = is_array($decoded) ? array_values($decoded) : []; + } elseif (is_array($colors)) { + $this->colors = array_values($colors); // Re-index array + } else { + $this->colors = []; + } + + $this->framework_class = $devicePalette->framework_class; + } else { + $this->editingDevicePaletteId = null; + $this->viewingDevicePaletteId = null; + $this->reset(['name', 'description', 'grays', 'colors', 'framework_class']); + } + + $this->colorInput = ''; + } + + public function saveDevicePalette(): void + { + $rules = [ + 'name' => 'required|string|max:255', + 'description' => 'nullable|string|max:255', + 'grays' => 'required|integer|min:1|max:256', + 'colors' => 'nullable|array', + 'colors.*' => 'string|regex:/^#[0-9A-Fa-f]{6}$/', + 'framework_class' => 'nullable|string|max:255', + ]; + + if ($this->editingDevicePaletteId) { + $rules['name'] = 'required|string|max:255|unique:device_palettes,name,'.$this->editingDevicePaletteId; + } else { + $rules['name'] = 'required|string|max:255|unique:device_palettes,name'; + } + + $this->validate($rules); + + if ($this->editingDevicePaletteId) { + $devicePalette = DevicePalette::findOrFail($this->editingDevicePaletteId); + $devicePalette->update([ + 'name' => $this->name, + 'description' => $this->description, + 'grays' => $this->grays, + 'colors' => ! empty($this->colors) ? $this->colors : null, + 'framework_class' => $this->framework_class, + ]); + $message = 'Device palette updated successfully.'; + } else { + DevicePalette::create([ + 'name' => $this->name, + 'description' => $this->description, + 'grays' => $this->grays, + 'colors' => ! empty($this->colors) ? $this->colors : null, + 'framework_class' => $this->framework_class, + 'source' => 'manual', + ]); + $message = 'Device palette created successfully.'; + } + + $this->reset(['name', 'description', 'grays', 'colors', 'framework_class', 'colorInput', 'editingDevicePaletteId', 'viewingDevicePaletteId']); + Flux::modal('device-palette-modal')->close(); + + $this->devicePalettes = DevicePalette::all(); + session()->flash('message', $message); + } + + public function deleteDevicePalette(string $devicePaletteId): void + { + $devicePalette = DevicePalette::findOrFail($devicePaletteId); + $devicePalette->delete(); + + $this->devicePalettes = DevicePalette::all(); + session()->flash('message', 'Device palette deleted successfully.'); + } + + public function duplicateDevicePalette(string $devicePaletteId): void + { + $devicePalette = DevicePalette::findOrFail($devicePaletteId); + + $this->editingDevicePaletteId = null; + $this->viewingDevicePaletteId = null; + $this->name = $devicePalette->name.' (Copy)'; + $this->description = $devicePalette->description; + $this->grays = $devicePalette->grays; + + $colors = $devicePalette->getAttribute('colors'); + if ($colors === null) { + $this->colors = []; + } elseif (is_string($colors)) { + $decoded = json_decode($colors, true); + $this->colors = is_array($decoded) ? array_values($decoded) : []; + } elseif (is_array($colors)) { + $this->colors = array_values($colors); + } else { + $this->colors = []; + } + + $this->framework_class = $devicePalette->framework_class; + $this->colorInput = ''; + + $this->js('Flux.modal("device-palette-modal").show()'); + } +} + +?> + +
+
+
+
+
+

Device Palettes

+ + + + Devices + Device Models + + +
+ + Add Device Palette + +
+ @if (session()->has('message')) +
+ + + + + +
+ @endif + + +
+
+ + @if ($viewingDevicePaletteId) + View Device Palette + @elseif ($editingDevicePaletteId) + Edit Device Palette + @else + Add Device Palette + @endif + +
+ +
+
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ Colors + @if (!$viewingDevicePaletteId) +
+ + Add +
+ @endif +
+ @if (!empty($colors) && is_array($colors) && count($colors) > 0) + @foreach ($colors as $index => $color) + @if (!empty($color)) +
+
+ {{ $color }} + @if (!$viewingDevicePaletteId) + + @endif +
+ @endif + @endforeach + @endif +
+ @if (!$viewingDevicePaletteId) +

Leave empty for grayscale-only palette

+ @endif +
+ + @if (!$viewingDevicePaletteId) +
+ + {{ $editingDevicePaletteId ? 'Update' : 'Create' }} Device Palette +
+ @else +
+ + Duplicate +
+ @endif + +
+
+ +
+ + + + + + + + + + + @foreach ($devicePalettes as $devicePalette) + + + + + + + @endforeach + +
+
Description
+
+
Grays
+
+
Colors
+
+
Actions
+
+
+
{{ $devicePalette->description ?? $devicePalette->name }}
+
{{ $devicePalette->name }}
+
+
+ {{ $devicePalette->grays }} + + @if ($devicePalette->colors) +
+ @foreach ($devicePalette->colors as $color) +
+ @endforeach + ({{ count($devicePalette->colors) }}) +
+ @else + Grayscale only + @endif +
+
+ + @if ($devicePalette->source === 'api') + + + + + + + @else + + + + + + + @endif + +
+
+
+
+
+ diff --git a/resources/views/livewire/devices/configure.blade.php b/resources/views/livewire/devices/configure.blade.php index 30b4481..f9d49ca 100644 --- a/resources/views/livewire/devices/configure.blade.php +++ b/resources/views/livewire/devices/configure.blade.php @@ -309,7 +309,7 @@ new class extends Component {
+ class="styled-container">
@php $current_image_uuid =$device->current_screen_image; @@ -368,6 +368,10 @@ new class extends Component { Update Firmware Show Logs + + Mirror URL + + Delete Device @@ -498,6 +502,26 @@ new class extends Component { + + @php + $mirrorUrl = url('/mirror/index.html') . '?mac_address=' . urlencode($device->mac_address) . '&api_key=' . urlencode($device->api_key); + @endphp + +
+
+ Mirror WebUI + Mirror this device onto older devices with a web browser โ€” Safari is supported back to iOS 9. +
+ + +
+
+ @if(!$device->mirror_device_id) @if($current_image_path) diff --git a/resources/views/livewire/devices/manage.blade.php b/resources/views/livewire/devices/manage.blade.php index d87bd1c..646adc0 100644 --- a/resources/views/livewire/devices/manage.blade.php +++ b/resources/views/livewire/devices/manage.blade.php @@ -121,7 +121,16 @@ new class extends Component { {{--@dump($devices)--}}
-

Devices

+
+

Devices

+ + + + Device Models + Device Palettes + + +
Add Device diff --git a/resources/views/livewire/playlists/index.blade.php b/resources/views/livewire/playlists/index.blade.php index 3e786b4..6c979e6 100644 --- a/resources/views/livewire/playlists/index.blade.php +++ b/resources/views/livewire/playlists/index.blade.php @@ -332,7 +332,7 @@ new class extends Component { @endforeach @if($devices->isEmpty() || $devices->every(fn($device) => $device->playlists->isEmpty())) -
+

No playlists found

Add playlists to your devices to see them here.

diff --git a/resources/views/livewire/plugins/config-modal.blade.php b/resources/views/livewire/plugins/config-modal.blade.php new file mode 100644 index 0000000..7aaacbb --- /dev/null +++ b/resources/views/livewire/plugins/config-modal.blade.php @@ -0,0 +1,516 @@ + loadData(); + } + + public function loadData(): void + { + $this->resetErrorBag(); + // Reload data + $this->plugin = $this->plugin->fresh(); + + $this->configuration_template = $this->plugin->configuration_template ?? []; + $this->configuration = is_array($this->plugin->configuration) ? $this->plugin->configuration : []; + + // Initialize multiValues by exploding the CSV strings from the DB + foreach ($this->configuration_template['custom_fields'] ?? [] as $field) { + if (($field['field_type'] ?? null) === 'multi_string') { + $fieldKey = $field['keyname']; + $rawValue = $this->configuration[$fieldKey] ?? ($field['default'] ?? ''); + + $currentValue = is_array($rawValue) ? '' : (string)$rawValue; + + $this->multiValues[$fieldKey] = $currentValue !== '' + ? array_values(array_filter(explode(',', $currentValue))) + : ['']; + } + } + } + + /** + * Triggered by @close on the modal to discard any typed but unsaved changes + */ + public int $resetIndex = 0; // Add this property + public function resetForm(): void + { + $this->loadData(); + $this->resetIndex++; // Increment to force DOM refresh + } + + public function saveConfiguration() + { + abort_unless(auth()->user()->plugins->contains($this->plugin), 403); + + // final validation layer + $this->validate([ + 'multiValues.*.*' => ['nullable', 'string', 'regex:/^[^,]*$/'], + ], [ + 'multiValues.*.*.regex' => 'Items cannot contain commas.', + ]); + + // Prepare config copy to send to db + $finalValues = $this->configuration; + foreach ($this->configuration_template['custom_fields'] ?? [] as $field) { + $fieldKey = $field['keyname']; + + // Handle multi_string: Join array back to CSV string + if ($field['field_type'] === 'multi_string' && isset($this->multiValues[$fieldKey])) { + $finalValues[$fieldKey] = implode(',', array_filter(array_map('trim', $this->multiValues[$fieldKey]))); + } + + // Handle code fields: Try to JSON decode if necessary (standard TRMNL behavior) + if ($field['field_type'] === 'code' && isset($finalValues[$fieldKey]) && is_string($finalValues[$fieldKey])) { + $decoded = json_decode($finalValues[$fieldKey], true); + if (json_last_error() === JSON_ERROR_NONE && (is_array($decoded) || is_object($decoded))) { + $finalValues[$fieldKey] = $decoded; + } + } + } + + // send to db + $this->plugin->update(['configuration' => $finalValues]); + $this->configuration = $finalValues; // update local state + $this->dispatch('config-updated'); // notifies listeners + Flux::modal('configuration-modal')->close(); + } + + // ------------------------------------This section contains helper functions for interacting with the form------------------------------------------------ + public function addMultiItem(string $fieldKey): void + { + $this->multiValues[$fieldKey][] = ''; + } + + public function removeMultiItem(string $fieldKey, int $index): void + { + unset($this->multiValues[$fieldKey][$index]); + + $this->multiValues[$fieldKey] = array_values($this->multiValues[$fieldKey]); + + if (empty($this->multiValues[$fieldKey])) { + $this->multiValues[$fieldKey][] = ''; + } + } + + // Livewire magic method to validate MultiValue input boxes + // Runs on every debounce + public function updatedMultiValues($value, $key) + { + $this->validate([ + 'multiValues.*.*' => ['nullable', 'string', 'regex:/^[^,]*$/'], + ], [ + 'multiValues.*.*.regex' => 'Items cannot contain commas.', + ]); + } + + public function loadXhrSelectOptions(string $fieldKey, string $endpoint, ?string $query = null): void + { + abort_unless(auth()->user()->plugins->contains($this->plugin), 403); + + try { + $requestData = []; + if ($query !== null) { + $requestData = [ + 'function' => $fieldKey, + 'query' => $query + ]; + } + + $response = $query !== null + ? Http::post($endpoint, $requestData) + : Http::post($endpoint); + + if ($response->successful()) { + $this->xhrSelectOptions[$fieldKey] = $response->json(); + } else { + $this->xhrSelectOptions[$fieldKey] = []; + } + } catch (\Exception $e) { + $this->xhrSelectOptions[$fieldKey] = []; + } + } + + public function searchXhrSelect(string $fieldKey, string $endpoint): void + { + $query = $this->searchQueries[$fieldKey] ?? ''; + if (!empty($query)) { + $this->loadXhrSelectOptions($fieldKey, $endpoint, $query); + } + } +};?> + + +
+
+
+ Configuration + Configure your plugin settings +
+ +
+ @if(isset($configuration_template['custom_fields']) && is_array($configuration_template['custom_fields'])) + @foreach($configuration_template['custom_fields'] as $field) + @php + $fieldKey = $field['keyname'] ?? $field['key'] ?? $field['name']; + $rawValue = $configuration[$fieldKey] ?? ($field['default'] ?? ''); + + # These are sanitized at Model/Plugin level, safe to render HTML + $safeDescription = $field['description'] ?? ''; + $safeHelp = $field['help_text'] ?? ''; + + // For code fields, if the value is an array, JSON encode it + if ($field['field_type'] === 'code' && is_array($rawValue)) { + $currentValue = json_encode($rawValue, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); + } else { + $currentValue = is_array($rawValue) ? '' : (string) $rawValue; + } + @endphp +
+ @if($field['field_type'] === 'author_bio') + @continue + @endif + + @if($field['field_type'] === 'copyable_webhook_url') + @continue + @endif + + @if($field['field_type'] === 'string' || $field['field_type'] === 'url') + + {{ $field['name'] }} + {!! $safeDescription !!} + + {!! $safeHelp !!} + + + @elseif($field['field_type'] === 'text') + + {{ $field['name'] }} + {!! $safeDescription !!} + + {!! $safeHelp !!} + + + @elseif($field['field_type'] === 'code') + + {{ $field['name'] }} + {!! $safeDescription !!} + + {!! $safeHelp !!} + + + @elseif($field['field_type'] === 'password') + + {{ $field['name'] }} + {!! $safeDescription !!} + + {!! $safeHelp !!} + + + @elseif($field['field_type'] === 'copyable') + + {{ $field['name'] }} + {!! $safeDescription !!} + + {!! $safeHelp !!} + + + @elseif($field['field_type'] === 'time_zone') + + {{ $field['name'] }} + {!! $safeDescription !!} + + + @foreach(timezone_identifiers_list() as $timezone) + + @endforeach + + {!! $safeHelp !!} + + + @elseif($field['field_type'] === 'number') + + {{ $field['name'] }} + {!! $safeDescription !!} + + {!! $safeHelp !!} + + + @elseif($field['field_type'] === 'boolean') + + {{ $field['name'] }} + {!! $safeDescription !!} + + {!! $safeHelp !!} + + + @elseif($field['field_type'] === 'date') + + {{ $field['name'] }} + {!! $safeDescription !!} + + {!! $safeHelp !!} + + + @elseif($field['field_type'] === 'time') + + {{ $field['name'] }} + {!! $safeDescription !!} + + {!! $safeHelp !!} + + + @elseif($field['field_type'] === 'select') + @if(isset($field['multiple']) && $field['multiple'] === true) + + {{ $field['name'] }} + {!! $safeDescription !!} + + @if(isset($field['options']) && is_array($field['options'])) + @foreach($field['options'] as $option) + @if(is_array($option)) + @foreach($option as $label => $value) + + @endforeach + @else + @php + $key = mb_strtolower(str_replace(' ', '_', $option)); + @endphp + + @endif + @endforeach + @endif + + {!! $safeHelp !!} + + @else + + {{ $field['name'] }} + {!! $safeDescription !!} + + + @if(isset($field['options']) && is_array($field['options'])) + @foreach($field['options'] as $option) + @if(is_array($option)) + @foreach($option as $label => $value) + + @endforeach + @else + @php + $key = mb_strtolower(str_replace(' ', '_', $option)); + @endphp + + @endif + @endforeach + @endif + + {!! $safeHelp !!} + + @endif + + @elseif($field['field_type'] === 'xhrSelect') + + {{ $field['name'] }} + {!! $safeDescription !!} + + + @if(isset($xhrSelectOptions[$fieldKey]) && is_array($xhrSelectOptions[$fieldKey])) + @foreach($xhrSelectOptions[$fieldKey] as $option) + @if(is_array($option)) + @if(isset($option['id']) && isset($option['name'])) + {{-- xhrSelectSearch format: { 'id' => 'db-456', 'name' => 'Team Goals' } --}} + + @else + {{-- xhrSelect format: { 'Braves' => 123 } --}} + @foreach($option as $label => $value) + + @endforeach + @endif + @else + + @endif + @endforeach + @endif + + {!! $safeHelp !!} + + + @elseif($field['field_type'] === 'xhrSelectSearch') +
+ + {{ $field['name'] }} + {!! $safeDescription !!} + + + + + {!! $safeHelp !!} + @if((isset($xhrSelectOptions[$fieldKey]) && is_array($xhrSelectOptions[$fieldKey]) && count($xhrSelectOptions[$fieldKey]) > 0) || !empty($currentValue)) + + + @if(isset($xhrSelectOptions[$fieldKey]) && is_array($xhrSelectOptions[$fieldKey])) + @foreach($xhrSelectOptions[$fieldKey] as $option) + @if(is_array($option)) + @if(isset($option['id']) && isset($option['name'])) + {{-- xhrSelectSearch format: { 'id' => 'db-456', 'name' => 'Team Goals' } --}} + + @else + {{-- xhrSelect format: { 'Braves' => 123 } --}} + @foreach($option as $label => $value) + + @endforeach + @endif + @else + + @endif + @endforeach + @endif + @if(!empty($currentValue) && (!isset($xhrSelectOptions[$fieldKey]) || empty($xhrSelectOptions[$fieldKey]))) + {{-- Show current value even if no options are loaded --}} + + @endif + + @endif +
+ @elseif($field['field_type'] === 'multi_string') + + {{ $field['name'] }} + {!! $safeDescription !!} + +
+ @foreach($multiValues[$fieldKey] as $index => $item) +
+ + + + @if(count($multiValues[$fieldKey]) > 1) + + @endif +
+ @error("multiValues.{$fieldKey}.{$index}") +
+ + {{-- $message comes from thrown error --}} + {{ $message }} +
+ @enderror + @endforeach + + + Add Item + +
+ {!! $safeHelp !!} +
+ @else + Field type "{{ $field['field_type'] }}" not yet supported + @endif +
+ @endforeach + @endif + +
+ + + Save Configuration + + @if($errors->any()) +
+ + + Fix errors before saving. + +
+ @endif +
+
+
+
+
diff --git a/resources/views/livewire/plugins/image-webhook-instance.blade.php b/resources/views/livewire/plugins/image-webhook-instance.blade.php new file mode 100644 index 0000000..e4ad9df --- /dev/null +++ b/resources/views/livewire/plugins/image-webhook-instance.blade.php @@ -0,0 +1,298 @@ +user()->plugins->contains($this->plugin), 403); + abort_unless($this->plugin->plugin_type === 'image_webhook', 404); + + $this->name = $this->plugin->name; + } + + protected array $rules = [ + 'name' => 'required|string|max:255', + 'checked_devices' => 'array', + 'device_playlist_names' => 'array', + 'device_playlists' => 'array', + 'device_weekdays' => 'array', + 'device_active_from' => 'array', + 'device_active_until' => 'array', + ]; + + public function updateName(): void + { + abort_unless(auth()->user()->plugins->contains($this->plugin), 403); + $this->validate(['name' => 'required|string|max:255']); + $this->plugin->update(['name' => $this->name]); + } + + + public function addToPlaylist() + { + $this->validate([ + 'checked_devices' => 'required|array|min:1', + ]); + + foreach ($this->checked_devices as $deviceId) { + if (!isset($this->device_playlists[$deviceId]) || empty($this->device_playlists[$deviceId])) { + $this->addError('device_playlists.' . $deviceId, 'Please select a playlist for each device.'); + return; + } + + if ($this->device_playlists[$deviceId] === 'new') { + if (!isset($this->device_playlist_names[$deviceId]) || empty($this->device_playlist_names[$deviceId])) { + $this->addError('device_playlist_names.' . $deviceId, 'Playlist name is required when creating a new playlist.'); + return; + } + } + } + + foreach ($this->checked_devices as $deviceId) { + $playlist = null; + + if ($this->device_playlists[$deviceId] === 'new') { + $playlist = \App\Models\Playlist::create([ + 'device_id' => $deviceId, + 'name' => $this->device_playlist_names[$deviceId], + 'weekdays' => !empty($this->device_weekdays[$deviceId] ?? null) ? $this->device_weekdays[$deviceId] : null, + 'active_from' => $this->device_active_from[$deviceId] ?? null, + 'active_until' => $this->device_active_until[$deviceId] ?? null, + ]); + } else { + $playlist = \App\Models\Playlist::findOrFail($this->device_playlists[$deviceId]); + } + + $maxOrder = $playlist->items()->max('order') ?? 0; + + // Image webhook plugins only support full layout + $playlist->items()->create([ + 'plugin_id' => $this->plugin->id, + 'order' => $maxOrder + 1, + ]); + } + + $this->reset([ + 'checked_devices', + 'device_playlists', + 'device_playlist_names', + 'device_weekdays', + 'device_active_from', + 'device_active_until', + ]); + Flux::modal('add-to-playlist')->close(); + } + + public function getDevicePlaylists($deviceId) + { + return \App\Models\Playlist::where('device_id', $deviceId)->get(); + } + + public function hasAnyPlaylistSelected(): bool + { + foreach ($this->checked_devices as $deviceId) { + if (isset($this->device_playlists[$deviceId]) && !empty($this->device_playlists[$deviceId])) { + return true; + } + } + return false; + } + + public function deletePlugin(): void + { + abort_unless(auth()->user()->plugins->contains($this->plugin), 403); + $this->plugin->delete(); + $this->redirect(route('plugins.image-webhook')); + } + + public function getImagePath(): ?string + { + if (!$this->plugin->current_image) { + return null; + } + + $extensions = ['png', 'bmp']; + foreach ($extensions as $ext) { + $path = 'images/generated/'.$this->plugin->current_image.'.'.$ext; + if (\Illuminate\Support\Facades\Storage::disk('public')->exists($path)) { + return $path; + } + } + + return null; + } +}; +?> + +
+
+
+

Image Webhook โ€“ {{$plugin->name}}

+ + + + Add to Playlist + + + + + + + Delete Instance + + + + +
+ + +
+
+ Add to Playlist +
+ +
+ +
+ + @foreach(auth()->user()->devices as $device) + + @endforeach + +
+ + @if(count($checked_devices) > 0) + +
+ @foreach($checked_devices as $deviceId) + @php + $device = auth()->user()->devices->find($deviceId); + @endphp +
+
+ {{ $device->name }} +
+ +
+ + + @foreach($this->getDevicePlaylists($deviceId) as $playlist) + + @endforeach + + +
+ + @if(isset($device_playlists[$deviceId]) && $device_playlists[$deviceId] === 'new') +
+
+ +
+
+ + + + + + + + + +
+
+
+ +
+
+ +
+
+
+ @endif +
+ @endforeach +
+ @endif + + +
+ + Add to Playlist +
+ +
+
+ + +
+ Delete {{ $plugin->name }}? +

This will also remove this instance from your playlists.

+
+ +
+ + + Cancel + + Delete instance +
+
+ +
+
+
+
+ +
+ +
+ + Save +
+
+ +
+ Webhook URL + + POST an image (PNG or BMP) to this URL to update the displayed image. + + + Images must be posted in a format that can directly be read by the device. You need to take care of image format, dithering, and bit-depth. Check device logs if the image is not shown. + + +
+
+ +
+
+ Current Image + @if($this->getImagePath()) + {{ $plugin->name }} + @else + + No image uploaded yet. POST an image to the webhook URL to get started. + + @endif +
+
+
+
+
+ diff --git a/resources/views/livewire/plugins/image-webhook.blade.php b/resources/views/livewire/plugins/image-webhook.blade.php new file mode 100644 index 0000000..3161443 --- /dev/null +++ b/resources/views/livewire/plugins/image-webhook.blade.php @@ -0,0 +1,163 @@ + 'required|string|max:255', + ]; + + public function mount(): void + { + $this->refreshInstances(); + } + + public function refreshInstances(): void + { + $this->instances = auth()->user() + ->plugins() + ->where('plugin_type', 'image_webhook') + ->orderBy('created_at', 'desc') + ->get() + ->toArray(); + } + + public function createInstance(): void + { + abort_unless(auth()->user() !== null, 403); + $this->validate(); + + Plugin::create([ + 'uuid' => Str::uuid(), + 'user_id' => auth()->id(), + 'name' => $this->name, + 'plugin_type' => 'image_webhook', + 'data_strategy' => 'static', // Not used for image_webhook, but required + 'data_stale_minutes' => 60, // Not used for image_webhook, but required + ]); + + $this->reset(['name']); + $this->refreshInstances(); + + Flux::modal('create-instance')->close(); + } + + public function deleteInstance(int $pluginId): void + { + abort_unless(auth()->user() !== null, 403); + + $plugin = Plugin::where('id', $pluginId) + ->where('user_id', auth()->id()) + ->where('plugin_type', 'image_webhook') + ->firstOrFail(); + + $plugin->delete(); + $this->refreshInstances(); + } +}; +?> + +
+
+
+

Image Webhook + Plugin +

+ + Create Instance + +
+ + +
+
+ Create Image Webhook Instance + Create a new instance that accepts images via webhook +
+ +
+
+ +
+ +
+ + Create Instance +
+
+
+
+ + @if(empty($instances)) +
+ + No instances yet + Create your first Image Webhook instance to get started. + +
+ @else + + + + + + + + + + @foreach($instances as $instance) + + + + + @endforeach + +
+
Name
+
+
Actions
+
+ {{ $instance['name'] }} + +
+ + + + + + + + +
+
+ @endif + + @foreach($instances as $instance) + +
+ Delete {{ $instance['name'] }}? +

This will also remove this instance from your playlists.

+
+ +
+ + + Cancel + + Delete instance +
+
+ @endforeach +
+
+ diff --git a/resources/views/livewire/plugins/index.blade.php b/resources/views/livewire/plugins/index.blade.php index ab42b67..d902183 100644 --- a/resources/views/livewire/plugins/index.blade.php +++ b/resources/views/livewire/plugins/index.blade.php @@ -26,6 +26,8 @@ new class extends Component { ['name' => 'Markup', 'flux_icon_name' => 'code-bracket', 'detail_view_route' => 'plugins.markup'], 'api' => ['name' => 'API', 'flux_icon_name' => 'braces', 'detail_view_route' => 'plugins.api'], + 'image-webhook' => + ['name' => 'Image Webhook', 'flux_icon_name' => 'photo', 'detail_view_route' => 'plugins.image-webhook'], ]; protected $rules = [ @@ -40,7 +42,12 @@ new class extends Component { public function refreshPlugins(): void { - $userPlugins = auth()->user()?->plugins?->makeHidden(['render_markup', 'data_payload'])->toArray(); + // Only show recipe plugins in the main list (image_webhook has its own management page) + $userPlugins = auth()->user()?->plugins() + ->where('plugin_type', 'recipe') + ->get() + ->makeHidden(['render_markup', 'data_payload']) + ->toArray(); $allPlugins = array_merge($this->native_plugins, $userPlugins ?? []); $allPlugins = array_values($allPlugins); $allPlugins = $this->sortPlugins($allPlugins); @@ -156,6 +163,7 @@ new class extends Component {

Plugins & Recipes

+
+ + + + Add Recipe + - - - Add Recipe - - - - - - - Import Recipe Archive - - - Import from Catalog - - Seed Example Recipes - - - + + + + + Import from OSS Catalog + + @if(config('services.trmnl.liquid_enabled')) + + Import from TRMNL Catalog + + @endif + + + Import Recipe Archive + + + Seed Example Recipes + + + +
-
+