diff --git a/.cursor/mcp.json b/.cursor/mcp.json deleted file mode 100644 index ea30195..0000000 --- a/.cursor/mcp.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "mcpServers": { - "laravel-boost": { - "command": "php", - "args": [ - "./artisan", - "boost:mcp" - ] - } - } -} \ No newline at end of file diff --git a/.cursor/rules/laravel-boost.mdc b/.cursor/rules/laravel-boost.mdc deleted file mode 100644 index 9464f06..0000000 --- a/.cursor/rules/laravel-boost.mdc +++ /dev/null @@ -1,534 +0,0 @@ ---- -alwaysApply: true ---- - -=== foundation rules === - -# Laravel Boost Guidelines - -The Laravel Boost guidelines are specifically curated by Laravel maintainers for this application. These guidelines should be followed closely to enhance the user's satisfaction building Laravel applications. - -## Foundational Context -This application is a Laravel application and its main Laravel ecosystems package & versions are below. You are an expert with them all. Ensure you abide by these specific packages & versions. - -- php - 8.3.24 -- laravel/framework (LARAVEL) - v12 -- laravel/prompts (PROMPTS) - v0 -- livewire/flux (FLUXUI_FREE) - v2 -- livewire/livewire (LIVEWIRE) - v3 -- livewire/volt (VOLT) - v1 -- larastan/larastan (LARASTAN) - v3 -- laravel/pint (PINT) - v1 -- pestphp/pest (PEST) - v3 -- tailwindcss (TAILWINDCSS) - v4 - - -## Conventions -- You must follow all existing code conventions used in this application. When creating or editing a file, check sibling files for the correct structure, approach, naming. -- Use descriptive names for variables and methods. For example, `isRegisteredForDiscounts`, not `discount()`. -- Check for existing components to reuse before writing a new one. - -## Verification Scripts -- Do not create verification scripts or tinker when tests cover that functionality and prove it works. Unit and feature tests are more important. - -## Application Structure & Architecture -- Stick to existing directory structure - don't create new base folders without approval. -- Do not change the application's dependencies without approval. - -## Frontend Bundling -- If the user doesn't see a frontend change reflected in the UI, it could mean they need to run `npm run build`, `npm run dev`, or `composer run dev`. Ask them. - -## Replies -- Be concise in your explanations - focus on what's important rather than explaining obvious details. - -## Documentation Files -- You must only create documentation files if explicitly requested by the user. - - -=== boost rules === - -## Laravel Boost -- Laravel Boost is an MCP server that comes with powerful tools designed specifically for this application. Use them. - -## Artisan -- Use the `list-artisan-commands` tool when you need to call an Artisan command to double check the available parameters. - -## URLs -- Whenever you share a project URL with the user you should use the `get-absolute-url` tool to ensure you're using the correct scheme, domain / IP, and port. - -## Tinker / Debugging -- You should use the `tinker` tool when you need to execute PHP to debug code or query Eloquent models directly. -- Use the `database-query` tool when you only need to read from the database. - -## Reading Browser Logs With the `browser-logs` Tool -- You can read browser logs, errors, and exceptions using the `browser-logs` tool from Boost. -- Only recent browser logs will be useful - ignore old logs. - -## Searching Documentation (Critically Important) -- Boost comes with a powerful `search-docs` tool you should use before any other approaches. This tool automatically passes a list of installed packages and their versions to the remote Boost API, so it returns only version-specific documentation specific for the user's circumstance. You should pass an array of packages to filter on if you know you need docs for particular packages. -- The 'search-docs' tool is perfect for all Laravel related packages, including Laravel, Inertia, Livewire, Filament, Tailwind, Pest, Nova, Nightwatch, etc. -- You must use this tool to search for Laravel-ecosystem documentation before falling back to other approaches. -- Search the documentation before making code changes to ensure we are taking the correct approach. -- Use multiple, broad, simple, topic based queries to start. For example: `['rate limiting', 'routing rate limiting', 'routing']`. -- Do not add package names to queries - package information is already shared. For example, use `test resource table`, not `filament 4 test resource table`. - -### Available Search Syntax -- You can and should pass multiple queries at once. The most relevant results will be returned first. - -1. Simple Word Searches with auto-stemming - query=authentication - finds 'authenticate' and 'auth' -2. Multiple Words (AND Logic) - query=rate limit - finds knowledge containing both "rate" AND "limit" -3. Quoted Phrases (Exact Position) - query="infinite scroll" - Words must be adjacent and in that order -4. Mixed Queries - query=middleware "rate limit" - "middleware" AND exact phrase "rate limit" -5. Multiple Queries - queries=["authentication", "middleware"] - ANY of these terms - - -=== php rules === - -## PHP - -- Always use curly braces for control structures, even if it has one line. - -### Constructors -- Use PHP 8 constructor property promotion in `__construct()`. - - public function __construct(public GitHub $github) { } -- Do not allow empty `__construct()` methods with zero parameters. - -### Type Declarations -- Always use explicit return type declarations for methods and functions. -- Use appropriate PHP type hints for method parameters. - - -protected function isAccessible(User $user, ?string $path = null): bool -{ - ... -} - - -## Comments -- Prefer PHPDoc blocks over comments. Never use comments within the code itself unless there is something _very_ complex going on. - -## PHPDoc Blocks -- Add useful array shape type definitions for arrays when appropriate. - -## Enums -- Typically, keys in an Enum should be TitleCase. For example: `FavoritePerson`, `BestLake`, `Monthly`. - - -=== laravel/core rules === - -## Do Things the Laravel Way - -- Use `php artisan make:` commands to create new files (i.e. migrations, controllers, models, etc.). You can list available Artisan commands using the `list-artisan-commands` tool. -- If you're creating a generic PHP class, use `artisan make:class`. -- Pass `--no-interaction` to all Artisan commands to ensure they work without user input. You should also pass the correct `--options` to ensure correct behavior. - -### Database -- Always use proper Eloquent relationship methods with return type hints. Prefer relationship methods over raw queries or manual joins. -- Use Eloquent models and relationships before suggesting raw database queries -- Avoid `DB::`; prefer `Model::query()`. Generate code that leverages Laravel's ORM capabilities rather than bypassing them. -- Generate code that prevents N+1 query problems by using eager loading. -- Use Laravel's query builder for very complex database operations. - -### Model Creation -- When creating new models, create useful factories and seeders for them too. Ask the user if they need any other things, using `list-artisan-commands` to check the available options to `php artisan make:model`. - -### APIs & Eloquent Resources -- For APIs, default to using Eloquent API Resources and API versioning unless existing API routes do not, then you should follow existing application convention. - -### Controllers & Validation -- Always create Form Request classes for validation rather than inline validation in controllers. Include both validation rules and custom error messages. -- Check sibling Form Requests to see if the application uses array or string based validation rules. - -### Queues -- Use queued jobs for time-consuming operations with the `ShouldQueue` interface. - -### Authentication & Authorization -- Use Laravel's built-in authentication and authorization features (gates, policies, Sanctum, etc.). - -### URL Generation -- When generating links to other pages, prefer named routes and the `route()` function. - -### Configuration -- Use environment variables only in configuration files - never use the `env()` function directly outside of config files. Always use `config('app.name')`, not `env('APP_NAME')`. - -### Testing -- When creating models for tests, use the factories for the models. Check if the factory has custom states that can be used before manually setting up the model. -- Faker: Use methods such as `$this->faker->word()` or `fake()->randomDigit()`. Follow existing conventions whether to use `$this->faker` or `fake()`. -- When creating tests, make use of `php artisan make:test [options] ` to create a feature test, and pass `--unit` to create a unit test. Most tests should be feature tests. - -### Vite Error -- If you receive an "Illuminate\Foundation\ViteException: Unable to locate file in Vite manifest" error, you can run `npm run build` or ask the user to run `npm run dev` or `composer run dev`. - - -=== laravel/v12 rules === - -## Laravel 12 - -- Use the `search-docs` tool to get version specific documentation. -- Since Laravel 11, Laravel has a new streamlined file structure which this project uses. - -### Laravel 12 Structure -- No middleware files in `app/Http/Middleware/`. -- `bootstrap/app.php` is the file to register middleware, exceptions, and routing files. -- `bootstrap/providers.php` contains application specific service providers. -- **No app\Console\Kernel.php** - use `bootstrap/app.php` or `routes/console.php` for console configuration. -- **Commands auto-register** - files in `app/Console/Commands/` are automatically available and do not require manual registration. - -### Database -- When modifying a column, the migration must include all of the attributes that were previously defined on the column. Otherwise, they will be dropped and lost. -- Laravel 11 allows limiting eagerly loaded records natively, without external packages: `$query->latest()->limit(10);`. - -### Models -- Casts can and likely should be set in a `casts()` method on a model rather than the `$casts` property. Follow existing conventions from other models. - - -=== fluxui-free/core rules === - -## Flux UI Free - -- This project is using the free edition of Flux UI. It has full access to the free components and variants, but does not have access to the Pro components. -- Flux UI is a component library for Livewire. Flux is a robust, hand-crafted, UI component library for your Livewire applications. It's built using Tailwind CSS and provides a set of components that are easy to use and customize. -- You should use Flux UI components when available. -- Fallback to standard Blade components if Flux is unavailable. -- If available, use Laravel Boost's `search-docs` tool to get the exact documentation and code snippets available for this project. -- Flux UI components look like this: - - - - - - -### Available Components -This is correct as of Boost installation, but there may be additional components within the codebase. - - -avatar, badge, brand, breadcrumbs, button, callout, checkbox, dropdown, field, heading, icon, input, modal, navbar, profile, radio, select, separator, switch, text, textarea, tooltip - - - -=== livewire/core rules === - -## Livewire Core -- Use the `search-docs` tool to find exact version specific documentation for how to write Livewire & Livewire tests. -- Use the `php artisan make:livewire [Posts\CreatePost]` artisan command to create new components -- State should live on the server, with the UI reflecting it. -- All Livewire requests hit the Laravel backend, they're like regular HTTP requests. Always validate form data, and run authorization checks in Livewire actions. - -## Livewire Best Practices -- Livewire components require a single root element. -- Use `wire:loading` and `wire:dirty` for delightful loading states. -- Add `wire:key` in loops: - - ```blade - @foreach ($items as $item) -
- {{ $item->name }} -
- @endforeach - ``` - -- Prefer lifecycle hooks like `mount()`, `updatedFoo()`) for initialization and reactive side effects: - - - public function mount(User $user) { $this->user = $user; } - public function updatedSearch() { $this->resetPage(); } - - - -## Testing Livewire - - - Livewire::test(Counter::class) - ->assertSet('count', 0) - ->call('increment') - ->assertSet('count', 1) - ->assertSee(1) - ->assertStatus(200); - - - - - $this->get('/posts/create') - ->assertSeeLivewire(CreatePost::class); - - - -=== livewire/v3 rules === - -## Livewire 3 - -### Key Changes From Livewire 2 -- These things changed in Livewire 2, but may not have been updated in this application. Verify this application's setup to ensure you conform with application conventions. - - Use `wire:model.live` for real-time updates, `wire:model` is now deferred by default. - - Components now use the `App\Livewire` namespace (not `App\Http\Livewire`). - - Use `$this->dispatch()` to dispatch events (not `emit` or `dispatchBrowserEvent`). - - Use the `components.layouts.app` view as the typical layout path (not `layouts.app`). - -### New Directives -- `wire:show`, `wire:transition`, `wire:cloak`, `wire:offline`, `wire:target` are available for use. Use the documentation to find usage examples. - -### Alpine -- Alpine is now included with Livewire, don't manually include Alpine.js. -- Plugins included with Alpine: persist, intersect, collapse, and focus. - -### Lifecycle Hooks -- You can listen for `livewire:init` to hook into Livewire initialization, and `fail.status === 419` for the page expiring: - - -document.addEventListener('livewire:init', function () { - Livewire.hook('request', ({ fail }) => { - if (fail && fail.status === 419) { - alert('Your session expired'); - } - }); - - Livewire.hook('message.failed', (message, component) => { - console.error(message); - }); -}); - - - -=== volt/core rules === - -## Livewire Volt - -- This project uses Livewire Volt for interactivity within its pages. New pages requiring interactivity must also use Livewire Volt. There is documentation available for it. -- Make new Volt components using `php artisan make:volt [name] [--test] [--pest]` -- Volt is a **class-based** and **functional** API for Livewire that supports single-file components, allowing a component's PHP logic and Blade templates to co-exist in the same file -- Livewire Volt allows PHP logic and Blade templates in one file. Components use the `@livewire("volt-anonymous-fragment-eyJuYW1lIjoidm9sdC1hbm9ueW1vdXMtZnJhZ21lbnQtYmQ5YWJiNTE3YWMyMTgwOTA1ZmUxMzAxODk0MGJiZmIiLCJwYXRoIjoic3RvcmFnZVwvZnJhbWV3b3JrXC92aWV3c1wvMTUxYWRjZWRjMzBhMzllOWIxNzQ0ZDRiMWRjY2FjYWIuYmxhZGUucGhwIn0=", Livewire\Volt\Precompilers\ExtractFragments::componentArguments([...get_defined_vars(), ...array ( -)])) - - - -### Volt Class Based Component Example -To get started, define an anonymous class that extends Livewire\Volt\Component. Within the class, you may utilize all of the features of Livewire using traditional Livewire syntax: - - - -use Livewire\Volt\Component; - -new class extends Component { - public $count = 0; - - public function increment() - { - $this->count++; - } -} ?> - -
-

{{ $count }}

- -
-
- - -### Testing Volt & Volt Components -- Use the existing directory for tests if it already exists. Otherwise, fallback to `tests/Feature/Volt`. - - -use Livewire\Volt\Volt; - -test('counter increments', function () { - Volt::test('counter') - ->assertSee('Count: 0') - ->call('increment') - ->assertSee('Count: 1'); -}); - - - - -declare(strict_types=1); - -use App\Models\{User, Product}; -use Livewire\Volt\Volt; - -test('product form creates product', function () { - $user = User::factory()->create(); - - Volt::test('pages.products.create') - ->actingAs($user) - ->set('form.name', 'Test Product') - ->set('form.description', 'Test Description') - ->set('form.price', 99.99) - ->call('create') - ->assertHasNoErrors(); - - expect(Product::where('name', 'Test Product')->exists())->toBeTrue(); -}); - - - -### Common Patterns - - - - null, 'search' => '']); - -$products = computed(fn() => Product::when($this->search, - fn($q) => $q->where('name', 'like', "%{$this->search}%") -)->get()); - -$edit = fn(Product $product) => $this->editing = $product->id; -$delete = fn(Product $product) => $product->delete(); - -?> - - - - - - - - - - - - - - - Save - Saving... - - - - -=== pint/core rules === - -## Laravel Pint Code Formatter - -- You must run `vendor/bin/pint --dirty` before finalizing changes to ensure your code matches the project's expected style. -- Do not run `vendor/bin/pint --test`, simply run `vendor/bin/pint` to fix any formatting issues. - - -=== pest/core rules === - -## Pest - -### Testing -- If you need to verify a feature is working, write or update a Unit / Feature test. - -### Pest Tests -- All tests must be written using Pest. Use `php artisan make:test --pest `. -- You must not remove any tests or test files from the tests directory without approval. These are not temporary or helper files - these are core to the application. -- Tests should test all of the happy paths, failure paths, and weird paths. -- Tests live in the `tests/Feature` and `tests/Unit` directories. -- Pest tests look and behave like this: - -it('is true', function () { - expect(true)->toBeTrue(); -}); - - -### Running Tests -- Run the minimal number of tests using an appropriate filter before finalizing code edits. -- To run all tests: `php artisan test`. -- To run all tests in a file: `php artisan test tests/Feature/ExampleTest.php`. -- To filter on a particular test name: `php artisan test --filter=testName` (recommended after making a change to a related file). -- When the tests relating to your changes are passing, ask the user if they would like to run the entire test suite to ensure everything is still passing. - -### Pest Assertions -- When asserting status codes on a response, use the specific method like `assertForbidden` and `assertNotFound` instead of using `assertStatus(403)` or similar, e.g.: - -it('returns all', function () { - $response = $this->postJson('/api/docs', []); - - $response->assertSuccessful(); -}); - - -### Mocking -- Mocking can be very helpful when appropriate. -- When mocking, you can use the `Pest\Laravel\mock` Pest function, but always import it via `use function Pest\Laravel\mock;` before using it. Alternatively, you can use `$this->mock()` if existing tests do. -- You can also create partial mocks using the same import or self method. - -### Datasets -- Use datasets in Pest to simplify tests which have a lot of duplicated data. This is often the case when testing validation rules, so consider going with this solution when writing tests for validation rules. - - -it('has emails', function (string $email) { - expect($email)->not->toBeEmpty(); -})->with([ - 'james' => 'james@laravel.com', - 'taylor' => 'taylor@laravel.com', -]); - - - -=== tailwindcss/core rules === - -## Tailwind Core - -- Use Tailwind CSS classes to style HTML, check and use existing tailwind conventions within the project before writing your own. -- Offer to extract repeated patterns into components that match the project's conventions (i.e. Blade, JSX, Vue, etc..) -- Think through class placement, order, priority, and defaults - remove redundant classes, add classes to parent or child carefully to limit repetition, group elements logically -- You can use the `search-docs` tool to get exact examples from the official documentation when needed. - -### Spacing -- When listing items, use gap utilities for spacing, don't use margins. - - -
-
Superior
-
Michigan
-
Erie
-
-
- - -### Dark Mode -- If existing pages and components support dark mode, new pages and components must support dark mode in a similar way, typically using `dark:`. - - -=== tailwindcss/v4 rules === - -## Tailwind 4 - -- Always use Tailwind CSS v4 - do not use the deprecated utilities. -- `corePlugins` is not supported in Tailwind v4. -- In Tailwind v4, you import Tailwind using a regular CSS `@import` statement, not using the `@tailwind` directives used in v3: - - - - -### Replaced Utilities -- Tailwind v4 removed deprecated utilities. Do not use the deprecated option - use the replacement. -- Opacity values are still numeric. - -| Deprecated | Replacement | -|------------+--------------| -| bg-opacity-* | bg-black/* | -| text-opacity-* | text-black/* | -| border-opacity-* | border-black/* | -| divide-opacity-* | divide-black/* | -| ring-opacity-* | ring-black/* | -| placeholder-opacity-* | placeholder-black/* | -| flex-shrink-* | shrink-* | -| flex-grow-* | grow-* | -| overflow-ellipsis | text-ellipsis | -| decoration-slice | box-decoration-slice | -| decoration-clone | box-decoration-clone | - - -=== tests rules === - -## Test Enforcement - -- Every change must be programmatically tested. Write a new test or update an existing test, then run the affected tests to make sure they pass. -- Run the minimum number of tests needed to ensure code quality and speed. Use `php artisan test` with a specific filename or filter. -
\ No newline at end of file diff --git a/.devcontainer/cli/Dockerfile b/.devcontainer/cli/Dockerfile index 908acef..ab13330 100644 --- a/.devcontainer/cli/Dockerfile +++ b/.devcontainer/cli/Dockerfile @@ -1,5 +1,5 @@ # From official php image. -FROM php:8.3-cli-alpine +FROM php:8.4-cli-alpine # Create a user group and account under id 1000. RUN addgroup -g 1000 -S user && adduser -u 1000 -D user -G user # Install quality-of-life packages. @@ -9,21 +9,22 @@ 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 RUN mkdir -p /usr/src/php/ext/imagick RUN chmod 777 /usr/src/php/ext/imagick -RUN curl -fsSL https://github.com/Imagick/imagick/archive/refs/tags/3.7.0.tar.gz | tar xvz -C "/usr/src/php/ext/imagick" --strip 1 +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/php83 -RUN ln -s /usr/local/bin/php /usr/bin/php83 +RUN rm -f /usr/bin/php84 +RUN ln -s /usr/local/bin/php /usr/bin/php84 # Install postgres pdo driver. # RUN apk add --no-cache postgresql-dev && docker-php-ext-install pdo_pgsql # Install redis driver. diff --git a/.devcontainer/fpm/Dockerfile b/.devcontainer/fpm/Dockerfile index b9770e3..3e658b6 100644 --- a/.devcontainer/fpm/Dockerfile +++ b/.devcontainer/fpm/Dockerfile @@ -1,5 +1,5 @@ # From official php image. -FROM php:8.3-fpm-alpine +FROM php:8.4-fpm-alpine RUN addgroup -g 1000 -S user && adduser -u 1000 -D user -G user # Install postgres pdo driver. # RUN apk add --no-cache postgresql-dev && docker-php-ext-install pdo_pgsql @@ -14,17 +14,18 @@ RUN apk add --no-cache \ nodejs \ npm \ imagemagick-dev \ - chromium + chromium \ + libzip-dev ENV PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium ENV PUPPETEER_DOCKER=1 RUN mkdir -p /usr/src/php/ext/imagick RUN chmod 777 /usr/src/php/ext/imagick -RUN curl -fsSL https://github.com/Imagick/imagick/archive/refs/tags/3.7.0.tar.gz | tar xvz -C "/usr/src/php/ext/imagick" --strip 1 +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/php83 -RUN ln -s /usr/local/bin/php /usr/bin/php83 +RUN rm -f /usr/bin/php84 +RUN ln -s /usr/local/bin/php /usr/bin/php84 diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md deleted file mode 100644 index a331541..0000000 --- a/.github/copilot-instructions.md +++ /dev/null @@ -1,531 +0,0 @@ - -=== foundation rules === - -# Laravel Boost Guidelines - -The Laravel Boost guidelines are specifically curated by Laravel maintainers for this application. These guidelines should be followed closely to enhance the user's satisfaction building Laravel applications. - -## Foundational Context -This application is a Laravel application and its main Laravel ecosystems package & versions are below. You are an expert with them all. Ensure you abide by these specific packages & versions. - -- php - 8.3.24 -- laravel/framework (LARAVEL) - v12 -- laravel/prompts (PROMPTS) - v0 -- livewire/flux (FLUXUI_FREE) - v2 -- livewire/livewire (LIVEWIRE) - v3 -- livewire/volt (VOLT) - v1 -- larastan/larastan (LARASTAN) - v3 -- laravel/pint (PINT) - v1 -- pestphp/pest (PEST) - v3 -- tailwindcss (TAILWINDCSS) - v4 - - -## Conventions -- You must follow all existing code conventions used in this application. When creating or editing a file, check sibling files for the correct structure, approach, naming. -- Use descriptive names for variables and methods. For example, `isRegisteredForDiscounts`, not `discount()`. -- Check for existing components to reuse before writing a new one. - -## Verification Scripts -- Do not create verification scripts or tinker when tests cover that functionality and prove it works. Unit and feature tests are more important. - -## Application Structure & Architecture -- Stick to existing directory structure - don't create new base folders without approval. -- Do not change the application's dependencies without approval. - -## Frontend Bundling -- If the user doesn't see a frontend change reflected in the UI, it could mean they need to run `npm run build`, `npm run dev`, or `composer run dev`. Ask them. - -## Replies -- Be concise in your explanations - focus on what's important rather than explaining obvious details. - -## Documentation Files -- You must only create documentation files if explicitly requested by the user. - - -=== boost rules === - -## Laravel Boost -- Laravel Boost is an MCP server that comes with powerful tools designed specifically for this application. Use them. - -## Artisan -- Use the `list-artisan-commands` tool when you need to call an Artisan command to double check the available parameters. - -## URLs -- Whenever you share a project URL with the user you should use the `get-absolute-url` tool to ensure you're using the correct scheme, domain / IP, and port. - -## Tinker / Debugging -- You should use the `tinker` tool when you need to execute PHP to debug code or query Eloquent models directly. -- Use the `database-query` tool when you only need to read from the database. - -## Reading Browser Logs With the `browser-logs` Tool -- You can read browser logs, errors, and exceptions using the `browser-logs` tool from Boost. -- Only recent browser logs will be useful - ignore old logs. - -## Searching Documentation (Critically Important) -- Boost comes with a powerful `search-docs` tool you should use before any other approaches. This tool automatically passes a list of installed packages and their versions to the remote Boost API, so it returns only version-specific documentation specific for the user's circumstance. You should pass an array of packages to filter on if you know you need docs for particular packages. -- The 'search-docs' tool is perfect for all Laravel related packages, including Laravel, Inertia, Livewire, Filament, Tailwind, Pest, Nova, Nightwatch, etc. -- You must use this tool to search for Laravel-ecosystem documentation before falling back to other approaches. -- Search the documentation before making code changes to ensure we are taking the correct approach. -- Use multiple, broad, simple, topic based queries to start. For example: `['rate limiting', 'routing rate limiting', 'routing']`. -- Do not add package names to queries - package information is already shared. For example, use `test resource table`, not `filament 4 test resource table`. - -### Available Search Syntax -- You can and should pass multiple queries at once. The most relevant results will be returned first. - -1. Simple Word Searches with auto-stemming - query=authentication - finds 'authenticate' and 'auth' -2. Multiple Words (AND Logic) - query=rate limit - finds knowledge containing both "rate" AND "limit" -3. Quoted Phrases (Exact Position) - query="infinite scroll" - Words must be adjacent and in that order -4. Mixed Queries - query=middleware "rate limit" - "middleware" AND exact phrase "rate limit" -5. Multiple Queries - queries=["authentication", "middleware"] - ANY of these terms - - -=== php rules === - -## PHP - -- Always use curly braces for control structures, even if it has one line. - -### Constructors -- Use PHP 8 constructor property promotion in `__construct()`. - - public function __construct(public GitHub $github) { } -- Do not allow empty `__construct()` methods with zero parameters. - -### Type Declarations -- Always use explicit return type declarations for methods and functions. -- Use appropriate PHP type hints for method parameters. - - -protected function isAccessible(User $user, ?string $path = null): bool -{ - ... -} - - -## Comments -- Prefer PHPDoc blocks over comments. Never use comments within the code itself unless there is something _very_ complex going on. - -## PHPDoc Blocks -- Add useful array shape type definitions for arrays when appropriate. - -## Enums -- Typically, keys in an Enum should be TitleCase. For example: `FavoritePerson`, `BestLake`, `Monthly`. - - -=== laravel/core rules === - -## Do Things the Laravel Way - -- Use `php artisan make:` commands to create new files (i.e. migrations, controllers, models, etc.). You can list available Artisan commands using the `list-artisan-commands` tool. -- If you're creating a generic PHP class, use `artisan make:class`. -- Pass `--no-interaction` to all Artisan commands to ensure they work without user input. You should also pass the correct `--options` to ensure correct behavior. - -### Database -- Always use proper Eloquent relationship methods with return type hints. Prefer relationship methods over raw queries or manual joins. -- Use Eloquent models and relationships before suggesting raw database queries -- Avoid `DB::`; prefer `Model::query()`. Generate code that leverages Laravel's ORM capabilities rather than bypassing them. -- Generate code that prevents N+1 query problems by using eager loading. -- Use Laravel's query builder for very complex database operations. - -### Model Creation -- When creating new models, create useful factories and seeders for them too. Ask the user if they need any other things, using `list-artisan-commands` to check the available options to `php artisan make:model`. - -### APIs & Eloquent Resources -- For APIs, default to using Eloquent API Resources and API versioning unless existing API routes do not, then you should follow existing application convention. - -### Controllers & Validation -- Always create Form Request classes for validation rather than inline validation in controllers. Include both validation rules and custom error messages. -- Check sibling Form Requests to see if the application uses array or string based validation rules. - -### Queues -- Use queued jobs for time-consuming operations with the `ShouldQueue` interface. - -### Authentication & Authorization -- Use Laravel's built-in authentication and authorization features (gates, policies, Sanctum, etc.). - -### URL Generation -- When generating links to other pages, prefer named routes and the `route()` function. - -### Configuration -- Use environment variables only in configuration files - never use the `env()` function directly outside of config files. Always use `config('app.name')`, not `env('APP_NAME')`. - -### Testing -- When creating models for tests, use the factories for the models. Check if the factory has custom states that can be used before manually setting up the model. -- Faker: Use methods such as `$this->faker->word()` or `fake()->randomDigit()`. Follow existing conventions whether to use `$this->faker` or `fake()`. -- When creating tests, make use of `php artisan make:test [options] ` to create a feature test, and pass `--unit` to create a unit test. Most tests should be feature tests. - -### Vite Error -- If you receive an "Illuminate\Foundation\ViteException: Unable to locate file in Vite manifest" error, you can run `npm run build` or ask the user to run `npm run dev` or `composer run dev`. - - -=== laravel/v12 rules === - -## Laravel 12 - -- Use the `search-docs` tool to get version specific documentation. -- Since Laravel 11, Laravel has a new streamlined file structure which this project uses. - -### Laravel 12 Structure -- No middleware files in `app/Http/Middleware/`. -- `bootstrap/app.php` is the file to register middleware, exceptions, and routing files. -- `bootstrap/providers.php` contains application specific service providers. -- **No app\Console\Kernel.php** - use `bootstrap/app.php` or `routes/console.php` for console configuration. -- **Commands auto-register** - files in `app/Console/Commands/` are automatically available and do not require manual registration. - -### Database -- When modifying a column, the migration must include all of the attributes that were previously defined on the column. Otherwise, they will be dropped and lost. -- Laravel 11 allows limiting eagerly loaded records natively, without external packages: `$query->latest()->limit(10);`. - -### Models -- Casts can and likely should be set in a `casts()` method on a model rather than the `$casts` property. Follow existing conventions from other models. - - -=== fluxui-free/core rules === - -## Flux UI Free - -- This project is using the free edition of Flux UI. It has full access to the free components and variants, but does not have access to the Pro components. -- Flux UI is a component library for Livewire. Flux is a robust, hand-crafted, UI component library for your Livewire applications. It's built using Tailwind CSS and provides a set of components that are easy to use and customize. -- You should use Flux UI components when available. -- Fallback to standard Blade components if Flux is unavailable. -- If available, use Laravel Boost's `search-docs` tool to get the exact documentation and code snippets available for this project. -- Flux UI components look like this: - - - - - - -### Available Components -This is correct as of Boost installation, but there may be additional components within the codebase. - - -avatar, badge, brand, breadcrumbs, button, callout, checkbox, dropdown, field, heading, icon, input, modal, navbar, profile, radio, select, separator, switch, text, textarea, tooltip - - - -=== livewire/core rules === - -## Livewire Core -- Use the `search-docs` tool to find exact version specific documentation for how to write Livewire & Livewire tests. -- Use the `php artisan make:livewire [Posts\CreatePost]` artisan command to create new components -- State should live on the server, with the UI reflecting it. -- All Livewire requests hit the Laravel backend, they're like regular HTTP requests. Always validate form data, and run authorization checks in Livewire actions. - -## Livewire Best Practices -- Livewire components require a single root element. -- Use `wire:loading` and `wire:dirty` for delightful loading states. -- Add `wire:key` in loops: - - ```blade - @foreach ($items as $item) -
- {{ $item->name }} -
- @endforeach - ``` - -- Prefer lifecycle hooks like `mount()`, `updatedFoo()`) for initialization and reactive side effects: - - - public function mount(User $user) { $this->user = $user; } - public function updatedSearch() { $this->resetPage(); } - - - -## Testing Livewire - - - Livewire::test(Counter::class) - ->assertSet('count', 0) - ->call('increment') - ->assertSet('count', 1) - ->assertSee(1) - ->assertStatus(200); - - - - - $this->get('/posts/create') - ->assertSeeLivewire(CreatePost::class); - - - -=== livewire/v3 rules === - -## Livewire 3 - -### Key Changes From Livewire 2 -- These things changed in Livewire 2, but may not have been updated in this application. Verify this application's setup to ensure you conform with application conventions. - - Use `wire:model.live` for real-time updates, `wire:model` is now deferred by default. - - Components now use the `App\Livewire` namespace (not `App\Http\Livewire`). - - Use `$this->dispatch()` to dispatch events (not `emit` or `dispatchBrowserEvent`). - - Use the `components.layouts.app` view as the typical layout path (not `layouts.app`). - -### New Directives -- `wire:show`, `wire:transition`, `wire:cloak`, `wire:offline`, `wire:target` are available for use. Use the documentation to find usage examples. - -### Alpine -- Alpine is now included with Livewire, don't manually include Alpine.js. -- Plugins included with Alpine: persist, intersect, collapse, and focus. - -### Lifecycle Hooks -- You can listen for `livewire:init` to hook into Livewire initialization, and `fail.status === 419` for the page expiring: - - -document.addEventListener('livewire:init', function () { - Livewire.hook('request', ({ fail }) => { - if (fail && fail.status === 419) { - alert('Your session expired'); - } - }); - - Livewire.hook('message.failed', (message, component) => { - console.error(message); - }); -}); - - - -=== volt/core rules === - -## Livewire Volt - -- This project uses Livewire Volt for interactivity within its pages. New pages requiring interactivity must also use Livewire Volt. There is documentation available for it. -- Make new Volt components using `php artisan make:volt [name] [--test] [--pest]` -- Volt is a **class-based** and **functional** API for Livewire that supports single-file components, allowing a component's PHP logic and Blade templates to co-exist in the same file -- Livewire Volt allows PHP logic and Blade templates in one file. Components use the `@livewire("volt-anonymous-fragment-eyJuYW1lIjoidm9sdC1hbm9ueW1vdXMtZnJhZ21lbnQtYmQ5YWJiNTE3YWMyMTgwOTA1ZmUxMzAxODk0MGJiZmIiLCJwYXRoIjoic3RvcmFnZVwvZnJhbWV3b3JrXC92aWV3c1wvMTUxYWRjZWRjMzBhMzllOWIxNzQ0ZDRiMWRjY2FjYWIuYmxhZGUucGhwIn0=", Livewire\Volt\Precompilers\ExtractFragments::componentArguments([...get_defined_vars(), ...array ( -)])) - - - -### Volt Class Based Component Example -To get started, define an anonymous class that extends Livewire\Volt\Component. Within the class, you may utilize all of the features of Livewire using traditional Livewire syntax: - - - -use Livewire\Volt\Component; - -new class extends Component { - public $count = 0; - - public function increment() - { - $this->count++; - } -} ?> - -
-

{{ $count }}

- -
-
- - -### Testing Volt & Volt Components -- Use the existing directory for tests if it already exists. Otherwise, fallback to `tests/Feature/Volt`. - - -use Livewire\Volt\Volt; - -test('counter increments', function () { - Volt::test('counter') - ->assertSee('Count: 0') - ->call('increment') - ->assertSee('Count: 1'); -}); - - - - -declare(strict_types=1); - -use App\Models\{User, Product}; -use Livewire\Volt\Volt; - -test('product form creates product', function () { - $user = User::factory()->create(); - - Volt::test('pages.products.create') - ->actingAs($user) - ->set('form.name', 'Test Product') - ->set('form.description', 'Test Description') - ->set('form.price', 99.99) - ->call('create') - ->assertHasNoErrors(); - - expect(Product::where('name', 'Test Product')->exists())->toBeTrue(); -}); - - - -### Common Patterns - - - - null, 'search' => '']); - -$products = computed(fn() => Product::when($this->search, - fn($q) => $q->where('name', 'like', "%{$this->search}%") -)->get()); - -$edit = fn(Product $product) => $this->editing = $product->id; -$delete = fn(Product $product) => $product->delete(); - -?> - - - - - - - - - - - - - - - Save - Saving... - - - - -=== pint/core rules === - -## Laravel Pint Code Formatter - -- You must run `vendor/bin/pint --dirty` before finalizing changes to ensure your code matches the project's expected style. -- Do not run `vendor/bin/pint --test`, simply run `vendor/bin/pint` to fix any formatting issues. - - -=== pest/core rules === - -## Pest - -### Testing -- If you need to verify a feature is working, write or update a Unit / Feature test. - -### Pest Tests -- All tests must be written using Pest. Use `php artisan make:test --pest `. -- You must not remove any tests or test files from the tests directory without approval. These are not temporary or helper files - these are core to the application. -- Tests should test all of the happy paths, failure paths, and weird paths. -- Tests live in the `tests/Feature` and `tests/Unit` directories. -- Pest tests look and behave like this: - -it('is true', function () { - expect(true)->toBeTrue(); -}); - - -### Running Tests -- Run the minimal number of tests using an appropriate filter before finalizing code edits. -- To run all tests: `php artisan test`. -- To run all tests in a file: `php artisan test tests/Feature/ExampleTest.php`. -- To filter on a particular test name: `php artisan test --filter=testName` (recommended after making a change to a related file). -- When the tests relating to your changes are passing, ask the user if they would like to run the entire test suite to ensure everything is still passing. - -### Pest Assertions -- When asserting status codes on a response, use the specific method like `assertForbidden` and `assertNotFound` instead of using `assertStatus(403)` or similar, e.g.: - -it('returns all', function () { - $response = $this->postJson('/api/docs', []); - - $response->assertSuccessful(); -}); - - -### Mocking -- Mocking can be very helpful when appropriate. -- When mocking, you can use the `Pest\Laravel\mock` Pest function, but always import it via `use function Pest\Laravel\mock;` before using it. Alternatively, you can use `$this->mock()` if existing tests do. -- You can also create partial mocks using the same import or self method. - -### Datasets -- Use datasets in Pest to simplify tests which have a lot of duplicated data. This is often the case when testing validation rules, so consider going with this solution when writing tests for validation rules. - - -it('has emails', function (string $email) { - expect($email)->not->toBeEmpty(); -})->with([ - 'james' => 'james@laravel.com', - 'taylor' => 'taylor@laravel.com', -]); - - - -=== tailwindcss/core rules === - -## Tailwind Core - -- Use Tailwind CSS classes to style HTML, check and use existing tailwind conventions within the project before writing your own. -- Offer to extract repeated patterns into components that match the project's conventions (i.e. Blade, JSX, Vue, etc..) -- Think through class placement, order, priority, and defaults - remove redundant classes, add classes to parent or child carefully to limit repetition, group elements logically -- You can use the `search-docs` tool to get exact examples from the official documentation when needed. - -### Spacing -- When listing items, use gap utilities for spacing, don't use margins. - - -
-
Superior
-
Michigan
-
Erie
-
-
- - -### Dark Mode -- If existing pages and components support dark mode, new pages and components must support dark mode in a similar way, typically using `dark:`. - - -=== tailwindcss/v4 rules === - -## Tailwind 4 - -- Always use Tailwind CSS v4 - do not use the deprecated utilities. -- `corePlugins` is not supported in Tailwind v4. -- In Tailwind v4, you import Tailwind using a regular CSS `@import` statement, not using the `@tailwind` directives used in v3: - - - - -### Replaced Utilities -- Tailwind v4 removed deprecated utilities. Do not use the deprecated option - use the replacement. -- Opacity values are still numeric. - -| Deprecated | Replacement | -|------------+--------------| -| bg-opacity-* | bg-black/* | -| text-opacity-* | text-black/* | -| border-opacity-* | border-black/* | -| divide-opacity-* | divide-black/* | -| ring-opacity-* | ring-black/* | -| placeholder-opacity-* | placeholder-black/* | -| flex-shrink-* | shrink-* | -| flex-grow-* | grow-* | -| overflow-ellipsis | text-ellipsis | -| decoration-slice | box-decoration-slice | -| decoration-clone | box-decoration-clone | - - -=== tests rules === - -## Test Enforcement - -- Every change must be programmatically tested. Write a new test or update an existing test, then run the affected tests to make sure they pass. -- Run the minimum number of tests needed to ensure code quality and speed. Use `php artisan test` with a specific filename or filter. -
\ No newline at end of file diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml index 048db51..a4ff129 100644 --- a/.github/workflows/docker-build.yml +++ b/.github/workflows/docker-build.yml @@ -42,8 +42,7 @@ jobs: with: images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} tags: | - type=ref,event=tag - latest + type=semver,pattern={{version}} - name: Build and push Docker image uses: docker/build-push-action@v6 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index fd03705..78e4fbb 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -22,7 +22,7 @@ jobs: - name: Setup PHP uses: shivammathur/setup-php@v2 with: - php-version: 8.3 + php-version: 8.4 coverage: xdebug - name: Setup Node diff --git a/.gitignore b/.gitignore index 3a2ae5a..0eb46d3 100644 --- a/.gitignore +++ b/.gitignore @@ -23,3 +23,17 @@ yarn-error.log /.zed /database/seeders/PersonalDeviceSeeder.php /.junie/mcp/mcp.json +/.cursor/mcp.json +/.cursor/rules/laravel-boost.mdc +/.github/copilot-instructions.md +/.junie/guidelines.md +/CLAUDE.md +/.mcp.json +/.ai +.DS_Store +/boost.json +/.gemini +/GEMINI.md +/.claude +/AGENTS.md +/opencode.json diff --git a/.junie/guidelines.md b/.junie/guidelines.md deleted file mode 100644 index a331541..0000000 --- a/.junie/guidelines.md +++ /dev/null @@ -1,531 +0,0 @@ - -=== foundation rules === - -# Laravel Boost Guidelines - -The Laravel Boost guidelines are specifically curated by Laravel maintainers for this application. These guidelines should be followed closely to enhance the user's satisfaction building Laravel applications. - -## Foundational Context -This application is a Laravel application and its main Laravel ecosystems package & versions are below. You are an expert with them all. Ensure you abide by these specific packages & versions. - -- php - 8.3.24 -- laravel/framework (LARAVEL) - v12 -- laravel/prompts (PROMPTS) - v0 -- livewire/flux (FLUXUI_FREE) - v2 -- livewire/livewire (LIVEWIRE) - v3 -- livewire/volt (VOLT) - v1 -- larastan/larastan (LARASTAN) - v3 -- laravel/pint (PINT) - v1 -- pestphp/pest (PEST) - v3 -- tailwindcss (TAILWINDCSS) - v4 - - -## Conventions -- You must follow all existing code conventions used in this application. When creating or editing a file, check sibling files for the correct structure, approach, naming. -- Use descriptive names for variables and methods. For example, `isRegisteredForDiscounts`, not `discount()`. -- Check for existing components to reuse before writing a new one. - -## Verification Scripts -- Do not create verification scripts or tinker when tests cover that functionality and prove it works. Unit and feature tests are more important. - -## Application Structure & Architecture -- Stick to existing directory structure - don't create new base folders without approval. -- Do not change the application's dependencies without approval. - -## Frontend Bundling -- If the user doesn't see a frontend change reflected in the UI, it could mean they need to run `npm run build`, `npm run dev`, or `composer run dev`. Ask them. - -## Replies -- Be concise in your explanations - focus on what's important rather than explaining obvious details. - -## Documentation Files -- You must only create documentation files if explicitly requested by the user. - - -=== boost rules === - -## Laravel Boost -- Laravel Boost is an MCP server that comes with powerful tools designed specifically for this application. Use them. - -## Artisan -- Use the `list-artisan-commands` tool when you need to call an Artisan command to double check the available parameters. - -## URLs -- Whenever you share a project URL with the user you should use the `get-absolute-url` tool to ensure you're using the correct scheme, domain / IP, and port. - -## Tinker / Debugging -- You should use the `tinker` tool when you need to execute PHP to debug code or query Eloquent models directly. -- Use the `database-query` tool when you only need to read from the database. - -## Reading Browser Logs With the `browser-logs` Tool -- You can read browser logs, errors, and exceptions using the `browser-logs` tool from Boost. -- Only recent browser logs will be useful - ignore old logs. - -## Searching Documentation (Critically Important) -- Boost comes with a powerful `search-docs` tool you should use before any other approaches. This tool automatically passes a list of installed packages and their versions to the remote Boost API, so it returns only version-specific documentation specific for the user's circumstance. You should pass an array of packages to filter on if you know you need docs for particular packages. -- The 'search-docs' tool is perfect for all Laravel related packages, including Laravel, Inertia, Livewire, Filament, Tailwind, Pest, Nova, Nightwatch, etc. -- You must use this tool to search for Laravel-ecosystem documentation before falling back to other approaches. -- Search the documentation before making code changes to ensure we are taking the correct approach. -- Use multiple, broad, simple, topic based queries to start. For example: `['rate limiting', 'routing rate limiting', 'routing']`. -- Do not add package names to queries - package information is already shared. For example, use `test resource table`, not `filament 4 test resource table`. - -### Available Search Syntax -- You can and should pass multiple queries at once. The most relevant results will be returned first. - -1. Simple Word Searches with auto-stemming - query=authentication - finds 'authenticate' and 'auth' -2. Multiple Words (AND Logic) - query=rate limit - finds knowledge containing both "rate" AND "limit" -3. Quoted Phrases (Exact Position) - query="infinite scroll" - Words must be adjacent and in that order -4. Mixed Queries - query=middleware "rate limit" - "middleware" AND exact phrase "rate limit" -5. Multiple Queries - queries=["authentication", "middleware"] - ANY of these terms - - -=== php rules === - -## PHP - -- Always use curly braces for control structures, even if it has one line. - -### Constructors -- Use PHP 8 constructor property promotion in `__construct()`. - - public function __construct(public GitHub $github) { } -- Do not allow empty `__construct()` methods with zero parameters. - -### Type Declarations -- Always use explicit return type declarations for methods and functions. -- Use appropriate PHP type hints for method parameters. - - -protected function isAccessible(User $user, ?string $path = null): bool -{ - ... -} - - -## Comments -- Prefer PHPDoc blocks over comments. Never use comments within the code itself unless there is something _very_ complex going on. - -## PHPDoc Blocks -- Add useful array shape type definitions for arrays when appropriate. - -## Enums -- Typically, keys in an Enum should be TitleCase. For example: `FavoritePerson`, `BestLake`, `Monthly`. - - -=== laravel/core rules === - -## Do Things the Laravel Way - -- Use `php artisan make:` commands to create new files (i.e. migrations, controllers, models, etc.). You can list available Artisan commands using the `list-artisan-commands` tool. -- If you're creating a generic PHP class, use `artisan make:class`. -- Pass `--no-interaction` to all Artisan commands to ensure they work without user input. You should also pass the correct `--options` to ensure correct behavior. - -### Database -- Always use proper Eloquent relationship methods with return type hints. Prefer relationship methods over raw queries or manual joins. -- Use Eloquent models and relationships before suggesting raw database queries -- Avoid `DB::`; prefer `Model::query()`. Generate code that leverages Laravel's ORM capabilities rather than bypassing them. -- Generate code that prevents N+1 query problems by using eager loading. -- Use Laravel's query builder for very complex database operations. - -### Model Creation -- When creating new models, create useful factories and seeders for them too. Ask the user if they need any other things, using `list-artisan-commands` to check the available options to `php artisan make:model`. - -### APIs & Eloquent Resources -- For APIs, default to using Eloquent API Resources and API versioning unless existing API routes do not, then you should follow existing application convention. - -### Controllers & Validation -- Always create Form Request classes for validation rather than inline validation in controllers. Include both validation rules and custom error messages. -- Check sibling Form Requests to see if the application uses array or string based validation rules. - -### Queues -- Use queued jobs for time-consuming operations with the `ShouldQueue` interface. - -### Authentication & Authorization -- Use Laravel's built-in authentication and authorization features (gates, policies, Sanctum, etc.). - -### URL Generation -- When generating links to other pages, prefer named routes and the `route()` function. - -### Configuration -- Use environment variables only in configuration files - never use the `env()` function directly outside of config files. Always use `config('app.name')`, not `env('APP_NAME')`. - -### Testing -- When creating models for tests, use the factories for the models. Check if the factory has custom states that can be used before manually setting up the model. -- Faker: Use methods such as `$this->faker->word()` or `fake()->randomDigit()`. Follow existing conventions whether to use `$this->faker` or `fake()`. -- When creating tests, make use of `php artisan make:test [options] ` to create a feature test, and pass `--unit` to create a unit test. Most tests should be feature tests. - -### Vite Error -- If you receive an "Illuminate\Foundation\ViteException: Unable to locate file in Vite manifest" error, you can run `npm run build` or ask the user to run `npm run dev` or `composer run dev`. - - -=== laravel/v12 rules === - -## Laravel 12 - -- Use the `search-docs` tool to get version specific documentation. -- Since Laravel 11, Laravel has a new streamlined file structure which this project uses. - -### Laravel 12 Structure -- No middleware files in `app/Http/Middleware/`. -- `bootstrap/app.php` is the file to register middleware, exceptions, and routing files. -- `bootstrap/providers.php` contains application specific service providers. -- **No app\Console\Kernel.php** - use `bootstrap/app.php` or `routes/console.php` for console configuration. -- **Commands auto-register** - files in `app/Console/Commands/` are automatically available and do not require manual registration. - -### Database -- When modifying a column, the migration must include all of the attributes that were previously defined on the column. Otherwise, they will be dropped and lost. -- Laravel 11 allows limiting eagerly loaded records natively, without external packages: `$query->latest()->limit(10);`. - -### Models -- Casts can and likely should be set in a `casts()` method on a model rather than the `$casts` property. Follow existing conventions from other models. - - -=== fluxui-free/core rules === - -## Flux UI Free - -- This project is using the free edition of Flux UI. It has full access to the free components and variants, but does not have access to the Pro components. -- Flux UI is a component library for Livewire. Flux is a robust, hand-crafted, UI component library for your Livewire applications. It's built using Tailwind CSS and provides a set of components that are easy to use and customize. -- You should use Flux UI components when available. -- Fallback to standard Blade components if Flux is unavailable. -- If available, use Laravel Boost's `search-docs` tool to get the exact documentation and code snippets available for this project. -- Flux UI components look like this: - - - - - - -### Available Components -This is correct as of Boost installation, but there may be additional components within the codebase. - - -avatar, badge, brand, breadcrumbs, button, callout, checkbox, dropdown, field, heading, icon, input, modal, navbar, profile, radio, select, separator, switch, text, textarea, tooltip - - - -=== livewire/core rules === - -## Livewire Core -- Use the `search-docs` tool to find exact version specific documentation for how to write Livewire & Livewire tests. -- Use the `php artisan make:livewire [Posts\CreatePost]` artisan command to create new components -- State should live on the server, with the UI reflecting it. -- All Livewire requests hit the Laravel backend, they're like regular HTTP requests. Always validate form data, and run authorization checks in Livewire actions. - -## Livewire Best Practices -- Livewire components require a single root element. -- Use `wire:loading` and `wire:dirty` for delightful loading states. -- Add `wire:key` in loops: - - ```blade - @foreach ($items as $item) -
- {{ $item->name }} -
- @endforeach - ``` - -- Prefer lifecycle hooks like `mount()`, `updatedFoo()`) for initialization and reactive side effects: - - - public function mount(User $user) { $this->user = $user; } - public function updatedSearch() { $this->resetPage(); } - - - -## Testing Livewire - - - Livewire::test(Counter::class) - ->assertSet('count', 0) - ->call('increment') - ->assertSet('count', 1) - ->assertSee(1) - ->assertStatus(200); - - - - - $this->get('/posts/create') - ->assertSeeLivewire(CreatePost::class); - - - -=== livewire/v3 rules === - -## Livewire 3 - -### Key Changes From Livewire 2 -- These things changed in Livewire 2, but may not have been updated in this application. Verify this application's setup to ensure you conform with application conventions. - - Use `wire:model.live` for real-time updates, `wire:model` is now deferred by default. - - Components now use the `App\Livewire` namespace (not `App\Http\Livewire`). - - Use `$this->dispatch()` to dispatch events (not `emit` or `dispatchBrowserEvent`). - - Use the `components.layouts.app` view as the typical layout path (not `layouts.app`). - -### New Directives -- `wire:show`, `wire:transition`, `wire:cloak`, `wire:offline`, `wire:target` are available for use. Use the documentation to find usage examples. - -### Alpine -- Alpine is now included with Livewire, don't manually include Alpine.js. -- Plugins included with Alpine: persist, intersect, collapse, and focus. - -### Lifecycle Hooks -- You can listen for `livewire:init` to hook into Livewire initialization, and `fail.status === 419` for the page expiring: - - -document.addEventListener('livewire:init', function () { - Livewire.hook('request', ({ fail }) => { - if (fail && fail.status === 419) { - alert('Your session expired'); - } - }); - - Livewire.hook('message.failed', (message, component) => { - console.error(message); - }); -}); - - - -=== volt/core rules === - -## Livewire Volt - -- This project uses Livewire Volt for interactivity within its pages. New pages requiring interactivity must also use Livewire Volt. There is documentation available for it. -- Make new Volt components using `php artisan make:volt [name] [--test] [--pest]` -- Volt is a **class-based** and **functional** API for Livewire that supports single-file components, allowing a component's PHP logic and Blade templates to co-exist in the same file -- Livewire Volt allows PHP logic and Blade templates in one file. Components use the `@livewire("volt-anonymous-fragment-eyJuYW1lIjoidm9sdC1hbm9ueW1vdXMtZnJhZ21lbnQtYmQ5YWJiNTE3YWMyMTgwOTA1ZmUxMzAxODk0MGJiZmIiLCJwYXRoIjoic3RvcmFnZVwvZnJhbWV3b3JrXC92aWV3c1wvMTUxYWRjZWRjMzBhMzllOWIxNzQ0ZDRiMWRjY2FjYWIuYmxhZGUucGhwIn0=", Livewire\Volt\Precompilers\ExtractFragments::componentArguments([...get_defined_vars(), ...array ( -)])) - - - -### Volt Class Based Component Example -To get started, define an anonymous class that extends Livewire\Volt\Component. Within the class, you may utilize all of the features of Livewire using traditional Livewire syntax: - - - -use Livewire\Volt\Component; - -new class extends Component { - public $count = 0; - - public function increment() - { - $this->count++; - } -} ?> - -
-

{{ $count }}

- -
-
- - -### Testing Volt & Volt Components -- Use the existing directory for tests if it already exists. Otherwise, fallback to `tests/Feature/Volt`. - - -use Livewire\Volt\Volt; - -test('counter increments', function () { - Volt::test('counter') - ->assertSee('Count: 0') - ->call('increment') - ->assertSee('Count: 1'); -}); - - - - -declare(strict_types=1); - -use App\Models\{User, Product}; -use Livewire\Volt\Volt; - -test('product form creates product', function () { - $user = User::factory()->create(); - - Volt::test('pages.products.create') - ->actingAs($user) - ->set('form.name', 'Test Product') - ->set('form.description', 'Test Description') - ->set('form.price', 99.99) - ->call('create') - ->assertHasNoErrors(); - - expect(Product::where('name', 'Test Product')->exists())->toBeTrue(); -}); - - - -### Common Patterns - - - - null, 'search' => '']); - -$products = computed(fn() => Product::when($this->search, - fn($q) => $q->where('name', 'like', "%{$this->search}%") -)->get()); - -$edit = fn(Product $product) => $this->editing = $product->id; -$delete = fn(Product $product) => $product->delete(); - -?> - - - - - - - - - - - - - - - Save - Saving... - - - - -=== pint/core rules === - -## Laravel Pint Code Formatter - -- You must run `vendor/bin/pint --dirty` before finalizing changes to ensure your code matches the project's expected style. -- Do not run `vendor/bin/pint --test`, simply run `vendor/bin/pint` to fix any formatting issues. - - -=== pest/core rules === - -## Pest - -### Testing -- If you need to verify a feature is working, write or update a Unit / Feature test. - -### Pest Tests -- All tests must be written using Pest. Use `php artisan make:test --pest `. -- You must not remove any tests or test files from the tests directory without approval. These are not temporary or helper files - these are core to the application. -- Tests should test all of the happy paths, failure paths, and weird paths. -- Tests live in the `tests/Feature` and `tests/Unit` directories. -- Pest tests look and behave like this: - -it('is true', function () { - expect(true)->toBeTrue(); -}); - - -### Running Tests -- Run the minimal number of tests using an appropriate filter before finalizing code edits. -- To run all tests: `php artisan test`. -- To run all tests in a file: `php artisan test tests/Feature/ExampleTest.php`. -- To filter on a particular test name: `php artisan test --filter=testName` (recommended after making a change to a related file). -- When the tests relating to your changes are passing, ask the user if they would like to run the entire test suite to ensure everything is still passing. - -### Pest Assertions -- When asserting status codes on a response, use the specific method like `assertForbidden` and `assertNotFound` instead of using `assertStatus(403)` or similar, e.g.: - -it('returns all', function () { - $response = $this->postJson('/api/docs', []); - - $response->assertSuccessful(); -}); - - -### Mocking -- Mocking can be very helpful when appropriate. -- When mocking, you can use the `Pest\Laravel\mock` Pest function, but always import it via `use function Pest\Laravel\mock;` before using it. Alternatively, you can use `$this->mock()` if existing tests do. -- You can also create partial mocks using the same import or self method. - -### Datasets -- Use datasets in Pest to simplify tests which have a lot of duplicated data. This is often the case when testing validation rules, so consider going with this solution when writing tests for validation rules. - - -it('has emails', function (string $email) { - expect($email)->not->toBeEmpty(); -})->with([ - 'james' => 'james@laravel.com', - 'taylor' => 'taylor@laravel.com', -]); - - - -=== tailwindcss/core rules === - -## Tailwind Core - -- Use Tailwind CSS classes to style HTML, check and use existing tailwind conventions within the project before writing your own. -- Offer to extract repeated patterns into components that match the project's conventions (i.e. Blade, JSX, Vue, etc..) -- Think through class placement, order, priority, and defaults - remove redundant classes, add classes to parent or child carefully to limit repetition, group elements logically -- You can use the `search-docs` tool to get exact examples from the official documentation when needed. - -### Spacing -- When listing items, use gap utilities for spacing, don't use margins. - - -
-
Superior
-
Michigan
-
Erie
-
-
- - -### Dark Mode -- If existing pages and components support dark mode, new pages and components must support dark mode in a similar way, typically using `dark:`. - - -=== tailwindcss/v4 rules === - -## Tailwind 4 - -- Always use Tailwind CSS v4 - do not use the deprecated utilities. -- `corePlugins` is not supported in Tailwind v4. -- In Tailwind v4, you import Tailwind using a regular CSS `@import` statement, not using the `@tailwind` directives used in v3: - - - - -### Replaced Utilities -- Tailwind v4 removed deprecated utilities. Do not use the deprecated option - use the replacement. -- Opacity values are still numeric. - -| Deprecated | Replacement | -|------------+--------------| -| bg-opacity-* | bg-black/* | -| text-opacity-* | text-black/* | -| border-opacity-* | border-black/* | -| divide-opacity-* | divide-black/* | -| ring-opacity-* | ring-black/* | -| placeholder-opacity-* | placeholder-black/* | -| flex-shrink-* | shrink-* | -| flex-grow-* | grow-* | -| overflow-ellipsis | text-ellipsis | -| decoration-slice | box-decoration-slice | -| decoration-clone | box-decoration-clone | - - -=== tests rules === - -## Test Enforcement - -- Every change must be programmatically tested. Write a new test or update an existing test, then run the affected tests to make sure they pass. -- Run the minimum number of tests needed to ensure code quality and speed. Use `php artisan test` with a specific filename or filter. -
\ No newline at end of file diff --git a/.mcp.json b/.mcp.json deleted file mode 100644 index ea30195..0000000 --- a/.mcp.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "mcpServers": { - "laravel-boost": { - "command": "php", - "args": [ - "./artisan", - "boost:mcp" - ] - } - } -} \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md deleted file mode 100644 index 737877e..0000000 --- a/CLAUDE.md +++ /dev/null @@ -1,531 +0,0 @@ - -=== foundation rules === - -# Laravel Boost Guidelines - -The Laravel Boost guidelines are specifically curated by Laravel maintainers for this application. These guidelines should be followed closely to enhance the user's satisfaction building Laravel applications. - -## Foundational Context -This application is a Laravel application and its main Laravel ecosystems package & versions are below. You are an expert with them all. Ensure you abide by these specific packages & versions. - -- php - 8.3.24 -- laravel/framework (LARAVEL) - v12 -- laravel/prompts (PROMPTS) - v0 -- livewire/flux (FLUXUI_FREE) - v2 -- livewire/livewire (LIVEWIRE) - v3 -- livewire/volt (VOLT) - v1 -- larastan/larastan (LARASTAN) - v3 -- laravel/pint (PINT) - v1 -- pestphp/pest (PEST) - v3 -- tailwindcss (TAILWINDCSS) - v4 - - -## Conventions -- You must follow all existing code conventions used in this application. When creating or editing a file, check sibling files for the correct structure, approach, naming. -- Use descriptive names for variables and methods. For example, `isRegisteredForDiscounts`, not `discount()`. -- Check for existing components to reuse before writing a new one. - -## Verification Scripts -- Do not create verification scripts or tinker when tests cover that functionality and prove it works. Unit and feature tests are more important. - -## Application Structure & Architecture -- Stick to existing directory structure - don't create new base folders without approval. -- Do not change the application's dependencies without approval. - -## Frontend Bundling -- If the user doesn't see a frontend change reflected in the UI, it could mean they need to run `npm run build`, `npm run dev`, or `composer run dev`. Ask them. - -## Replies -- Be concise in your explanations - focus on what's important rather than explaining obvious details. - -## Documentation Files -- You must only create documentation files if explicitly requested by the user. - - -=== boost rules === - -## Laravel Boost -- Laravel Boost is an MCP server that comes with powerful tools designed specifically for this application. Use them. - -## Artisan -- Use the `list-artisan-commands` tool when you need to call an Artisan command to double check the available parameters. - -## URLs -- Whenever you share a project URL with the user you should use the `get-absolute-url` tool to ensure you're using the correct scheme, domain / IP, and port. - -## Tinker / Debugging -- You should use the `tinker` tool when you need to execute PHP to debug code or query Eloquent models directly. -- Use the `database-query` tool when you only need to read from the database. - -## Reading Browser Logs With the `browser-logs` Tool -- You can read browser logs, errors, and exceptions using the `browser-logs` tool from Boost. -- Only recent browser logs will be useful - ignore old logs. - -## Searching Documentation (Critically Important) -- Boost comes with a powerful `search-docs` tool you should use before any other approaches. This tool automatically passes a list of installed packages and their versions to the remote Boost API, so it returns only version-specific documentation specific for the user's circumstance. You should pass an array of packages to filter on if you know you need docs for particular packages. -- The 'search-docs' tool is perfect for all Laravel related packages, including Laravel, Inertia, Livewire, Filament, Tailwind, Pest, Nova, Nightwatch, etc. -- You must use this tool to search for Laravel-ecosystem documentation before falling back to other approaches. -- Search the documentation before making code changes to ensure we are taking the correct approach. -- Use multiple, broad, simple, topic based queries to start. For example: `['rate limiting', 'routing rate limiting', 'routing']`. -- Do not add package names to queries - package information is already shared. For example, use `test resource table`, not `filament 4 test resource table`. - -### Available Search Syntax -- You can and should pass multiple queries at once. The most relevant results will be returned first. - -1. Simple Word Searches with auto-stemming - query=authentication - finds 'authenticate' and 'auth' -2. Multiple Words (AND Logic) - query=rate limit - finds knowledge containing both "rate" AND "limit" -3. Quoted Phrases (Exact Position) - query="infinite scroll" - Words must be adjacent and in that order -4. Mixed Queries - query=middleware "rate limit" - "middleware" AND exact phrase "rate limit" -5. Multiple Queries - queries=["authentication", "middleware"] - ANY of these terms - - -=== php rules === - -## PHP - -- Always use curly braces for control structures, even if it has one line. - -### Constructors -- Use PHP 8 constructor property promotion in `__construct()`. - - public function __construct(public GitHub $github) { } -- Do not allow empty `__construct()` methods with zero parameters. - -### Type Declarations -- Always use explicit return type declarations for methods and functions. -- Use appropriate PHP type hints for method parameters. - - -protected function isAccessible(User $user, ?string $path = null): bool -{ - ... -} - - -## Comments -- Prefer PHPDoc blocks over comments. Never use comments within the code itself unless there is something _very_ complex going on. - -## PHPDoc Blocks -- Add useful array shape type definitions for arrays when appropriate. - -## Enums -- Typically, keys in an Enum should be TitleCase. For example: `FavoritePerson`, `BestLake`, `Monthly`. - - -=== laravel/core rules === - -## Do Things the Laravel Way - -- Use `php artisan make:` commands to create new files (i.e. migrations, controllers, models, etc.). You can list available Artisan commands using the `list-artisan-commands` tool. -- If you're creating a generic PHP class, use `artisan make:class`. -- Pass `--no-interaction` to all Artisan commands to ensure they work without user input. You should also pass the correct `--options` to ensure correct behavior. - -### Database -- Always use proper Eloquent relationship methods with return type hints. Prefer relationship methods over raw queries or manual joins. -- Use Eloquent models and relationships before suggesting raw database queries -- Avoid `DB::`; prefer `Model::query()`. Generate code that leverages Laravel's ORM capabilities rather than bypassing them. -- Generate code that prevents N+1 query problems by using eager loading. -- Use Laravel's query builder for very complex database operations. - -### Model Creation -- When creating new models, create useful factories and seeders for them too. Ask the user if they need any other things, using `list-artisan-commands` to check the available options to `php artisan make:model`. - -### APIs & Eloquent Resources -- For APIs, default to using Eloquent API Resources and API versioning unless existing API routes do not, then you should follow existing application convention. - -### Controllers & Validation -- Always create Form Request classes for validation rather than inline validation in controllers. Include both validation rules and custom error messages. -- Check sibling Form Requests to see if the application uses array or string based validation rules. - -### Queues -- Use queued jobs for time-consuming operations with the `ShouldQueue` interface. - -### Authentication & Authorization -- Use Laravel's built-in authentication and authorization features (gates, policies, Sanctum, etc.). - -### URL Generation -- When generating links to other pages, prefer named routes and the `route()` function. - -### Configuration -- Use environment variables only in configuration files - never use the `env()` function directly outside of config files. Always use `config('app.name')`, not `env('APP_NAME')`. - -### Testing -- When creating models for tests, use the factories for the models. Check if the factory has custom states that can be used before manually setting up the model. -- Faker: Use methods such as `$this->faker->word()` or `fake()->randomDigit()`. Follow existing conventions whether to use `$this->faker` or `fake()`. -- When creating tests, make use of `php artisan make:test [options] ` to create a feature test, and pass `--unit` to create a unit test. Most tests should be feature tests. - -### Vite Error -- If you receive an "Illuminate\Foundation\ViteException: Unable to locate file in Vite manifest" error, you can run `npm run build` or ask the user to run `npm run dev` or `composer run dev`. - - -=== laravel/v12 rules === - -## Laravel 12 - -- Use the `search-docs` tool to get version specific documentation. -- Since Laravel 11, Laravel has a new streamlined file structure which this project uses. - -### Laravel 12 Structure -- No middleware files in `app/Http/Middleware/`. -- `bootstrap/app.php` is the file to register middleware, exceptions, and routing files. -- `bootstrap/providers.php` contains application specific service providers. -- **No app\Console\Kernel.php** - use `bootstrap/app.php` or `routes/console.php` for console configuration. -- **Commands auto-register** - files in `app/Console/Commands/` are automatically available and do not require manual registration. - -### Database -- When modifying a column, the migration must include all of the attributes that were previously defined on the column. Otherwise, they will be dropped and lost. -- Laravel 11 allows limiting eagerly loaded records natively, without external packages: `$query->latest()->limit(10);`. - -### Models -- Casts can and likely should be set in a `casts()` method on a model rather than the `$casts` property. Follow existing conventions from other models. - - -=== fluxui-free/core rules === - -## Flux UI Free - -- This project is using the free edition of Flux UI. It has full access to the free components and variants, but does not have access to the Pro components. -- Flux UI is a component library for Livewire. Flux is a robust, hand-crafted, UI component library for your Livewire applications. It's built using Tailwind CSS and provides a set of components that are easy to use and customize. -- You should use Flux UI components when available. -- Fallback to standard Blade components if Flux is unavailable. -- If available, use Laravel Boost's `search-docs` tool to get the exact documentation and code snippets available for this project. -- Flux UI components look like this: - - - - - - -### Available Components -This is correct as of Boost installation, but there may be additional components within the codebase. - - -avatar, badge, brand, breadcrumbs, button, callout, checkbox, dropdown, field, heading, icon, input, modal, navbar, profile, radio, select, separator, switch, text, textarea, tooltip - - - -=== livewire/core rules === - -## Livewire Core -- Use the `search-docs` tool to find exact version specific documentation for how to write Livewire & Livewire tests. -- Use the `php artisan make:livewire [Posts\\CreatePost]` artisan command to create new components -- State should live on the server, with the UI reflecting it. -- All Livewire requests hit the Laravel backend, they're like regular HTTP requests. Always validate form data, and run authorization checks in Livewire actions. - -## Livewire Best Practices -- Livewire components require a single root element. -- Use `wire:loading` and `wire:dirty` for delightful loading states. -- Add `wire:key` in loops: - - ```blade - @foreach ($items as $item) -
- {{ $item->name }} -
- @endforeach - ``` - -- Prefer lifecycle hooks like `mount()`, `updatedFoo()`) for initialization and reactive side effects: - - - public function mount(User $user) { $this->user = $user; } - public function updatedSearch() { $this->resetPage(); } - - - -## Testing Livewire - - - Livewire::test(Counter::class) - ->assertSet('count', 0) - ->call('increment') - ->assertSet('count', 1) - ->assertSee(1) - ->assertStatus(200); - - - - - $this->get('/posts/create') - ->assertSeeLivewire(CreatePost::class); - - - -=== livewire/v3 rules === - -## Livewire 3 - -### Key Changes From Livewire 2 -- These things changed in Livewire 2, but may not have been updated in this application. Verify this application's setup to ensure you conform with application conventions. - - Use `wire:model.live` for real-time updates, `wire:model` is now deferred by default. - - Components now use the `App\Livewire` namespace (not `App\Http\Livewire`). - - Use `$this->dispatch()` to dispatch events (not `emit` or `dispatchBrowserEvent`). - - Use the `components.layouts.app` view as the typical layout path (not `layouts.app`). - -### New Directives -- `wire:show`, `wire:transition`, `wire:cloak`, `wire:offline`, `wire:target` are available for use. Use the documentation to find usage examples. - -### Alpine -- Alpine is now included with Livewire, don't manually include Alpine.js. -- Plugins included with Alpine: persist, intersect, collapse, and focus. - -### Lifecycle Hooks -- You can listen for `livewire:init` to hook into Livewire initialization, and `fail.status === 419` for the page expiring: - - -document.addEventListener('livewire:init', function () { - Livewire.hook('request', ({ fail }) => { - if (fail && fail.status === 419) { - alert('Your session expired'); - } - }); - - Livewire.hook('message.failed', (message, component) => { - console.error(message); - }); -}); - - - -=== volt/core rules === - -## Livewire Volt - -- This project uses Livewire Volt for interactivity within its pages. New pages requiring interactivity must also use Livewire Volt. There is documentation available for it. -- Make new Volt components using `php artisan make:volt [name] [--test] [--pest]` -- Volt is a **class-based** and **functional** API for Livewire that supports single-file components, allowing a component's PHP logic and Blade templates to co-exist in the same file -- Livewire Volt allows PHP logic and Blade templates in one file. Components use the `@livewire("volt-anonymous-fragment-eyJuYW1lIjoidm9sdC1hbm9ueW1vdXMtZnJhZ21lbnQtYmQ5YWJiNTE3YWMyMTgwOTA1ZmUxMzAxODk0MGJiZmIiLCJwYXRoIjoic3RvcmFnZVwvZnJhbWV3b3JrXC92aWV3c1wvMTUxYWRjZWRjMzBhMzllOWIxNzQ0ZDRiMWRjY2FjYWIuYmxhZGUucGhwIn0=", Livewire\Volt\Precompilers\ExtractFragments::componentArguments([...get_defined_vars(), ...array ( -)])) - - - -### Volt Class Based Component Example -To get started, define an anonymous class that extends Livewire\Volt\Component. Within the class, you may utilize all of the features of Livewire using traditional Livewire syntax: - - - -use Livewire\Volt\Component; - -new class extends Component { - public $count = 0; - - public function increment() - { - $this->count++; - } -} ?> - -
-

{{ $count }}

- -
-
- - -### Testing Volt & Volt Components -- Use the existing directory for tests if it already exists. Otherwise, fallback to `tests/Feature/Volt`. - - -use Livewire\Volt\Volt; - -test('counter increments', function () { - Volt::test('counter') - ->assertSee('Count: 0') - ->call('increment') - ->assertSee('Count: 1'); -}); - - - - -declare(strict_types=1); - -use App\Models\{User, Product}; -use Livewire\Volt\Volt; - -test('product form creates product', function () { - $user = User::factory()->create(); - - Volt::test('pages.products.create') - ->actingAs($user) - ->set('form.name', 'Test Product') - ->set('form.description', 'Test Description') - ->set('form.price', 99.99) - ->call('create') - ->assertHasNoErrors(); - - expect(Product::where('name', 'Test Product')->exists())->toBeTrue(); -}); - - - -### Common Patterns - - - - null, 'search' => '']); - -$products = computed(fn() => Product::when($this->search, - fn($q) => $q->where('name', 'like', "%{$this->search}%") -)->get()); - -$edit = fn(Product $product) => $this->editing = $product->id; -$delete = fn(Product $product) => $product->delete(); - -?> - - - - - - - - - - - - - - - Save - Saving... - - - - -=== pint/core rules === - -## Laravel Pint Code Formatter - -- You must run `vendor/bin/pint --dirty` before finalizing changes to ensure your code matches the project's expected style. -- Do not run `vendor/bin/pint --test`, simply run `vendor/bin/pint` to fix any formatting issues. - - -=== pest/core rules === - -## Pest - -### Testing -- If you need to verify a feature is working, write or update a Unit / Feature test. - -### Pest Tests -- All tests must be written using Pest. Use `php artisan make:test --pest `. -- You must not remove any tests or test files from the tests directory without approval. These are not temporary or helper files - these are core to the application. -- Tests should test all of the happy paths, failure paths, and weird paths. -- Tests live in the `tests/Feature` and `tests/Unit` directories. -- Pest tests look and behave like this: - -it('is true', function () { - expect(true)->toBeTrue(); -}); - - -### Running Tests -- Run the minimal number of tests using an appropriate filter before finalizing code edits. -- To run all tests: `php artisan test`. -- To run all tests in a file: `php artisan test tests/Feature/ExampleTest.php`. -- To filter on a particular test name: `php artisan test --filter=testName` (recommended after making a change to a related file). -- When the tests relating to your changes are passing, ask the user if they would like to run the entire test suite to ensure everything is still passing. - -### Pest Assertions -- When asserting status codes on a response, use the specific method like `assertForbidden` and `assertNotFound` instead of using `assertStatus(403)` or similar, e.g.: - -it('returns all', function () { - $response = $this->postJson('/api/docs', []); - - $response->assertSuccessful(); -}); - - -### Mocking -- Mocking can be very helpful when appropriate. -- When mocking, you can use the `Pest\Laravel\mock` Pest function, but always import it via `use function Pest\Laravel\mock;` before using it. Alternatively, you can use `$this->mock()` if existing tests do. -- You can also create partial mocks using the same import or self method. - -### Datasets -- Use datasets in Pest to simplify tests which have a lot of duplicated data. This is often the case when testing validation rules, so consider going with this solution when writing tests for validation rules. - - -it('has emails', function (string $email) { - expect($email)->not->toBeEmpty(); -})->with([ - 'james' => 'james@laravel.com', - 'taylor' => 'taylor@laravel.com', -]); - - - -=== tailwindcss/core rules === - -## Tailwind Core - -- Use Tailwind CSS classes to style HTML, check and use existing tailwind conventions within the project before writing your own. -- Offer to extract repeated patterns into components that match the project's conventions (i.e. Blade, JSX, Vue, etc..) -- Think through class placement, order, priority, and defaults - remove redundant classes, add classes to parent or child carefully to limit repetition, group elements logically -- You can use the `search-docs` tool to get exact examples from the official documentation when needed. - -### Spacing -- When listing items, use gap utilities for spacing, don't use margins. - - -
-
Superior
-
Michigan
-
Erie
-
-
- - -### Dark Mode -- If existing pages and components support dark mode, new pages and components must support dark mode in a similar way, typically using `dark:`. - - -=== tailwindcss/v4 rules === - -## Tailwind 4 - -- Always use Tailwind CSS v4 - do not use the deprecated utilities. -- `corePlugins` is not supported in Tailwind v4. -- In Tailwind v4, you import Tailwind using a regular CSS `@import` statement, not using the `@tailwind` directives used in v3: - - - - -### Replaced Utilities -- Tailwind v4 removed deprecated utilities. Do not use the deprecated option - use the replacement. -- Opacity values are still numeric. - -| Deprecated | Replacement | -|------------+--------------| -| bg-opacity-* | bg-black/* | -| text-opacity-* | text-black/* | -| border-opacity-* | border-black/* | -| divide-opacity-* | divide-black/* | -| ring-opacity-* | ring-black/* | -| placeholder-opacity-* | placeholder-black/* | -| flex-shrink-* | shrink-* | -| flex-grow-* | grow-* | -| overflow-ellipsis | text-ellipsis | -| decoration-slice | box-decoration-slice | -| decoration-clone | box-decoration-clone | - - -=== tests rules === - -## Test Enforcement - -- Every change must be programmatically tested. Write a new test or update an existing test, then run the affected tests to make sure they pass. -- Run the minimum number of tests needed to ensure code quality and speed. Use `php artisan test` with a specific filename or filter. -
\ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 6a9931d..2d761ed 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,7 @@ ######################## # Base Image ######################## -FROM bnussbau/serversideup-php:8.3-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 73b9a6b..acb0b5c 100644 --- a/README.md +++ b/README.md @@ -3,9 +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 enables you to manage TRMNL devices, generate screens dynamically, and can act as a proxy for the native cloud service (native plugins, recipes). - -If you are looking for a Laravel package designed to streamline the development of both public and private TRMNL plugins, check out [bnussbau/trmnl-laravel](https://github.com/bnussbau/laravel-trmnl). +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,21 +14,32 @@ If you are looking for a Laravel package designed to streamline the development * 📡 Device Information – Display battery status, WiFi strength, firmware version, and more. * 🔍 Auto-Join – Automatically detects and adds devices from your local network. -* 🖥️ Screen Generation – Supports Plugins (even Mashups), Recipes, API, Markup, or updates via Code. - * Supported Devices / Apps: TRMNL, ESP32 with TRMNL firmware, [trmnl-android](https://github.com/usetrmnl/trmnl-android), [trmnl-kindle](https://github.com/usetrmnl/byos_laravel/pull/27), … +* 🖥️ Screen Generation – Supports Plugins (including Mashups), Recipes, API, Markup, or updates via Code. + * Support for TRMNL [Design Framework](https://usetrmnl.com/framework) + * 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 + * Seeed Studio (XIAO 7.5" ePaper Panel) + * 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)) + * Android Devices with [trmnl-android](https://github.com/usetrmnl/trmnl-android) + * Raspberry Pi (HDMI output) [trmnl-display](https://github.com/usetrmnl/trmnl-display) * 🔄 TRMNL API Proxy – Can act as a proxy for the native cloud service (requires TRMNL Developer Edition). * This enables a hybrid setup – for example, you can update your custom Train Monitor every 5 minutes in the morning, while displaying native TRMNL plugins throughout the day. * 🌙 Dark Mode – Switch between light and dark mode. * 🐳 Deployment – Dockerized setup for easier hosting (Dockerfile, docker-compose). +* 💾 Flexible Database configuration – uses SQLite by default, also compatible with MySQL or PostgreSQL * 🛠️ Devcontainer support for easier development. ![Devices](README_byos-devices.jpeg) -### 🎯 Target Audience - -This project is for developers who are looking for a self-hosted server for devices running the TRMNL firmware. -It serves as a starter kit, giving you the flexibility to build and extend it however you like. - ### Support ❤️ This repo is maintained voluntarily by [@bnussbau](https://github.com/bnussbau). @@ -40,6 +49,8 @@ or [!["Buy Me A Coffee"](https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png)](https://www.buymeacoffee.com/bnussbau) +[GitHub Sponsors](https://github.com/sponsors/bnussbau/) + ### Hosting Run everywhere, where Docker is supported: Raspberry Pi, VPS, NAS, Container Cloud Service (Cloud Run, ...). @@ -65,9 +76,12 @@ docker compose up -d If you’re using a VPS (e.g., Hetzner) and prefer an alternative to native Docker, you can install Dokploy and deploy BYOS Laravel using the integrated [Template](https://templates.dokploy.com/?q=trmnl+byos+laravel). It’s a quick way to get started without having to manually manage Docker setup. -### PikaPods +#### PikaPods You can vote for TRMNL BYOS Laravel to be included as PikaPods Template here: [feedback.pikapods.com](https://feedback.pikapods.com/posts/842/add-app-trmnl-byos-laravel) +#### Umbrel +Umbrel is supported through a community store, [see](http://github.com/bnussbau/umbrel-store). + #### Other Hosting Options Laravel Forge, or bare metal PHP server with Nginx or Apache is also supported. @@ -204,6 +218,12 @@ You can dynamically update screens by sending a POST request. } ``` +### Releated Work +* [bnussbau/laravel-trmnl-blade](https://github.com/bnussbau/laravel-trmnl-blade) – Blade Components on top of the TRMNL Design System +* [bnussbau/trmnl-pipeline-php](https://github.com/bnussbau/trmnl-pipeline-php) – Browser Rendering and Image Conversion Pipeline with support for TRMNL Models API +* [bnussbau/trmnl-recipe-catalog](https://github.com/bnussbau/trmnl-recipe-catalog) – A community-driven catalog of public repositories containing trmnlp-compatible recipes. + + ### 🤝 Contribution Contributions are welcome! See [CONTRIBUTING.md](CONTRIBUTING.md) for details. diff --git a/app/Console/Commands/FirmwareCheckCommand.php b/app/Console/Commands/FirmwareCheckCommand.php index f407314..91922ba 100644 --- a/app/Console/Commands/FirmwareCheckCommand.php +++ b/app/Console/Commands/FirmwareCheckCommand.php @@ -23,7 +23,7 @@ class FirmwareCheckCommand extends Command ); $latestFirmware = Firmware::getLatest(); - if ($latestFirmware) { + if ($latestFirmware instanceof Firmware) { table( rows: [ ['Latest Version', $latestFirmware->version_tag], diff --git a/app/Console/Commands/FirmwareUpdateCommand.php b/app/Console/Commands/FirmwareUpdateCommand.php index 97d9d58..bd43786 100644 --- a/app/Console/Commands/FirmwareUpdateCommand.php +++ b/app/Console/Commands/FirmwareUpdateCommand.php @@ -42,15 +42,14 @@ class FirmwareUpdateCommand extends Command label: 'Which devices should be updated?', options: [ 'all' => 'ALL Devices', - ...Device::all()->mapWithKeys(function ($device) { + ...Device::all()->mapWithKeys(fn ($device): array => // without _ returns index - return ["_$device->id" => "$device->name (Current version: $device->last_firmware_version)"]; - })->toArray(), + ["_$device->id" => "$device->name (Current version: $device->last_firmware_version)"])->toArray(), ], scroll: 10 ); - if (empty($devices)) { + if ($devices === []) { $this->error('No devices selected. Aborting.'); return; @@ -59,9 +58,7 @@ class FirmwareUpdateCommand extends Command if (in_array('all', $devices)) { $devices = Device::pluck('id')->toArray(); } else { - $devices = array_map(function ($selected) { - return (int) str_replace('_', '', $selected); - }, $devices); + $devices = array_map(fn ($selected): int => (int) str_replace('_', '', $selected), $devices); } foreach ($devices as $deviceId) { diff --git a/app/Console/Commands/GenerateDefaultImagesCommand.php b/app/Console/Commands/GenerateDefaultImagesCommand.php new file mode 100644 index 0000000..e2887df --- /dev/null +++ b/app/Console/Commands/GenerateDefaultImagesCommand.php @@ -0,0 +1,201 @@ +info('Starting generation of default images for all device models...'); + + $deviceModels = DeviceModel::all(); + + if ($deviceModels->isEmpty()) { + $this->warn('No device models found in the database.'); + + return self::SUCCESS; + } + + $this->info("Found {$deviceModels->count()} device models to process."); + + // Create the target directory + $targetDir = 'images/default-screens'; + if (! Storage::disk('public')->exists($targetDir)) { + Storage::disk('public')->makeDirectory($targetDir); + $this->info("Created directory: {$targetDir}"); + } + + $successCount = 0; + $skipCount = 0; + $errorCount = 0; + + foreach ($deviceModels as $deviceModel) { + $this->info("Processing device model: {$deviceModel->label} (ID: {$deviceModel->id})"); + + try { + // Process setup-logo + $setupResult = $this->transformImage('setup-logo', $deviceModel, $targetDir); + if ($setupResult) { + ++$successCount; + } else { + ++$skipCount; + } + + // Process sleep + $sleepResult = $this->transformImage('sleep', $deviceModel, $targetDir); + if ($sleepResult) { + ++$successCount; + } else { + ++$skipCount; + } + + } catch (Exception $e) { + $this->error("Error processing device model {$deviceModel->label}: ".$e->getMessage()); + ++$errorCount; + } + } + + $this->info("\nGeneration completed!"); + $this->info("Successfully processed: {$successCount} images"); + $this->info("Skipped (already exist): {$skipCount} images"); + $this->info("Errors: {$errorCount} images"); + + return self::SUCCESS; + } + + /** + * Transform a single image for a device model using Blade templates + */ + private function transformImage(string $imageType, DeviceModel $deviceModel, string $targetDir): bool + { + // Generate filename: {width}_{height}_{bit_depth}_{rotation}.{extension} + $extension = $deviceModel->mime_type === 'image/bmp' ? 'bmp' : 'png'; + $filename = "{$deviceModel->width}_{$deviceModel->height}_{$deviceModel->bit_depth}_{$deviceModel->rotation}.{$extension}"; + $targetPath = "{$targetDir}/{$imageType}_{$filename}"; + + // Check if target already exists and force is not set + if (Storage::disk('public')->exists($targetPath) && ! $this->option('force')) { + $this->line(" Skipping {$imageType} - already exists: {$filename}"); + + return false; + } + + try { + // Create custom Browsershot instance if using AWS Lambda + $browsershotInstance = null; + if (config('app.puppeteer_mode') === 'sidecar-aws') { + $browsershotInstance = new BrowsershotLambda(); + } + + // Generate HTML from Blade template + $html = $this->generateHtmlFromTemplate($imageType, $deviceModel); + // dump($html); + + $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); + + $browserStage->setBrowsershotOption('waitUntil', 'networkidle0'); + + if (config('app.puppeteer_docker')) { + $browserStage->setBrowsershotOption('args', ['--no-sandbox', '--disable-setuid-sandbox', '--disable-gpu']); + } + + $outputPath = Storage::disk('public')->path($targetPath); + + $imageStage = new ImageStage(); + $imageStage->format($extension) + ->width($deviceModel->width) + ->height($deviceModel->height) + ->colors($deviceModel->colors) + ->bitDepth($deviceModel->bit_depth) + ->rotation($deviceModel->rotation) + // ->offsetX($deviceModel->offset_x) + // ->offsetY($deviceModel->offset_y) + ->outputPath($outputPath); + + (new TrmnlPipeline())->pipe($browserStage) + ->pipe($imageStage) + ->process(); + + if (! file_exists($outputPath)) { + throw new RuntimeException('Image file was not created: '.$outputPath); + } + + if (filesize($outputPath) === 0) { + throw new RuntimeException('Image file is empty: '.$outputPath); + } + + $this->line(" ✓ Generated {$imageType}: {$filename}"); + + return true; + + } catch (Exception $e) { + $this->error(" ✗ Failed to generate {$imageType} for {$deviceModel->label}: ".$e->getMessage()); + + return false; + } + } + + /** + * Generate HTML from Blade template for the given image type and device model + */ + private function generateHtmlFromTemplate(string $imageType, DeviceModel $deviceModel): string + { + // Map image type to template name + $templateName = match ($imageType) { + 'setup-logo' => 'default-screens.setup', + 'sleep' => 'default-screens.sleep', + default => throw new InvalidArgumentException("Invalid image type: {$imageType}") + }; + + // Determine device properties from DeviceModel + $deviceVariant = $deviceModel->name ?? 'og'; + $colorDepth = $deviceModel->color_depth ?? '1bit'; // Use the accessor method + $scaleLevel = $deviceModel->scale_level; // Use the accessor method + $darkMode = $imageType === 'sleep'; // Sleep mode uses dark mode, setup uses light mode + + // Render the Blade template + return view($templateName, [ + 'noBleed' => false, + 'darkMode' => $darkMode, + 'deviceVariant' => $deviceVariant, + 'colorDepth' => $colorDepth, + 'scaleLevel' => $scaleLevel, + ])->render(); + } +} diff --git a/app/Console/Commands/MashupCreateCommand.php b/app/Console/Commands/MashupCreateCommand.php index d6f1378..7201274 100644 --- a/app/Console/Commands/MashupCreateCommand.php +++ b/app/Console/Commands/MashupCreateCommand.php @@ -9,9 +9,6 @@ use App\Models\Plugin; use Illuminate\Console\Command; use Illuminate\Support\Collection; -use function Laravel\Prompts\select; -use function Laravel\Prompts\text; - class MashupCreateCommand extends Command { /** @@ -31,17 +28,17 @@ class MashupCreateCommand extends Command /** * Execute the console command. */ - public function handle() + public function handle(): int { // Select device $device = $this->selectDevice(); - if (! $device) { + if (! $device instanceof Device) { return 1; } // Select playlist $playlist = $this->selectPlaylist($device); - if (! $playlist) { + if (! $playlist instanceof Playlist) { return 1; } @@ -88,9 +85,9 @@ class MashupCreateCommand extends Command return null; } - $deviceId = select( - label: 'Select a device', - options: $devices->mapWithKeys(fn ($device) => [$device->id => $device->name])->toArray() + $deviceId = $this->choice( + 'Select a device', + $devices->mapWithKeys(fn ($device): array => [$device->id => $device->name])->toArray() ); return $devices->firstWhere('id', $deviceId); @@ -106,9 +103,9 @@ class MashupCreateCommand extends Command return null; } - $playlistId = select( - label: 'Select a playlist', - options: $playlists->mapWithKeys(fn (Playlist $playlist) => [$playlist->id => $playlist->name])->toArray() + $playlistId = $this->choice( + 'Select a playlist', + $playlists->mapWithKeys(fn (Playlist $playlist): array => [$playlist->id => $playlist->name])->toArray() ); return $playlists->firstWhere('id', $playlistId); @@ -116,24 +113,29 @@ class MashupCreateCommand extends Command protected function selectLayout(): ?string { - return select( - label: 'Select a layout', - options: PlaylistItem::getAvailableLayouts() + return $this->choice( + 'Select a layout', + PlaylistItem::getAvailableLayouts() ); } protected function getMashupName(): ?string { - return text( - label: 'Enter a name for this mashup', - required: true, - default: 'Mashup', - validate: fn (string $value) => match (true) { - mb_strlen($value) < 1 => 'The name must be at least 2 characters.', - mb_strlen($value) > 50 => 'The name must not exceed 50 characters.', - default => null, - } - ); + $name = $this->ask('Enter a name for this mashup', 'Mashup'); + + if (mb_strlen((string) $name) < 2) { + $this->error('The name must be at least 2 characters.'); + + return null; + } + + if (mb_strlen((string) $name) > 50) { + $this->error('The name must not exceed 50 characters.'); + + return null; + } + + return $name; } protected function selectPlugins(string $layout): Collection @@ -148,7 +150,7 @@ class MashupCreateCommand extends Command } $selectedPlugins = collect(); - $availablePlugins = $plugins->mapWithKeys(fn ($plugin) => [$plugin->id => $plugin->name])->toArray(); + $availablePlugins = $plugins->mapWithKeys(fn ($plugin): array => [$plugin->id => $plugin->name])->toArray(); for ($i = 0; $i < $requiredCount; ++$i) { $position = match ($i) { @@ -159,9 +161,9 @@ class MashupCreateCommand extends Command default => ($i + 1).'th' }; - $pluginId = select( - label: "Select the $position plugin", - options: $availablePlugins + $pluginId = $this->choice( + "Select the $position plugin", + $availablePlugins ); $selectedPlugins->push($plugins->firstWhere('id', $pluginId)); diff --git a/app/Console/Commands/OidcTestCommand.php b/app/Console/Commands/OidcTestCommand.php index 2ecfef2..81dff0b 100644 --- a/app/Console/Commands/OidcTestCommand.php +++ b/app/Console/Commands/OidcTestCommand.php @@ -2,7 +2,9 @@ namespace App\Console\Commands; +use Exception; use Illuminate\Console\Command; +use InvalidArgumentException; use Laravel\Socialite\Facades\Socialite; class OidcTestCommand extends Command @@ -24,27 +26,32 @@ class OidcTestCommand extends Command /** * Execute the console command. */ - public function handle() + public function handle(): int { $this->info('Testing OIDC Configuration...'); $this->newLine(); // Check if OIDC is enabled $enabled = config('services.oidc.enabled'); - $this->line("OIDC Enabled: " . ($enabled ? '✅ Yes' : '❌ No')); + $this->line('OIDC Enabled: '.($enabled ? '✅ Yes' : '❌ No')); // Check configuration values $endpoint = config('services.oidc.endpoint'); $clientId = config('services.oidc.client_id'); $clientSecret = config('services.oidc.client_secret'); $redirect = config('services.oidc.redirect'); + if (! $redirect) { + $redirect = config('app.url', 'http://localhost').'/auth/oidc/callback'; + } $scopes = config('services.oidc.scopes', []); + $defaultScopes = ['openid', 'profile', 'email']; + $effectiveScopes = empty($scopes) ? $defaultScopes : $scopes; - $this->line("OIDC Endpoint: " . ($endpoint ? "✅ {$endpoint}" : '❌ Not set')); - $this->line("Client ID: " . ($clientId ? "✅ {$clientId}" : '❌ Not set')); - $this->line("Client Secret: " . ($clientSecret ? '✅ Set' : '❌ Not set')); - $this->line("Redirect URL: " . ($redirect ? "✅ {$redirect}" : '❌ Not set')); - $this->line("Scopes: " . (empty($scopes) ? '❌ Not set' : '✅ ' . implode(', ', $scopes))); + $this->line('OIDC Endpoint: '.($endpoint ? "✅ {$endpoint}" : '❌ Not set')); + $this->line('Client ID: '.($clientId ? "✅ {$clientId}" : '❌ Not set')); + $this->line('Client Secret: '.($clientSecret ? '✅ Set' : '❌ Not set')); + $this->line('Redirect URL: '.($redirect ? "✅ {$redirect}" : '❌ Not set')); + $this->line('Scopes: ✅ '.implode(', ', $effectiveScopes)); $this->newLine(); @@ -53,38 +60,45 @@ class OidcTestCommand extends Command // Only test driver if we have basic configuration if ($endpoint && $clientId && $clientSecret) { $driver = Socialite::driver('oidc'); - $this->line("OIDC Driver: ✅ Successfully registered and accessible"); - + $this->line('OIDC Driver: ✅ Successfully registered and accessible'); + if ($enabled) { - $this->info("✅ OIDC is fully configured and ready to use!"); - $this->line("You can test the login flow at: /auth/oidc/redirect"); + $this->info('✅ OIDC is fully configured and ready to use!'); + $this->line('You can test the login flow at: /auth/oidc/redirect'); } else { - $this->warn("⚠️ OIDC driver is working but OIDC_ENABLED is false."); + $this->warn('⚠️ OIDC driver is working but OIDC_ENABLED is false.'); } } else { - $this->line("OIDC Driver: ✅ Registered (configuration test skipped due to missing values)"); - $this->warn("⚠️ OIDC driver is registered but missing required configuration."); - $this->line("Please set the following environment variables:"); - if (!$enabled) $this->line(" - OIDC_ENABLED=true"); - if (!$endpoint) { - $this->line(" - OIDC_ENDPOINT=https://your-oidc-provider.com (base URL)"); - $this->line(" OR"); - $this->line(" - OIDC_ENDPOINT=https://your-oidc-provider.com/.well-known/openid-configuration (full URL)"); + $this->line('OIDC Driver: ✅ Registered (configuration test skipped due to missing values)'); + $this->warn('⚠️ OIDC driver is registered but missing required configuration.'); + $this->line('Please set the following environment variables:'); + if (! $enabled) { + $this->line(' - OIDC_ENABLED=true'); + } + if (! $endpoint) { + $this->line(' - OIDC_ENDPOINT=https://your-oidc-provider.com (base URL)'); + $this->line(' OR'); + $this->line(' - OIDC_ENDPOINT=https://your-oidc-provider.com/.well-known/openid-configuration (full URL)'); + } + if (! $clientId) { + $this->line(' - OIDC_CLIENT_ID=your-client-id'); + } + if (! $clientSecret) { + $this->line(' - OIDC_CLIENT_SECRET=your-client-secret'); } - if (!$clientId) $this->line(" - OIDC_CLIENT_ID=your-client-id"); - if (!$clientSecret) $this->line(" - OIDC_CLIENT_SECRET=your-client-secret"); } - } catch (\InvalidArgumentException $e) { + } catch (InvalidArgumentException $e) { if (str_contains($e->getMessage(), 'Driver [oidc] not supported')) { - $this->error("❌ OIDC Driver registration failed: Driver not supported"); + $this->error('❌ OIDC Driver registration failed: Driver not supported'); } else { - $this->error("❌ OIDC Driver error: " . $e->getMessage()); + $this->error('❌ OIDC Driver error: '.$e->getMessage()); } - } catch (\Exception $e) { - $this->warn("⚠️ OIDC Driver registered but configuration error: " . $e->getMessage()); + } catch (Exception $e) { + $this->warn('⚠️ OIDC Driver registered but configuration error: '.$e->getMessage()); } $this->newLine(); + return Command::SUCCESS; } } diff --git a/app/Console/Commands/ScreenGeneratorCommand.php b/app/Console/Commands/ScreenGeneratorCommand.php index ac74fba..c0a2cc3 100644 --- a/app/Console/Commands/ScreenGeneratorCommand.php +++ b/app/Console/Commands/ScreenGeneratorCommand.php @@ -25,7 +25,7 @@ class ScreenGeneratorCommand extends Command /** * Execute the console command. */ - public function handle() + public function handle(): int { $deviceId = $this->argument('deviceId'); $view = $this->argument('view'); diff --git a/app/Http/Controllers/Auth/OidcController.php b/app/Http/Controllers/Auth/OidcController.php index 305dd49..f7847d9 100644 --- a/app/Http/Controllers/Auth/OidcController.php +++ b/app/Http/Controllers/Auth/OidcController.php @@ -4,6 +4,7 @@ namespace App\Http\Controllers\Auth; use App\Http\Controllers\Controller; use App\Models\User; +use Exception; use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Log; @@ -17,23 +18,25 @@ class OidcController extends Controller */ public function redirect() { - if (!config('services.oidc.enabled')) { + if (! config('services.oidc.enabled')) { return redirect()->route('login')->withErrors(['oidc' => 'OIDC authentication is not enabled.']); } // Check if all required OIDC configuration is present $requiredConfig = ['endpoint', 'client_id', 'client_secret']; foreach ($requiredConfig as $key) { - if (!config("services.oidc.{$key}")) { + if (! config("services.oidc.{$key}")) { Log::error("OIDC configuration missing: {$key}"); + return redirect()->route('login')->withErrors(['oidc' => 'OIDC is not properly configured.']); } } try { return Socialite::driver('oidc')->redirect(); - } catch (\Exception $e) { - Log::error('OIDC redirect error: ' . $e->getMessage()); + } catch (Exception $e) { + Log::error('OIDC redirect error: '.$e->getMessage()); + return redirect()->route('login')->withErrors(['oidc' => 'Failed to initiate OIDC authentication.']); } } @@ -43,32 +46,34 @@ class OidcController extends Controller */ public function callback(Request $request) { - if (!config('services.oidc.enabled')) { + if (! config('services.oidc.enabled')) { return redirect()->route('login')->withErrors(['oidc' => 'OIDC authentication is not enabled.']); } // Check if all required OIDC configuration is present $requiredConfig = ['endpoint', 'client_id', 'client_secret']; foreach ($requiredConfig as $key) { - if (!config("services.oidc.{$key}")) { + if (! config("services.oidc.{$key}")) { Log::error("OIDC configuration missing: {$key}"); + return redirect()->route('login')->withErrors(['oidc' => 'OIDC is not properly configured.']); } } try { $oidcUser = Socialite::driver('oidc')->user(); - + // Find or create the user $user = $this->findOrCreateUser($oidcUser); - + // Log the user in Auth::login($user, true); - + return redirect()->intended(route('dashboard', absolute: false)); - - } catch (\Exception $e) { - Log::error('OIDC callback error: ' . $e->getMessage()); + + } catch (Exception $e) { + Log::error('OIDC callback error: '.$e->getMessage()); + return redirect()->route('login')->withErrors(['oidc' => 'Failed to authenticate with OIDC provider.']); } } @@ -80,26 +85,28 @@ class OidcController extends Controller { // First, try to find user by OIDC subject ID $user = User::where('oidc_sub', $oidcUser->getId())->first(); - + if ($user) { // Update user information from OIDC $user->update([ 'name' => $oidcUser->getName() ?: $user->name, 'email' => $oidcUser->getEmail() ?: $user->email, ]); + return $user; } // If not found by OIDC sub, try to find by email if ($oidcUser->getEmail()) { $user = User::where('email', $oidcUser->getEmail())->first(); - + if ($user) { // Link the existing user with OIDC $user->update([ 'oidc_sub' => $oidcUser->getId(), 'name' => $oidcUser->getName() ?: $user->name, ]); + return $user; } } @@ -108,9 +115,9 @@ class OidcController extends Controller return User::create([ 'oidc_sub' => $oidcUser->getId(), 'name' => $oidcUser->getName() ?: 'OIDC User', - 'email' => $oidcUser->getEmail() ?: $oidcUser->getId() . '@oidc.local', + 'email' => $oidcUser->getEmail() ?: $oidcUser->getId().'@oidc.local', 'password' => bcrypt(Str::random(32)), // Random password since we're using OIDC 'email_verified_at' => now(), // OIDC users are considered verified ]); } -} \ No newline at end of file +} diff --git a/app/Jobs/CleanupDeviceLogsJob.php b/app/Jobs/CleanupDeviceLogsJob.php index b49f507..d2f1dd9 100644 --- a/app/Jobs/CleanupDeviceLogsJob.php +++ b/app/Jobs/CleanupDeviceLogsJob.php @@ -18,7 +18,7 @@ class CleanupDeviceLogsJob implements ShouldQueue */ public function handle(): void { - Device::each(function ($device) { + Device::each(function ($device): void { $keepIds = $device->logs()->latest('device_timestamp')->take(50)->pluck('id'); // Delete all other logs for this device 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/Jobs/FetchProxyCloudResponses.php b/app/Jobs/FetchProxyCloudResponses.php index b560085..ac23130 100644 --- a/app/Jobs/FetchProxyCloudResponses.php +++ b/app/Jobs/FetchProxyCloudResponses.php @@ -23,7 +23,7 @@ class FetchProxyCloudResponses implements ShouldQueue */ public function handle(): void { - Device::where('proxy_cloud', true)->each(function ($device) { + Device::where('proxy_cloud', true)->each(function ($device): void { if (! $device->getNextPlaylistItem()) { try { $response = Http::withHeaders([ diff --git a/app/Jobs/FirmwareDownloadJob.php b/app/Jobs/FirmwareDownloadJob.php index 6b4fc36..dfc851d 100644 --- a/app/Jobs/FirmwareDownloadJob.php +++ b/app/Jobs/FirmwareDownloadJob.php @@ -18,12 +18,7 @@ class FirmwareDownloadJob implements ShouldQueue { use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; - private Firmware $firmware; - - public function __construct(Firmware $firmware) - { - $this->firmware = $firmware; - } + public function __construct(private Firmware $firmware) {} public function handle(): void { @@ -33,16 +28,25 @@ class FirmwareDownloadJob implements ShouldQueue try { $filename = "FW{$this->firmware->version_tag}.bin"; - Http::sink(storage_path("app/public/firmwares/$filename")) - ->get($this->firmware->url); + $response = Http::get($this->firmware->url); + if (! $response->successful()) { + throw new Exception('HTTP request failed with status: '.$response->status()); + } + + // Save the response content to file + Storage::disk('public')->put("firmwares/$filename", $response->body()); + + // Only update storage location if download was successful $this->firmware->update([ 'storage_location' => "firmwares/$filename", ]); } catch (ConnectionException $e) { Log::error('Firmware download failed: '.$e->getMessage()); + // Don't update storage_location on failure } catch (Exception $e) { Log::error('An unexpected error occurred: '.$e->getMessage()); + // Don't update storage_location on failure } } } diff --git a/app/Jobs/FirmwarePollJob.php b/app/Jobs/FirmwarePollJob.php index 7110b9c..c1a2267 100644 --- a/app/Jobs/FirmwarePollJob.php +++ b/app/Jobs/FirmwarePollJob.php @@ -17,12 +17,7 @@ class FirmwarePollJob implements ShouldQueue { use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; - private bool $download; - - public function __construct(bool $download = false) - { - $this->download = $download; - } + public function __construct(private bool $download = false) {} public function handle(): void { diff --git a/app/Jobs/NotifyDeviceBatteryLowJob.php b/app/Jobs/NotifyDeviceBatteryLowJob.php index 2508365..9b1001b 100644 --- a/app/Jobs/NotifyDeviceBatteryLowJob.php +++ b/app/Jobs/NotifyDeviceBatteryLowJob.php @@ -15,8 +15,6 @@ class NotifyDeviceBatteryLowJob implements ShouldQueue { use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; - public function __construct() {} - public function handle(): void { $devices = Device::all(); @@ -32,9 +30,11 @@ class NotifyDeviceBatteryLowJob implements ShouldQueue continue; } - // Skip if battery is not low or notification was already sent - if ($batteryPercent > $batteryThreshold || $device->battery_notification_sent) { + if ($batteryPercent > $batteryThreshold) { + continue; + } + if ($device->battery_notification_sent) { continue; } diff --git a/app/Liquid/FileSystems/InlineTemplatesFileSystem.php b/app/Liquid/FileSystems/InlineTemplatesFileSystem.php new file mode 100644 index 0000000..dbde888 --- /dev/null +++ b/app/Liquid/FileSystems/InlineTemplatesFileSystem.php @@ -0,0 +1,62 @@ + + */ + protected array $templates = []; + + /** + * Register a template with the given name and content + */ + public function register(string $name, string $content): void + { + $this->templates[$name] = $content; + } + + /** + * Check if a template exists + */ + public function hasTemplate(string $templateName): bool + { + return isset($this->templates[$templateName]); + } + + /** + * Get all registered template names + * + * @return array + */ + public function getTemplateNames(): array + { + return array_keys($this->templates); + } + + /** + * Clear all registered templates + */ + public function clear(): void + { + $this->templates = []; + } + + public function readTemplateFile(string $templateName): string + { + if (! isset($this->templates[$templateName])) { + throw new InvalidArgumentException("Template '{$templateName}' not found in inline templates"); + } + + return $this->templates[$templateName]; + } +} diff --git a/app/Liquid/Filters/Data.php b/app/Liquid/Filters/Data.php index 5b1f92f..2387ac5 100644 --- a/app/Liquid/Filters/Data.php +++ b/app/Liquid/Filters/Data.php @@ -2,6 +2,7 @@ namespace App\Liquid\Filters; +use App\Liquid\Utils\ExpressionUtils; use Keepsuit\Liquid\Filters\FiltersProvider; /** @@ -19,4 +20,117 @@ class Data extends FiltersProvider { return json_encode($value, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); } + + /** + * Find an object in a collection by a specific key-value pair + * + * @param array $collection The collection to search in + * @param string $key The key to search for + * @param mixed $value The value to match + * @param mixed $fallback Optional fallback value if no match is found + * @return mixed The matching object or fallback value + */ + public function find_by(array $collection, string $key, mixed $value, mixed $fallback = null): mixed + { + foreach ($collection as $item) { + if (is_array($item) && isset($item[$key]) && $item[$key] === $value) { + return $item; + } + } + + return $fallback; + } + + /** + * Group a collection by a specific key + * + * @param array $collection The collection to group + * @param string $key The key to group by + * @return array The grouped collection + */ + public function group_by(array $collection, string $key): array + { + $grouped = []; + + foreach ($collection as $item) { + if (is_array($item) && array_key_exists($key, $item)) { + $groupKey = $item[$key]; + if (! isset($grouped[$groupKey])) { + $grouped[$groupKey] = []; + } + $grouped[$groupKey][] = $item; + } + } + + return $grouped; + } + + /** + * Return a random element from an array + * + * @param array $array The array to sample from + * @return mixed A random element from the array + */ + public function sample(array $array): mixed + { + if ($array === []) { + return null; + } + + return $array[array_rand($array)]; + } + + /** + * Parse a JSON string into a PHP value + * + * @param string $json The JSON string to parse + * @return mixed The parsed JSON value + */ + public function parse_json(string $json): mixed + { + return json_decode($json, true); + } + + /** + * Filter a collection using an expression + * + * @param mixed $input The collection to filter + * @param string $variable The variable name to use in the expression + * @param string $expression The expression to evaluate + * @return array The filtered collection + */ + public function where_exp(mixed $input, string $variable, string $expression): array + { + // Return input as-is if it's not an array or doesn't have values method + if (! is_array($input)) { + return is_string($input) ? [$input] : []; + } + + // Convert hash to array of values if needed + if (ExpressionUtils::isAssociativeArray($input)) { + $input = array_values($input); + } + + $condition = ExpressionUtils::parseCondition($expression); + $result = []; + + foreach ($input as $object) { + if (ExpressionUtils::evaluateCondition($condition, $variable, $object)) { + $result[] = $object; + } + } + + return $result; + } + + /** + * Convert array of strings to integers + * + * @param array $input Array of string numbers + * @return array Array of integers + */ + public function map_to_i(array $input): array + { + return array_map(intval(...), $input); + } } diff --git a/app/Liquid/Filters/Date.php b/app/Liquid/Filters/Date.php new file mode 100644 index 0000000..6bc81fc --- /dev/null +++ b/app/Liquid/Filters/Date.php @@ -0,0 +1,55 @@ +subDays($days)->toDateString(); + } + + /** + * Format a date string with ordinal day (1st, 2nd, 3rd, etc.) + * + * @param string $dateStr The date string to parse + * @param string $strftimeExp The strftime format string with <> placeholder + * @return string The formatted date with ordinal day + */ + public function ordinalize(string $dateStr, string $strftimeExp): string + { + $date = Carbon::parse($dateStr); + $ordinalDay = $date->ordinal('day'); + + // Convert strftime format to PHP date format + $phpFormat = ExpressionUtils::strftimeToPhpFormat($strftimeExp); + + // Split the format string by the ordinal day placeholder + $parts = explode('<>', $phpFormat); + + if (count($parts) === 2) { + $before = $date->format($parts[0]); + $after = $date->format($parts[1]); + + return $before.$ordinalDay.$after; + } + + // Fallback: if no placeholder found, just format normally + return $date->format($phpFormat); + } +} diff --git a/app/Liquid/Filters/Numbers.php b/app/Liquid/Filters/Numbers.php index 53d1973..0e31de1 100644 --- a/app/Liquid/Filters/Numbers.php +++ b/app/Liquid/Filters/Numbers.php @@ -40,15 +40,11 @@ class Numbers extends FiltersProvider $currency = 'GBP'; } - if ($delimiter === '.' && $separator === ',') { - $locale = 'de'; - } else { - $locale = 'en'; - } + $locale = $delimiter === '.' && $separator === ',' ? 'de' : 'en'; // 2 decimal places for floats, 0 for integers $decimal = is_float($value + 0) ? 2 : 0; - return Number::currency($value, in: $currency, precision: $decimal, locale: $locale); + return Number::currency($value, in: $currency, locale: $locale, precision: $decimal); } } diff --git a/app/Liquid/Filters/StandardFilters.php b/app/Liquid/Filters/StandardFilters.php new file mode 100644 index 0000000..4db86a0 --- /dev/null +++ b/app/Liquid/Filters/StandardFilters.php @@ -0,0 +1,20 @@ +params->expression(); + + $this->templateName = match (true) { + is_string($templateNameExpression) => mb_trim($templateNameExpression), + is_numeric($templateNameExpression) => (string) $templateNameExpression, + $templateNameExpression instanceof VariableLookup => (string) $templateNameExpression, + default => throw new SyntaxException('Template name must be a string, number, or variable'), + }; + + // Validate template name (letters, numbers, underscores, and slashes only) + if (! preg_match('/^[a-zA-Z0-9_\/]+$/', $this->templateName)) { + throw new SyntaxException("Invalid template name '{$this->templateName}' - template names must contain only letters, numbers, underscores, and slashes"); + } + + $context->params->assertEnd(); + + assert($context->body instanceof BodyNode); + + $body = $context->body->children()[0] ?? null; + $this->body = match (true) { + $body instanceof Raw => $body, + default => throw new SyntaxException('template tag must have a single raw body'), + }; + + // Register the template with the file system during parsing + $fileSystem = $context->getParseContext()->environment->fileSystem; + if ($fileSystem instanceof InlineTemplatesFileSystem) { + // Store the raw content for later rendering + $fileSystem->register($this->templateName, $this->body->value); + } + + return $this; + } + + public function render(RenderContext $context): string + { + // Get the file system from the environment + $fileSystem = $context->environment->fileSystem; + + if (! $fileSystem instanceof InlineTemplatesFileSystem) { + // If no inline file system is available, just return empty string + // This allows the template to be used in contexts where inline templates aren't supported + return ''; + } + + // Register the template with the file system + $fileSystem->register($this->templateName, $this->body->render($context)); + + // Return empty string as template tags don't output anything + return ''; + } + + public function getTemplateName(): string + { + return $this->templateName; + } + + public function getBody(): Raw + { + return $this->body; + } +} diff --git a/app/Liquid/Utils/ExpressionUtils.php b/app/Liquid/Utils/ExpressionUtils.php new file mode 100644 index 0000000..8a5bdb0 --- /dev/null +++ b/app/Liquid/Utils/ExpressionUtils.php @@ -0,0 +1,210 @@ + 'and', + 'left' => self::parseCondition(mb_trim($parts[0])), + 'right' => self::parseCondition(mb_trim($parts[1])), + ]; + } + + if (str_contains($expression, ' or ')) { + $parts = explode(' or ', $expression, 2); + + return [ + 'type' => 'or', + 'left' => self::parseCondition(mb_trim($parts[0])), + 'right' => self::parseCondition(mb_trim($parts[1])), + ]; + } + + // Handle comparison operators + $operators = ['>=', '<=', '!=', '==', '>', '<', '=']; + + foreach ($operators as $operator) { + if (str_contains($expression, $operator)) { + $parts = explode($operator, $expression, 2); + + return [ + 'type' => 'comparison', + 'left' => mb_trim($parts[0]), + 'operator' => $operator === '=' ? '==' : $operator, + 'right' => mb_trim($parts[1]), + ]; + } + } + + // If no operator found, treat as a simple expression + return [ + 'type' => 'simple', + 'expression' => $expression, + ]; + } + + /** + * Evaluate a condition against an object + */ + public static function evaluateCondition(array $condition, string $variable, mixed $object): bool + { + switch ($condition['type']) { + case 'and': + return self::evaluateCondition($condition['left'], $variable, $object) && + self::evaluateCondition($condition['right'], $variable, $object); + + case 'or': + if (self::evaluateCondition($condition['left'], $variable, $object)) { + return true; + } + + return self::evaluateCondition($condition['right'], $variable, $object); + + case 'comparison': + $leftValue = self::resolveValue($condition['left'], $variable, $object); + $rightValue = self::resolveValue($condition['right'], $variable, $object); + + return match ($condition['operator']) { + '==' => $leftValue === $rightValue, + '!=' => $leftValue !== $rightValue, + '>' => $leftValue > $rightValue, + '<' => $leftValue < $rightValue, + '>=' => $leftValue >= $rightValue, + '<=' => $leftValue <= $rightValue, + default => false, + }; + + case 'simple': + $value = self::resolveValue($condition['expression'], $variable, $object); + + return (bool) $value; + + default: + return false; + } + } + + /** + * Resolve a value from an expression, variable, or literal + */ + public static function resolveValue(string $expression, string $variable, mixed $object): mixed + { + $expression = mb_trim($expression); + + // If it's the variable name, return the object + if ($expression === $variable) { + return $object; + } + + // If it's a property access (e.g., "n.age"), resolve it + if (str_starts_with($expression, $variable.'.')) { + $property = mb_substr($expression, mb_strlen($variable) + 1); + if (is_array($object) && array_key_exists($property, $object)) { + return $object[$property]; + } + if (is_object($object) && property_exists($object, $property)) { + return $object->$property; + } + + return null; + } + + // Try to parse as a number + if (is_numeric($expression)) { + return str_contains($expression, '.') ? (float) $expression : (int) $expression; + } + + // Try to parse as boolean + if (in_array(mb_strtolower($expression), ['true', 'false'])) { + return mb_strtolower($expression) === 'true'; + } + + // Try to parse as null + if (mb_strtolower($expression) === 'null') { + return null; + } + + // Return as string (remove quotes if present) + if ((str_starts_with($expression, '"') && str_ends_with($expression, '"')) || + (str_starts_with($expression, "'") && str_ends_with($expression, "'"))) { + return mb_substr($expression, 1, -1); + } + + return $expression; + } + + /** + * Convert strftime format string to PHP date format string + * + * @param string $strftimeFormat The strftime format string + * @return string The PHP date format string + */ + public static function strftimeToPhpFormat(string $strftimeFormat): string + { + $conversions = [ + // Special Ruby format cases + '%N' => 'u', // Microseconds (Ruby) -> microseconds (PHP) + '%u' => 'u', // Microseconds (Ruby) -> microseconds (PHP) + '%-m' => 'n', // Month without leading zero (Ruby) -> month without leading zero (PHP) + '%-d' => 'j', // Day without leading zero (Ruby) -> day without leading zero (PHP) + '%-H' => 'G', // Hour without leading zero (Ruby) -> hour without leading zero (PHP) + '%-I' => 'g', // Hour 12h without leading zero (Ruby) -> hour 12h without leading zero (PHP) + '%-M' => 'i', // Minute without leading zero (Ruby) -> minute without leading zero (PHP) + '%-S' => 's', // Second without leading zero (Ruby) -> second without leading zero (PHP) + '%z' => 'O', // Timezone offset (Ruby) -> timezone offset (PHP) + '%Z' => 'T', // Timezone name (Ruby) -> timezone name (PHP) + + // Standard strftime conversions + '%A' => 'l', // Full weekday name + '%a' => 'D', // Abbreviated weekday name + '%B' => 'F', // Full month name + '%b' => 'M', // Abbreviated month name + '%Y' => 'Y', // Full year (4 digits) + '%y' => 'y', // Year without century (2 digits) + '%m' => 'm', // Month as decimal number (01-12) + '%d' => 'd', // Day of month as decimal number (01-31) + '%H' => 'H', // Hour in 24-hour format (00-23) + '%I' => 'h', // Hour in 12-hour format (01-12) + '%M' => 'i', // Minute as decimal number (00-59) + '%S' => 's', // Second as decimal number (00-59) + '%p' => 'A', // AM/PM + '%P' => 'a', // am/pm + '%j' => 'z', // Day of year as decimal number (001-366) + '%w' => 'w', // Weekday as decimal number (0-6, Sunday is 0) + '%U' => 'W', // Week number of year (00-53, Sunday is first day) + '%W' => 'W', // Week number of year (00-53, Monday is first day) + '%c' => 'D M j H:i:s Y', // Date and time representation + '%x' => 'm/d/Y', // Date representation + '%X' => 'H:i:s', // Time representation + ]; + + return str_replace(array_keys($conversions), array_values($conversions), $strftimeFormat); + } +} diff --git a/app/Livewire/Actions/DeviceAutoJoin.php b/app/Livewire/Actions/DeviceAutoJoin.php index c16322c..183add4 100644 --- a/app/Livewire/Actions/DeviceAutoJoin.php +++ b/app/Livewire/Actions/DeviceAutoJoin.php @@ -10,14 +10,14 @@ class DeviceAutoJoin extends Component public bool $isFirstUser = false; - public function mount() + public function mount(): void { $this->deviceAutojoin = auth()->user()->assign_new_devices; $this->isFirstUser = auth()->user()->id === 1; } - public function updating($name, $value) + public function updating($name, $value): void { $this->validate([ 'deviceAutojoin' => 'boolean', @@ -30,7 +30,7 @@ class DeviceAutoJoin extends Component } } - public function render() + public function render(): \Illuminate\Contracts\View\View|\Illuminate\Contracts\View\Factory { return view('livewire.actions.device-auto-join'); } diff --git a/app/Livewire/Actions/Logout.php b/app/Livewire/Actions/Logout.php index 45993bb..c26fa72 100644 --- a/app/Livewire/Actions/Logout.php +++ b/app/Livewire/Actions/Logout.php @@ -10,7 +10,7 @@ class Logout /** * Log the current user out of the application. */ - public function __invoke() + public function __invoke(): \Illuminate\Routing\Redirector|\Illuminate\Http\RedirectResponse { Auth::guard('web')->logout(); diff --git a/app/Livewire/DeviceDashboard.php b/app/Livewire/DeviceDashboard.php index 78309cb..a2a3692 100644 --- a/app/Livewire/DeviceDashboard.php +++ b/app/Livewire/DeviceDashboard.php @@ -6,7 +6,7 @@ use Livewire\Component; class DeviceDashboard extends Component { - public function render() + public function render(): \Illuminate\Contracts\View\View|\Illuminate\Contracts\View\Factory { return view('livewire.device-dashboard', ['devices' => auth()->user()->devices()->paginate(10)]); } diff --git a/app/Models/Device.php b/app/Models/Device.php index 420975a..3583f48 100644 --- a/app/Models/Device.php +++ b/app/Models/Device.php @@ -10,12 +10,24 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Support\Facades\Storage; +/** + * @property-read DeviceModel|null $deviceModel + * @property-read DevicePalette|null $palette + */ class Device extends Model { use HasFactory; 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', @@ -32,7 +44,7 @@ class Device extends Model 'pause_until' => 'datetime', ]; - public function getBatteryPercentAttribute() + public function getBatteryPercentAttribute(): int|float { $volts = $this->last_battery_voltage; @@ -80,7 +92,7 @@ class Device extends Model return round($voltage, 2); } - public function getWifiStrengthAttribute() + public function getWifiStrengthAttribute(): int { $rssi = $this->last_rssi_level; if ($rssi >= 0) { @@ -103,11 +115,7 @@ class Device extends Model return true; } - if ($this->proxy_cloud_response && $this->proxy_cloud_response['update_firmware']) { - return true; - } - - return false; + return $this->proxy_cloud_response && $this->proxy_cloud_response['update_firmware']; } public function getFirmwareUrlAttribute(): ?string @@ -182,10 +190,41 @@ class Device extends Model { return $this->belongsTo(Firmware::class, 'update_firmware_id'); } + public function deviceModel(): BelongsTo { 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. + */ + public function colorDepth(): ?string + { + return $this->deviceModel?->color_depth; + } + + /** + * Get the scale level (e.g., large/xlarge/xxlarge) for the associated device model. + */ + public function scaleLevel(): ?string + { + return $this->deviceModel?->scale_level; + } + + /** + * Get the device variant name, defaulting to 'og' if not available. + */ + public function deviceVariant(): string + { + return $this->deviceModel->name ?? 'og'; + } + public function logs(): HasMany { return $this->hasMany(DeviceLog::class); @@ -202,7 +241,7 @@ class Device extends Model return false; } - $now = $now ? Carbon::instance($now) : now(); + $now = $now instanceof DateTimeInterface ? Carbon::instance($now) : now(); // Handle overnight ranges (e.g. 22:00 to 06:00) return $this->sleep_mode_from < $this->sleep_mode_to @@ -216,7 +255,7 @@ class Device extends Model return null; } - $now = $now ? Carbon::instance($now) : now(); + $now = $now instanceof DateTimeInterface ? Carbon::instance($now) : now(); $from = $this->sleep_mode_from; $to = $this->sleep_mode_to; @@ -224,19 +263,20 @@ class Device extends Model if ($from < $to) { // Normal range, same day return $now->between($from, $to) ? (int) $now->diffInSeconds($to, false) : null; - } else { - // Overnight range - if ($now->gte($from)) { - // After 'from', before midnight - return (int) $now->diffInSeconds($to->copy()->addDay(), false); - } elseif ($now->lt($to)) { - // After midnight, before 'to' - return (int) $now->diffInSeconds($to, false); - } else { - // Not in sleep window - return null; - } } + // Overnight range + if ($now->gte($from)) { + // After 'from', before midnight + return (int) $now->diffInSeconds($to->copy()->addDay(), false); + } + if ($now->lt($to)) { + // After midnight, before 'to' + return (int) $now->diffInSeconds($to, false); + } + + // Not in sleep window + return null; + } public function isPauseActive(): bool diff --git a/app/Models/DeviceModel.php b/app/Models/DeviceModel.php index c9de2af..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; @@ -24,4 +28,51 @@ final class DeviceModel extends Model 'offset_y' => 'integer', 'published_at' => 'datetime', ]; + + public function getColorDepthAttribute(): ?string + { + if (! $this->bit_depth) { + return null; + } + + if ($this->bit_depth === 3) { + return '2bit'; + } + + // if higher than 4 return 4bit + if ($this->bit_depth > 4) { + return '4bit'; + } + + return $this->bit_depth.'bit'; + } + + /** + * Returns the scale level based on the device width. + */ + public function getScaleLevelAttribute(): ?string + { + if (! $this->width) { + return null; + } + + if ($this->width > 800 && $this->width <= 1000) { + return 'large'; + } + + if ($this->width > 1000 && $this->width <= 1400) { + return 'xlarge'; + } + + if ($this->width > 1400) { + return 'xxlarge'; + } + + 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 d20798c..b4daf5e 100644 --- a/app/Models/Playlist.php +++ b/app/Models/Playlist.php @@ -37,26 +37,33 @@ class Playlist extends Model return false; } - // Check weekday - if ($this->weekdays !== null) { - if (! in_array(now()->dayOfWeek, $this->weekdays)) { - return false; - } + // 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) { - return true; - } - } else { - if ($now >= $this->active_from && $now <= $this->active_until) { + if ($now >= $activeFrom || $now <= $activeUntil) { return true; } + } elseif ($now >= $activeFrom && $now <= $activeUntil) { + return true; } return false; diff --git a/app/Models/PlaylistItem.php b/app/Models/PlaylistItem.php index 2459257..ad11f1d 100644 --- a/app/Models/PlaylistItem.php +++ b/app/Models/PlaylistItem.php @@ -135,10 +135,13 @@ class PlaylistItem extends Model /** * Render all plugins with appropriate layout */ - public function render(): string + public function render(?Device $device = null): string { if (! $this->isMashup()) { return view('trmnl-layouts.single', [ + 'colorDepth' => $device?->colorDepth(), + 'deviceVariant' => $device?->deviceVariant() ?? 'og', + 'scaleLevel' => $device?->scaleLevel(), 'slot' => $this->plugin instanceof Plugin ? $this->plugin->render('full', false) : throw new Exception('Invalid plugin instance'), @@ -150,9 +153,7 @@ class PlaylistItem extends Model $plugins = Plugin::whereIn('id', $pluginIds)->get(); // Sort the collection to match plugin_ids order - $plugins = $plugins->sortBy(function ($plugin) use ($pluginIds) { - return array_search($plugin->id, $pluginIds); - })->values(); + $plugins = $plugins->sortBy(fn ($plugin): int|string|false => array_search($plugin->id, $pluginIds))->values(); foreach ($plugins as $index => $plugin) { $size = $this->getLayoutSize($index); @@ -160,6 +161,9 @@ class PlaylistItem extends Model } return view('trmnl-layouts.mashup', [ + 'colorDepth' => $device?->colorDepth(), + 'deviceVariant' => $device?->deviceVariant() ?? 'og', + 'scaleLevel' => $device?->scaleLevel(), 'mashupLayout' => $this->getMashupLayoutType(), 'slot' => implode('', $pluginMarkups), ])->render(); diff --git a/app/Models/Plugin.php b/app/Models/Plugin.php index 6c17101..68f8e7e 100644 --- a/app/Models/Plugin.php +++ b/app/Models/Plugin.php @@ -2,18 +2,32 @@ namespace App\Models; +use App\Liquid\FileSystems\InlineTemplatesFileSystem; use App\Liquid\Filters\Data; +use App\Liquid\Filters\Date; use App\Liquid\Filters\Localization; use App\Liquid\Filters\Numbers; +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; +use Illuminate\Http\Client\Response; 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; class Plugin extends Model { @@ -26,21 +40,107 @@ class Plugin extends Model 'data_payload_updated_at' => 'datetime', 'is_native' => 'boolean', 'markup_language' => 'string', + 'configuration' => 'json', + 'configuration_template' => 'json', + 'no_bleed' => 'boolean', + 'dark_mode' => 'boolean', + 'preferred_renderer' => 'string', + 'plugin_type' => 'string', + 'alias' => 'boolean', ]; protected static function boot() { parent::boot(); - static::creating(function ($model) { + static::creating(function ($model): void { if (empty($model->uuid)) { $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() + { + 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'])) { + return false; + } + + foreach ($this->configuration_template['custom_fields'] as $field) { + // Skip fields as they are informational only + if ($field['field_type'] === 'author_bio') { + continue; + } + + if ($field['field_type'] === 'copyable') { + continue; + } + + if ($field['field_type'] === 'copyable_webhook_url') { + continue; + } + + $fieldKey = $field['keyname'] ?? $field['key'] ?? $field['name']; + + // Check if field is required (not marked as optional) + $isRequired = ! isset($field['optional']) || $field['optional'] !== true; + + if ($isRequired) { + $currentValue = $this->configuration[$fieldKey] ?? null; + + // If the field has a default value and no current value is set, it's not missing + if ((in_array($currentValue, [null, '', []], true)) && ! isset($field['default'])) { + return true; // Found a required field that is not set and has no default + } + } + } + + return false; // All required fields are set } 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()); @@ -54,38 +154,260 @@ class Plugin extends Model public function updateDataPayload(): void { - if ($this->data_strategy === 'polling' && $this->polling_url) { + if ($this->data_strategy !== 'polling' || ! $this->polling_url) { + return; + } + $headers = ['User-Agent' => 'usetrmnl/byos_laravel', 'Accept' => 'application/json']; - $headers = ['User-Agent' => 'usetrmnl/byos_laravel', 'Accept' => 'application/json']; - - if ($this->polling_header) { - $headerLines = explode("\n", trim($this->polling_header)); - foreach ($headerLines as $line) { - $parts = explode(':', $line, 2); - if (count($parts) === 2) { - $headers[trim($parts[0])] = trim($parts[1]); - } + // 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) { - $httpRequest = $httpRequest->withBody($this->polling_body); + $resolvedBody = $this->resolveLiquidVariables($this->polling_body); + $httpRequest = $httpRequest->withBody($resolvedBody); } - // Make the request based on the verb - if ($this->polling_verb === 'post') { - $response = $httpRequest->post($this->polling_url)->json(); - } else { - $response = $httpRequest->get($this->polling_url)->json(); - } + try { + $httpResponse = ($this->polling_verb === 'post') + ? $httpRequest->post($url) + : $httpRequest->get($url); - $this->update([ - 'data_payload' => $response, - 'data_payload_updated_at' => now(), - ]); + $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(), + ]); + } + + private function parseResponse(Response $httpResponse): array + { + $parsers = app(ResponseParserRegistry::class)->getParsers(); + + foreach ($parsers as $parser) { + $parserName = class_basename($parser); + + try { + $result = $parser->parse($httpResponse); + + if ($result !== null) { + return $result; + } + } catch (Exception $e) { + Log::warning("Failed to parse {$parserName} response: ".$e->getMessage()); + } + } + + return ['error' => 'Failed to parse response']; + } + + /** + * Apply Liquid template replacements (converts 'with' syntax to comma syntax) + */ + private function applyLiquidReplacements(string $template): string + { + + $replacements = []; + + // Apply basic replacements + $template = str_replace(array_keys($replacements), array_values($replacements), $template); + + // Convert Ruby/strftime date formats to PHP date formats + $template = $this->convertDateFormats($template); + + // Convert {% render "template" with %} syntax to {% render "template", %} syntax + $template = preg_replace( + '/{%\s*render\s+([^}]+?)\s+with\s+/i', + '{% render $1, ', + $template + ); + + // Convert for loops with filters to use temporary variables + // This handles: {% for item in collection | filter: "key", "value" %} + // Converts to: {% assign temp_filtered = collection | filter: "key", "value" %}{% for item in temp_filtered %} + $template = preg_replace_callback( + '/{%\s*for\s+(\w+)\s+in\s+([^|%}]+)\s*\|\s*([^%}]+)%}/', + function (array $matches): string { + $variableName = mb_trim($matches[1]); + $collection = mb_trim($matches[2]); + $filter = mb_trim($matches[3]); + $tempVarName = '_temp_'.uniqid(); + + return "{% assign {$tempVarName} = {$collection} | {$filter} %}{% for {$variableName} in {$tempVarName} %}"; + }, + (string) $template + ); + + return $template; + } + + /** + * Convert Ruby/strftime date formats to PHP date formats in Liquid templates + */ + private function convertDateFormats(string $template): string + { + // Handle date filter formats: date: "format" or date: 'format' + $template = preg_replace_callback( + '/date:\s*(["\'])([^"\']+)\1/', + function (array $matches): string { + $quote = $matches[1]; + $format = $matches[2]; + $convertedFormat = \App\Liquid\Utils\ExpressionUtils::strftimeToPhpFormat($format); + + return 'date: '.$quote.$convertedFormat.$quote; + }, + $template + ); + + // Handle l_date filter formats: l_date: "format" or l_date: 'format' + $template = preg_replace_callback( + '/l_date:\s*(["\'])([^"\']+)\1/', + function (array $matches): string { + $quote = $matches[1]; + $format = $matches[2]; + $convertedFormat = \App\Liquid\Utils\ExpressionUtils::strftimeToPhpFormat($format); + + return 'l_date: '.$quote.$convertedFormat.$quote; + }, + (string) $template + ); + + 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); + $liquidTemplate = $environment->parseString($template); + $context = $environment->newRenderContext(data: $variables); + + 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(); } /** @@ -93,32 +415,112 @@ class Plugin extends Model * * @throws LiquidException */ - public function render(string $size = 'full', bool $standalone = true): string + 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') { - $environment = App::make('liquid.environment'); + // Get timezone from user or fall back to app timezone + $timezone = $this->user->timezone ?? config('app.timezone'); - // Register all custom filters - $environment->filterRegistry->register(Numbers::class); - $environment->filterRegistry->register(Data::class); - $environment->filterRegistry->register(StringMarkup::class); - $environment->filterRegistry->register(Uniqueness::class); - $environment->filterRegistry->register(Localization::class); + // Calculate UTC offset in seconds + $utcOffset = (string) Carbon::now($timezone)->getOffset(); - $template = $environment->parseString($this->render_markup); - $context = $environment->newRenderContext(data: ['size' => $size, 'data' => $this->data_payload]); - $renderedContent = $template->render($context); + // 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 : []), + ], + ], + ], + ]; + + // 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, 'data' => $this->data_payload]); + $renderedContent = Blade::render($this->render_markup, [ + 'size' => $size, + 'data' => $this->data_payload, + 'config' => $this->configuration ?? [], + ]); } if ($standalone) { - return view('trmnl-layouts.single', [ + 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' => $renderedContent, + ])->render(); + } + + return view('trmnl-layouts.mashup', [ + 'mashupLayout' => $this->getPreviewMashupLayoutForSize($size), + 'colorDepth' => $device?->colorDepth(), + 'deviceVariant' => $device?->deviceVariant() ?? 'og', + 'darkMode' => $this->dark_mode, + 'scaleLevel' => $device?->scaleLevel(), 'slot' => $renderedContent, ])->render(); + } return $renderedContent; @@ -126,21 +528,115 @@ class Plugin extends Model if ($this->render_markup_view) { if ($standalone) { - return view('trmnl-layouts.single', [ - 'slot' => view($this->render_markup_view, [ - 'size' => $size, - 'data' => $this->data_payload, - ])->render(), + $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', + 'darkMode' => $this->dark_mode, + 'scaleLevel' => $device?->scaleLevel(), + 'slot' => $renderedView, ])->render(); } return view($this->render_markup_view, [ 'size' => $size, 'data' => $this->data_payload, + 'config' => $this->configuration ?? [], ])->render(); } return '

No render markup yet defined for this plugin.

'; } + + /** + * Get a configuration value by key + */ + public function getConfiguration(string $key, $default = null) + { + return $this->configuration[$key] ?? $default; + } + + public function getPreviewMashupLayoutForSize(string $size): string + { + return match ($size) { + 'half_vertical' => '1Lx1R', + 'quadrant' => '2x2', + 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/Notifications/BatteryLow.php b/app/Notifications/BatteryLow.php index c76c87f..17fb1da 100644 --- a/app/Notifications/BatteryLow.php +++ b/app/Notifications/BatteryLow.php @@ -13,15 +13,10 @@ class BatteryLow extends Notification { use Queueable; - private Device $device; - /** * Create a new notification instance. */ - public function __construct(Device $device) - { - $this->device = $device; - } + public function __construct(private Device $device) {} /** * Get the notification's delivery channels. @@ -41,7 +36,7 @@ class BatteryLow extends Notification return (new MailMessage)->markdown('mail.battery-low', ['device' => $this->device]); } - public function toWebhook(object $notifiable) + public function toWebhook(object $notifiable): WebhookMessage { return WebhookMessage::create() ->data([ diff --git a/app/Notifications/Channels/WebhookChannel.php b/app/Notifications/Channels/WebhookChannel.php index d116200..796cb24 100644 --- a/app/Notifications/Channels/WebhookChannel.php +++ b/app/Notifications/Channels/WebhookChannel.php @@ -11,13 +11,7 @@ use Illuminate\Support\Arr; class WebhookChannel extends Notification { - /** @var Client */ - protected $client; - - public function __construct(Client $client) - { - $this->client = $client; - } + public function __construct(protected Client $client) {} /** * Send the given notification. diff --git a/app/Notifications/Messages/WebhookMessage.php b/app/Notifications/Messages/WebhookMessage.php index 920c16d..6dc58eb 100644 --- a/app/Notifications/Messages/WebhookMessage.php +++ b/app/Notifications/Messages/WebhookMessage.php @@ -13,13 +13,6 @@ final class WebhookMessage extends Notification */ private $query; - /** - * The POST data of the Webhook request. - * - * @var mixed - */ - private $data; - /** * The headers to send with the request. * @@ -36,9 +29,8 @@ final class WebhookMessage extends Notification /** * @param mixed $data - * @return static */ - public static function create($data = '') + public static function create($data = ''): self { return new self($data); } @@ -46,10 +38,12 @@ final class WebhookMessage extends Notification /** * @param mixed $data */ - public function __construct($data = '') - { - $this->data = $data; - } + public function __construct( + /** + * The POST data of the Webhook request. + */ + private $data = '' + ) {} /** * Set the Webhook parameters to be URL encoded. @@ -57,7 +51,7 @@ final class WebhookMessage extends Notification * @param mixed $query * @return $this */ - public function query($query) + public function query($query): self { $this->query = $query; @@ -70,7 +64,7 @@ final class WebhookMessage extends Notification * @param mixed $data * @return $this */ - public function data($data) + public function data($data): self { $this->data = $data; @@ -84,7 +78,7 @@ final class WebhookMessage extends Notification * @param string $value * @return $this */ - public function header($name, $value) + public function header($name, $value): self { $this->headers[$name] = $value; @@ -97,7 +91,7 @@ final class WebhookMessage extends Notification * @param string $userAgent * @return $this */ - public function userAgent($userAgent) + public function userAgent($userAgent): self { $this->headers['User-Agent'] = $userAgent; @@ -109,17 +103,14 @@ final class WebhookMessage extends Notification * * @return $this */ - public function verify($value = true) + public function verify($value = true): self { $this->verify = $value; return $this; } - /** - * @return array - */ - public function toArray() + public function toArray(): array { return [ 'query' => $this->query, diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 6ac75bf..b8ad9bb 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -3,6 +3,7 @@ namespace App\Providers; use App\Services\OidcProvider; +use Illuminate\Http\Request; use Illuminate\Support\Facades\URL; use Illuminate\Support\ServiceProvider; use Laravel\Socialite\Facades\Socialite; @@ -26,11 +27,25 @@ class AppServiceProvider extends ServiceProvider URL::forceScheme('https'); } + Request::macro('hasValidSignature', function ($absolute = true, array $ignoreQuery = []) { + $https = clone $this; + $https->server->set('HTTPS', 'on'); + + $http = clone $this; + $http->server->set('HTTPS', 'off'); + if (URL::hasValidSignature($https, $absolute, $ignoreQuery)) { + return true; + } + + return URL::hasValidSignature($http, $absolute, $ignoreQuery); + }); + // Register OIDC provider with Socialite - Socialite::extend('oidc', function ($app) { - $config = $app['config']['services.oidc'] ?? []; + Socialite::extend('oidc', function (\Illuminate\Contracts\Foundation\Application $app): OidcProvider { + $config = $app->make('config')->get('services.oidc', []); + return new OidcProvider( - $app['request'], + $app->make(Request::class), $config['client_id'] ?? null, $config['client_secret'] ?? null, $config['redirect'] ?? null, diff --git a/app/Services/ImageGenerationService.php b/app/Services/ImageGenerationService.php index d0ecddc..405ea3f 100644 --- a/app/Services/ImageGenerationService.php +++ b/app/Services/ImageGenerationService.php @@ -6,65 +6,33 @@ use App\Enums\ImageFormat; use App\Models\Device; use App\Models\DeviceModel; use App\Models\Plugin; +use Bnussbau\TrmnlPipeline\Stages\BrowserStage; +use Bnussbau\TrmnlPipeline\Stages\ImageStage; +use Bnussbau\TrmnlPipeline\TrmnlPipeline; use Exception; +use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Storage; -use Imagick; -use ImagickException; -use ImagickPixel; -use Log; +use InvalidArgumentException; use Ramsey\Uuid\Uuid; use RuntimeException; -use Spatie\Browsershot\Browsershot; use Wnx\SidecarBrowsershot\BrowsershotLambda; +use function config; +use function file_exists; +use function filesize; + class ImageGenerationService { public static function generateImage(string $markup, $deviceId): string { - $device = Device::with('deviceModel')->find($deviceId); - $uuid = Uuid::uuid4()->toString(); - $pngPath = Storage::disk('public')->path('/images/generated/'.$uuid.'.png'); - $bmpPath = Storage::disk('public')->path('/images/generated/'.$uuid.'.bmp'); - - // Get image generation settings from DeviceModel if available, otherwise use device settings - $imageSettings = self::getImageSettings($device); - - // Generate PNG - if (config('app.puppeteer_mode') === 'sidecar-aws') { - try { - $browsershot = BrowsershotLambda::html($markup) - ->windowSize(800, 480); - - if (config('app.puppeteer_wait_for_network_idle')) { - $browsershot->waitUntilNetworkIdle(); - } - - $browsershot->save($pngPath); - } catch (Exception $e) { - Log::error('Failed to generate PNG: '.$e->getMessage()); - throw new RuntimeException('Failed to generate PNG: '.$e->getMessage(), 0, $e); - } - } else { - try { - $browsershot = Browsershot::html($markup) - ->setOption('args', config('app.puppeteer_docker') ? ['--no-sandbox', '--disable-setuid-sandbox', '--disable-gpu'] : []); - if (config('app.puppeteer_wait_for_network_idle')) { - $browsershot->waitUntilNetworkIdle(); - } - if (config('app.puppeteer_window_size_strategy') == 'v2') { - $browsershot->windowSize($imageSettings['width'], $imageSettings['height']); - } else { - $browsershot->windowSize(800, 480); - } - $browsershot->save($pngPath); - } catch (Exception $e) { - Log::error('Failed to generate PNG: '.$e->getMessage()); - throw new RuntimeException('Failed to generate PNG: '.$e->getMessage(), 0, $e); - } - } - - // Convert image based on DeviceModel settings or fallback to device settings - self::convertImage($pngPath, $bmpPath, $imageSettings); + $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"); @@ -72,6 +40,116 @@ class ImageGenerationService 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 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); + + // Create custom Browsershot instance if using AWS Lambda + $browsershotInstance = null; + if (config('app.puppeteer_mode') === 'sidecar-aws') { + $browsershotInstance = new BrowsershotLambda(); + } + + $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']) + ->height($imageSettings['height']); + } else { + // default behaviour for Framework v1 + $browserStage->useDefaultDimensions(); + } + + if (config('app.puppeteer_wait_for_network_idle')) { + $browserStage->setBrowsershotOption('waitUntil', 'networkidle0'); + } + + if (config('app.puppeteer_docker')) { + $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']) + ->height($imageSettings['height']) + ->colors($imageSettings['colors']) + ->bitDepth($imageSettings['bit_depth']) + ->rotation($imageSettings['rotation']) + ->offsetX($imageSettings['offset_x']) + ->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) { + $imageStage->dither(); + } + + (new TrmnlPipeline())->pipe($browserStage) + ->pipe($imageStage) + ->process(); + + if (! file_exists($outputPath)) { + throw new RuntimeException('Image file was not created: '.$outputPath); + } + + if (filesize($outputPath) === 0) { + throw new RuntimeException('Image file is empty: '.$outputPath); + } + + Log::info("Generated image: $uuid"); + + return $uuid; + + } catch (Exception $e) { + Log::error('Failed to generate image: '.$e->getMessage()); + throw new RuntimeException('Failed to generate image: '.$e->getMessage(), 0, $e); + } + } + /** * Get image generation settings from DeviceModel if available, otherwise use device settings */ @@ -79,36 +157,63 @@ class ImageGenerationService { // If device has a DeviceModel, use its settings if ($device->deviceModel) { - /** @var DeviceModel $model */ - $model = $device->deviceModel; + return self::getImageSettingsFromModel($device->deviceModel); + } + // Fallback to device settings + $imageFormat = $device->image_format ?? ImageFormat::AUTO->value; + $mimeType = self::getMimeTypeFromImageFormat($imageFormat); + $colors = self::getColorsFromImageFormat($imageFormat); + $bitDepth = self::getBitDepthFromImageFormat($imageFormat); + + return [ + 'width' => $device->width ?? 800, + 'height' => $device->height ?? 480, + 'colors' => $colors, + 'bit_depth' => $bitDepth, + 'scale_factor' => 1.0, + 'rotation' => $device->rotate ?? 0, + 'mime_type' => $mimeType, + 'offset_x' => 0, + 'offset_y' => 0, + 'image_format' => $imageFormat, + 'use_model_settings' => false, + ]; + } + + /** + * Get image generation settings from a DeviceModel + */ + private static function getImageSettingsFromModel(?DeviceModel $deviceModel): array + { + if ($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), + '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, ]; } - // Fallback to device settings + // Default settings if no device model provided return [ - 'width' => $device->width ?? 800, - 'height' => $device->height ?? 480, + 'width' => 800, + 'height' => 480, 'colors' => 2, 'bit_depth' => 1, 'scale_factor' => 1.0, - 'rotation' => $device->rotate ?? 0, + 'rotation' => 0, 'mime_type' => 'image/png', 'offset_x' => 0, 'offset_y' => 0, - 'image_format' => $device->image_format, + 'image_format' => ImageFormat::AUTO->value, 'use_model_settings' => false, ]; } @@ -137,197 +242,73 @@ class ImageGenerationService } /** - * Convert image based on the provided settings + * Get MIME type from ImageFormat */ - private static function convertImage(string $pngPath, string $bmpPath, array $settings): void + private static function getMimeTypeFromImageFormat(string $imageFormat): string { - $imageFormat = $settings['image_format']; - $useModelSettings = $settings['use_model_settings'] ?? false; - - if ($useModelSettings) { - // Use DeviceModel-specific conversion - self::convertUsingModelSettings($pngPath, $bmpPath, $settings); - } else { - // Use legacy device-specific conversion - self::convertUsingLegacySettings($pngPath, $bmpPath, $imageFormat, $settings); - } + return match ($imageFormat) { + ImageFormat::BMP3_1BIT_SRGB->value => 'image/bmp', + ImageFormat::PNG_8BIT_GRAYSCALE->value, + ImageFormat::PNG_8BIT_256C->value, + ImageFormat::PNG_2BIT_4C->value => 'image/png', + ImageFormat::AUTO->value => 'image/png', // Default for AUTO + default => 'image/png', + }; } /** - * Convert image using DeviceModel settings + * Get colors from ImageFormat */ - private static function convertUsingModelSettings(string $pngPath, string $bmpPath, array $settings): void + private static function getColorsFromImageFormat(string $imageFormat): int { - try { - $imagick = new Imagick($pngPath); - - // Apply scale factor if needed - if ($settings['scale_factor'] !== 1.0) { - $newWidth = (int) ($settings['width'] * $settings['scale_factor']); - $newHeight = (int) ($settings['height'] * $settings['scale_factor']); - $imagick->resizeImage($newWidth, $newHeight, Imagick::FILTER_LANCZOS, 1, true); - } else { - // Resize to model dimensions if different from generated size - if ($imagick->getImageWidth() !== $settings['width'] || $imagick->getImageHeight() !== $settings['height']) { - $imagick->resizeImage($settings['width'], $settings['height'], Imagick::FILTER_LANCZOS, 1, true); - } - } - - // Apply rotation - if ($settings['rotation'] !== 0) { - $imagick->rotateImage(new ImagickPixel('black'), $settings['rotation']); - } - - // Apply offset if specified - if ($settings['offset_x'] !== 0 || $settings['offset_y'] !== 0) { - $imagick->rollImage($settings['offset_x'], $settings['offset_y']); - } - - // Handle special case for 4-color, 2-bit PNG - if ($settings['colors'] === 4 && $settings['bit_depth'] === 2 && $settings['mime_type'] === 'image/png') { - self::convertTo4Color2BitPng($imagick, $settings['width'], $settings['height']); - } else { - // Set image type and color depth based on model settings - $imagick->setImageType(Imagick::IMGTYPE_GRAYSCALE); - - if ($settings['bit_depth'] === 1) { - $imagick->quantizeImage(2, Imagick::COLORSPACE_GRAY, 0, true, false); - $imagick->setImageDepth(1); - } else { - $imagick->quantizeImage($settings['colors'], Imagick::COLORSPACE_GRAY, 0, true, false); - $imagick->setImageDepth($settings['bit_depth']); - } - } - - $imagick->stripImage(); - - // Save in the appropriate format - if ($settings['mime_type'] === 'image/bmp') { - $imagick->setFormat('BMP3'); - $imagick->writeImage($bmpPath); - } else { - $imagick->setFormat('png'); - $imagick->writeImage($pngPath); - } - - $imagick->clear(); - } catch (ImagickException $e) { - throw new RuntimeException('Failed to convert image using model settings: '.$e->getMessage(), 0, $e); - } + return match ($imageFormat) { + ImageFormat::BMP3_1BIT_SRGB->value, + ImageFormat::PNG_8BIT_GRAYSCALE->value => 2, + ImageFormat::PNG_8BIT_256C->value => 256, + ImageFormat::PNG_2BIT_4C->value => 4, + ImageFormat::AUTO->value => 2, // Default for AUTO + default => 2, + }; } /** - * Convert image to 4-color, 2-bit PNG using custom colormap and dithering + * Get bit depth from ImageFormat */ - private static function convertTo4Color2BitPng(Imagick $imagick, int $width, int $height): void + private static function getBitDepthFromImageFormat(string $imageFormat): int { - // Step 1: Create 4-color grayscale colormap in memory - $colors = ['#000000', '#555555', '#aaaaaa', '#ffffff']; - $colormap = new Imagick(); - - foreach ($colors as $color) { - $swatch = new Imagick(); - $swatch->newImage(1, 1, new ImagickPixel($color)); - $swatch->setImageFormat('png'); - $colormap->addImage($swatch); - } - - $colormap = $colormap->appendImages(true); // horizontal - $colormap->setType(Imagick::IMGTYPE_PALETTE); - $colormap->setImageFormat('png'); - - // Step 2: Resize to target dimensions without keeping aspect ratio - $imagick->resizeImage($width, $height, Imagick::FILTER_LANCZOS, 1, false); - - // Step 3: Apply Floyd–Steinberg dithering - $imagick->setOption('dither', 'FloydSteinberg'); - - // Step 4: Remap to our 4-color colormap - // $imagick->remapImage($colormap, Imagick::DITHERMETHOD_FLOYDSTEINBERG); - - // Step 5: Force 2-bit grayscale PNG - $imagick->setImageFormat('png'); - $imagick->setImageDepth(2); - $imagick->setType(Imagick::IMGTYPE_GRAYSCALE); - - // Cleanup colormap - $colormap->clear(); + return match ($imageFormat) { + ImageFormat::BMP3_1BIT_SRGB->value, + ImageFormat::PNG_8BIT_GRAYSCALE->value => 1, + ImageFormat::PNG_8BIT_256C->value => 8, + ImageFormat::PNG_2BIT_4C->value => 2, + ImageFormat::AUTO->value => 1, // Default for AUTO + default => 1, + }; } /** - * Convert image using legacy device settings + * Detect whether the provided HTML markup contains an tag with class "image-dither". */ - private static function convertUsingLegacySettings(string $pngPath, string $bmpPath, string $imageFormat, array $settings): void + private static function markupContainsDitherImage(string $markup): bool { - switch ($imageFormat) { - case ImageFormat::BMP3_1BIT_SRGB->value: - try { - self::convertToBmpImageMagick($pngPath, $bmpPath); - } catch (ImagickException $e) { - throw new RuntimeException('Failed to convert image to BMP: '.$e->getMessage(), 0, $e); - } - break; - case ImageFormat::PNG_8BIT_GRAYSCALE->value: - case ImageFormat::PNG_8BIT_256C->value: - try { - self::convertToPngImageMagick($pngPath, $settings['width'], $settings['height'], $settings['rotation'], quantize: $imageFormat === ImageFormat::PNG_8BIT_GRAYSCALE->value); - } catch (ImagickException $e) { - throw new RuntimeException('Failed to convert image to PNG: '.$e->getMessage(), 0, $e); - } - break; - case ImageFormat::AUTO->value: - default: - // For AUTO format, we need to check if this is a legacy device - // This would require checking if the device has a firmware version - // For now, we'll use the device's current logic - try { - self::convertToPngImageMagick($pngPath, $settings['width'], $settings['height'], $settings['rotation']); - } catch (ImagickException $e) { - throw new RuntimeException('Failed to convert image to PNG: '.$e->getMessage(), 0, $e); - } - } - } - - /** - * @throws ImagickException - */ - private static function convertToBmpImageMagick(string $pngPath, string $bmpPath): void - { - $imagick = new Imagick($pngPath); - $imagick->setImageType(Imagick::IMGTYPE_GRAYSCALE); - $imagick->quantizeImage(2, Imagick::COLORSPACE_GRAY, 0, true, false); - $imagick->setImageDepth(1); - $imagick->stripImage(); - $imagick->setFormat('BMP3'); - $imagick->writeImage($bmpPath); - $imagick->clear(); - } - - /** - * @throws ImagickException - */ - private static function convertToPngImageMagick(string $pngPath, ?int $width, ?int $height, ?int $rotate, $quantize = true): void - { - $imagick = new Imagick($pngPath); - if ($width !== 800 || $height !== 480) { - $imagick->resizeImage($width, $height, Imagick::FILTER_LANCZOS, 1, true); - } - if ($rotate !== null && $rotate !== 0) { - $imagick->rotateImage(new ImagickPixel('black'), $rotate); + if (mb_trim($markup) === '') { + return false; } - $imagick->setImageType(Imagick::IMGTYPE_GRAYSCALE); - $imagick->setOption('dither', 'FloydSteinberg'); - - if ($quantize) { - $imagick->quantizeImage(2, Imagick::COLORSPACE_GRAY, 0, true, false); + // Find (or with single quotes) and inspect class tokens + $imgWithClassPattern = '/]*\bclass\s*=\s*(["\'])(.*?)\1[^>]*>/i'; + if (! preg_match_all($imgWithClassPattern, $markup, $matches)) { + return false; } - $imagick->setImageDepth(8); - $imagick->stripImage(); - $imagick->setFormat('png'); - $imagick->writeImage($pngPath); - $imagick->clear(); + foreach ($matches[2] as $classValue) { + // Look for class token 'image-dither' or 'image--dither' + if (preg_match('/(?:^|\s)image--?dither(?:\s|$)/', $classValue)) { + return true; + } + } + + return false; } public static function cleanupFolder(): void @@ -353,16 +334,20 @@ 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) { + ->where(function ($query): void { $query->where('width', '!=', 800) ->orWhere('height', '!=', 480) ->orWhere('rotate', '!=', 0); }) - ->orWhereHas('deviceModel', function ($query) { + ->orWhereHas('deviceModel', function ($query): void { // Only allow caching if all device models have standard dimensions (800x480, rotation=0) - $query->where(function ($subQuery) { + $query->where(function ($subQuery): void { $subQuery->where('width', '!=', 800) ->orWhere('height', '!=', 480) ->orWhere('rotation', '!=', 0); @@ -377,4 +362,180 @@ class ImageGenerationService } } } + + /** + * Get device-specific default image path for setup or sleep mode + */ + public static function getDeviceSpecificDefaultImage(Device $device, string $imageType): ?string + { + // Validate image type + if (! in_array($imageType, ['setup-logo', 'sleep', 'error'])) { + return null; + } + + // If device has a DeviceModel, try to find device-specific image + if ($device->deviceModel) { + $model = $device->deviceModel; + $extension = $model->mime_type === 'image/bmp' ? 'bmp' : 'png'; + $filename = "{$model->width}_{$model->height}_{$model->bit_depth}_{$model->rotation}.{$extension}"; + $deviceSpecificPath = "images/default-screens/{$imageType}_{$filename}"; + + if (Storage::disk('public')->exists($deviceSpecificPath)) { + return $deviceSpecificPath; + } + } + + // Fallback to original hardcoded images + $fallbackPath = "images/{$imageType}.bmp"; + if (Storage::disk('public')->exists($fallbackPath)) { + return $fallbackPath; + } + + // Try PNG fallback + $fallbackPathPng = "images/{$imageType}.png"; + if (Storage::disk('public')->exists($fallbackPathPng)) { + return $fallbackPathPng; + } + + return null; + } + + /** + * Generate a default screen image from Blade template + */ + public static function generateDefaultScreenImage(Device $device, string $imageType, ?string $pluginName = null): string + { + // Validate image type + 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); + + $fileExtension = $imageSettings['mime_type'] === 'image/bmp' ? 'bmp' : 'png'; + $outputPath = Storage::disk('public')->path('/images/generated/'.$uuid.'.'.$fileExtension); + + // Generate HTML from Blade template + $html = self::generateDefaultScreenHtml($device, $imageType, $pluginName); + + // Create custom Browsershot instance if using AWS Lambda + $browsershotInstance = null; + if (config('app.puppeteer_mode') === 'sidecar-aws') { + $browsershotInstance = new BrowsershotLambda(); + } + + $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']) + ->height($imageSettings['height']); + } else { + $browserStage->useDefaultDimensions(); + } + + if (config('app.puppeteer_wait_for_network_idle')) { + $browserStage->setBrowsershotOption('waitUntil', 'networkidle0'); + } + + if (config('app.puppeteer_docker')) { + $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']) + ->height($imageSettings['height']) + ->colors($imageSettings['colors']) + ->bitDepth($imageSettings['bit_depth']) + ->rotation($imageSettings['rotation']) + ->offsetX($imageSettings['offset_x']) + ->offsetY($imageSettings['offset_y']) + ->outputPath($outputPath); + + // Apply color palette if available + if ($colorPalette) { + $imageStage->colormap($colorPalette); + } + + (new TrmnlPipeline())->pipe($browserStage) + ->pipe($imageStage) + ->process(); + + if (! file_exists($outputPath)) { + throw new RuntimeException('Image file was not created: '.$outputPath); + } + + if (filesize($outputPath) === 0) { + throw new RuntimeException('Image file is empty: '.$outputPath); + } + + Log::info("Device $device->id: generated default screen image: $uuid for type: $imageType"); + + return $uuid; + + } catch (Exception $e) { + Log::error('Failed to generate default screen image: '.$e->getMessage()); + throw new RuntimeException('Failed to generate default screen image: '.$e->getMessage(), 0, $e); + } + } + + /** + * Generate HTML from Blade template for default screens + */ + 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}") + }; + + // Determine device properties from DeviceModel or device settings + $deviceVariant = $device->deviceVariant(); + $deviceOrientation = $device->rotate > 0 ? 'portrait' : 'landscape'; + $colorDepth = $device->colorDepth() ?? '1bit'; + $scaleLevel = $device->scaleLevel(); + $darkMode = $imageType === 'sleep'; // Sleep mode uses dark mode, setup uses light mode + + // Build view data + $viewData = [ + 'noBleed' => false, + 'darkMode' => $darkMode, + 'deviceVariant' => $deviceVariant, + 'deviceOrientation' => $deviceOrientation, + 'colorDepth' => $colorDepth, + 'scaleLevel' => $scaleLevel, + ]; + + // 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/OidcProvider.php b/app/Services/OidcProvider.php index ad9799d..8ea2e44 100644 --- a/app/Services/OidcProvider.php +++ b/app/Services/OidcProvider.php @@ -2,11 +2,11 @@ namespace App\Services; +use Exception; +use GuzzleHttp\Client; use Laravel\Socialite\Two\AbstractProvider; use Laravel\Socialite\Two\ProviderInterface; use Laravel\Socialite\Two\User; -use GuzzleHttp\Client; -use Illuminate\Support\Arr; class OidcProvider extends AbstractProvider implements ProviderInterface { @@ -33,22 +33,22 @@ class OidcProvider extends AbstractProvider implements ProviderInterface /** * Create a new provider instance. */ - public function __construct($request, $clientId, $clientSecret, $redirectUrl, $scopes = [], $guzzle = []) + public function __construct(\Illuminate\Http\Request $request, $clientId, $clientSecret, $redirectUrl, $scopes = [], $guzzle = []) { parent::__construct($request, $clientId, $clientSecret, $redirectUrl, $guzzle); - + $endpoint = config('services.oidc.endpoint'); - if (!$endpoint) { - throw new \Exception('OIDC endpoint is not configured. Please set OIDC_ENDPOINT environment variable.'); + if (! $endpoint) { + throw new Exception('OIDC endpoint is not configured. Please set OIDC_ENDPOINT environment variable.'); } - + // Handle both full well-known URL and base URL - if (str_ends_with($endpoint, '/.well-known/openid-configuration')) { + if (str_ends_with((string) $endpoint, '/.well-known/openid-configuration')) { $this->baseUrl = str_replace('/.well-known/openid-configuration', '', $endpoint); } else { - $this->baseUrl = rtrim($endpoint, '/'); + $this->baseUrl = mb_rtrim($endpoint, '/'); } - + $this->scopes = $scopes ?: ['openid', 'profile', 'email']; $this->loadOidcConfiguration(); } @@ -59,21 +59,21 @@ class OidcProvider extends AbstractProvider implements ProviderInterface protected function loadOidcConfiguration() { try { - $url = $this->baseUrl . '/.well-known/openid-configuration'; - $client = new Client(); + $url = $this->baseUrl.'/.well-known/openid-configuration'; + $client = app(Client::class); $response = $client->get($url); $this->oidcConfig = json_decode($response->getBody()->getContents(), true); - - if (!$this->oidcConfig) { - throw new \Exception('OIDC configuration is empty or invalid JSON'); + + if (! $this->oidcConfig) { + throw new Exception('OIDC configuration is empty or invalid JSON'); } - - if (!isset($this->oidcConfig['authorization_endpoint'])) { - throw new \Exception('authorization_endpoint not found in OIDC configuration'); + + if (! isset($this->oidcConfig['authorization_endpoint'])) { + throw new Exception('authorization_endpoint not found in OIDC configuration'); } - - } catch (\Exception $e) { - throw new \Exception('Failed to load OIDC configuration: ' . $e->getMessage()); + + } catch (Exception $e) { + throw new Exception('Failed to load OIDC configuration: '.$e->getMessage(), $e->getCode(), $e); } } @@ -82,9 +82,10 @@ class OidcProvider extends AbstractProvider implements ProviderInterface */ protected function getAuthUrl($state) { - if (!$this->oidcConfig || !isset($this->oidcConfig['authorization_endpoint'])) { - throw new \Exception('OIDC configuration not loaded or authorization_endpoint not found.'); + if (! $this->oidcConfig || ! isset($this->oidcConfig['authorization_endpoint'])) { + throw new Exception('OIDC configuration not loaded or authorization_endpoint not found.'); } + return $this->buildAuthUrlFromBase($this->oidcConfig['authorization_endpoint'], $state); } @@ -93,9 +94,10 @@ class OidcProvider extends AbstractProvider implements ProviderInterface */ protected function getTokenUrl() { - if (!$this->oidcConfig || !isset($this->oidcConfig['token_endpoint'])) { - throw new \Exception('OIDC configuration not loaded or token_endpoint not found.'); + if (! $this->oidcConfig || ! isset($this->oidcConfig['token_endpoint'])) { + throw new Exception('OIDC configuration not loaded or token_endpoint not found.'); } + return $this->oidcConfig['token_endpoint']; } @@ -104,13 +106,13 @@ class OidcProvider extends AbstractProvider implements ProviderInterface */ protected function getUserByToken($token) { - if (!$this->oidcConfig || !isset($this->oidcConfig['userinfo_endpoint'])) { - throw new \Exception('OIDC configuration not loaded or userinfo_endpoint not found.'); + if (! $this->oidcConfig || ! isset($this->oidcConfig['userinfo_endpoint'])) { + throw new Exception('OIDC configuration not loaded or userinfo_endpoint not found.'); } - + $response = $this->getHttpClient()->get($this->oidcConfig['userinfo_endpoint'], [ 'headers' => [ - 'Authorization' => 'Bearer ' . $token, + 'Authorization' => 'Bearer '.$token, ], ]); @@ -120,7 +122,7 @@ class OidcProvider extends AbstractProvider implements ProviderInterface /** * Map the raw user array to a Socialite User instance. */ - protected function mapUserToObject(array $user) + public function mapUserToObject(array $user) { return (new User)->setRaw($user)->map([ 'id' => $user['sub'], @@ -153,4 +155,4 @@ class OidcProvider extends AbstractProvider implements ProviderInterface 'grant_type' => 'authorization_code', ]); } -} \ No newline at end of file +} 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/PluginExportService.php b/app/Services/PluginExportService.php new file mode 100644 index 0000000..241764d --- /dev/null +++ b/app/Services/PluginExportService.php @@ -0,0 +1,172 @@ +generateSettingsYaml($plugin); + $settingsYaml = Yaml::dump($settings, 10, 2, Yaml::DUMP_MULTI_LINE_LITERAL_BLOCK); + File::put($tempDir.'/settings.yml', $settingsYaml); + // Generate full template content + $fullTemplate = $this->generateFullTemplate($plugin); + $extension = $plugin->markup_language === 'liquid' ? 'liquid' : 'blade.php'; + File::put($tempDir.'/full.'.$extension, $fullTemplate); + // Generate shared.liquid if needed (for liquid templates) + if ($plugin->markup_language === 'liquid') { + $sharedTemplate = $this->generateSharedTemplate(); + /** @phpstan-ignore-next-line */ + if ($sharedTemplate) { + File::put($tempDir.'/shared.liquid', $sharedTemplate); + } + } + // Create ZIP file + $zipPath = $tempDir.'/plugin_'.$plugin->trmnlp_id.'.zip'; + $zip = new ZipArchive(); + if ($zip->open($zipPath, ZipArchive::CREATE) !== true) { + throw new Exception('Could not create ZIP file.'); + } + // Add files directly to ZIP root + $this->addDirectoryToZip($zip, $tempDir, ''); + $zip->close(); + + // Return the ZIP file as a download response + return response()->download($zipPath, 'plugin_'.$plugin->trmnlp_id.'.zip'); + } + + /** + * Generate the settings.yml content for the plugin + */ + private function generateSettingsYaml(Plugin $plugin): array + { + $settings = []; + + // Add fields in the specific order requested + $settings['name'] = $plugin->name; + $settings['no_screen_padding'] = 'no'; // Default value + $settings['dark_mode'] = 'no'; // Default value + $settings['strategy'] = $plugin->data_strategy; + + // Add static data if available + if ($plugin->data_payload) { + $settings['static_data'] = json_encode($plugin->data_payload, JSON_PRETTY_PRINT); + } + + // Add polling configuration if applicable + if ($plugin->data_strategy === 'polling') { + if ($plugin->polling_verb) { + $settings['polling_verb'] = $plugin->polling_verb; + } + if ($plugin->polling_url) { + $settings['polling_url'] = $plugin->polling_url; + } + if ($plugin->polling_header) { + // Convert header format from "key: value" to "key=value" + $settings['polling_headers'] = str_replace(':', '=', $plugin->polling_header); + } + if ($plugin->polling_body) { + $settings['polling_body'] = $plugin->polling_body; + } + } + + $settings['refresh_interval'] = $plugin->data_stale_minutes; + $settings['id'] = $plugin->trmnlp_id; + + // Add custom fields from configuration template + if (isset($plugin->configuration_template['custom_fields'])) { + $settings['custom_fields'] = $plugin->configuration_template['custom_fields']; + } + + return $settings; + } + + /** + * Generate the full template content + */ + private function generateFullTemplate(Plugin $plugin): string + { + $markup = $plugin->render_markup; + + // Remove the wrapper div if it exists (it will be added during import) + $markup = preg_replace('/^
\s*/', '', $markup); + $markup = preg_replace('/\s*<\/div>\s*$/', '', $markup); + + return mb_trim($markup); + } + + /** + * Generate the shared template content (for liquid templates) + */ + private function generateSharedTemplate(): null + { + // For now, we don't have a way to store shared templates separately + // TODO - add support for shared templates + return null; + } + + /** + * Add a directory and its contents to a ZIP file + */ + private function addDirectoryToZip(ZipArchive $zip, string $dirPath, string $zipPath): void + { + $files = new RecursiveIteratorIterator( + new RecursiveDirectoryIterator($dirPath), + RecursiveIteratorIterator::LEAVES_ONLY + ); + + foreach ($files as $file) { + if (! $file->isDir()) { + $filePath = $file->getRealPath(); + $fileName = basename((string) $filePath); + + // For root directory, just use the filename + $relativePath = $zipPath === '' ? $fileName : $zipPath.'/'.mb_substr((string) $filePath, mb_strlen($dirPath) + 1); + + $zip->addFile($filePath, $relativePath); + } + } + } +} diff --git a/app/Services/PluginImportService.php b/app/Services/PluginImportService.php new file mode 100644 index 0000000..49dce99 --- /dev/null +++ b/app/Services/PluginImportService.php @@ -0,0 +1,598 @@ +getRealPath(); + + // Extract the ZIP file using ZipArchive + $zip = new ZipArchive(); + if ($zip->open($zipFullPath) !== true) { + throw new Exception('Could not open the ZIP file.'); + } + + $zip->extractTo($tempDir); + $zip->close(); + + // 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']) { + 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); + + // Determine which template file to use and read its content + $templatePath = null; + $markupLanguage = 'blade'; + + 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 + if (! isset($settings['custom_fields']) || ! is_array($settings['custom_fields'])) { + $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']) + && 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, + 'name' => $settings['name'] ?? 'Imported Plugin', + 'trmnlp_id' => $settings['id'] ?? Uuid::v7(), + 'data_stale_minutes' => $settings['refresh_interval'] ?? 15, + 'data_strategy' => $settings['strategy'] ?? 'static', + 'polling_url' => $settings['polling_url'] ?? null, + 'polling_verb' => $settings['polling_verb'] ?? 'get', + 'polling_header' => isset($settings['polling_headers']) + ? str_replace('=', ':', $settings['polling_headers']) + : null, + 'polling_body' => $settings['polling_body'] ?? null, + 'markup_language' => $markupLanguage, + 'render_markup' => $fullLiquid, + 'configuration_template' => $configurationTemplate, + 'data_payload' => json_decode($settings['static_data'] ?? '{}', true), + ]); + + if (! $plugin_updated) { + // Extract default values from custom_fields and populate configuration + $configuration = []; + foreach ($settings['custom_fields'] as $field) { + if (isset($field['keyname']) && isset($field['default'])) { + $configuration[$field['keyname']] = $field['default']; + } + } + // set only if plugin is new + $plugin->update([ + 'configuration' => $configuration, + ]); + } + $plugin['trmnlp_yaml'] = $settingsYaml; + + return $plugin; + + } finally { + // Clean up temporary directory + Storage::deleteDirectory($tempDirName); + } + } + + /** + * Import a plugin from a ZIP URL + * + * @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, $preferredRenderer = null, ?string $iconUrl = null, bool $allowDuplicate = false): Plugin + { + // Download the ZIP file + $response = Http::timeout(60)->get($zipUrl); + + if (! $response->successful()) { + throw new Exception('Could not download the ZIP file from the provided URL.'); + } + + // Create a temporary file + $tempDirName = 'temp/'.uniqid('plugin_import_', true); + Storage::makeDirectory($tempDirName); + $tempDir = Storage::path($tempDirName); + $zipPath = $tempDir.'/plugin.zip'; + + // Save the downloaded content to a temporary file + File::put($zipPath, $response->body()); + + try { + // Extract the ZIP file using ZipArchive + $zip = new ZipArchive(); + if ($zip->open($zipPath) !== true) { + throw new Exception('Could not open the downloaded ZIP file.'); + } + + $zip->extractTo($tempDir); + $zip->close(); + + // 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']) { + 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); + + // Determine which template file to use and read its content + $templatePath = null; + $markupLanguage = 'blade'; + + 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 + if (! isset($settings['custom_fields']) || ! is_array($settings['custom_fields'])) { + $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'], + ]; + + // 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' => $trmnlpId, + ], + [ + 'user_id' => $user->id, + 'name' => $settings['name'] ?? 'Imported Plugin', + 'trmnlp_id' => $trmnlpId, + 'data_stale_minutes' => $settings['refresh_interval'] ?? 15, + 'data_strategy' => $settings['strategy'] ?? 'static', + 'polling_url' => $settings['polling_url'] ?? null, + 'polling_verb' => $settings['polling_verb'] ?? 'get', + 'polling_header' => isset($settings['polling_headers']) + ? str_replace('=', ':', $settings['polling_headers']) + : null, + 'polling_body' => $settings['polling_body'] ?? null, + 'markup_language' => $markupLanguage, + 'render_markup' => $fullLiquid, + 'configuration_template' => $configurationTemplate, + 'data_payload' => json_decode($settings['static_data'] ?? '{}', true), + 'preferred_renderer' => $preferredRenderer, + 'icon_url' => $iconUrl, + ]); + + if (! $plugin_updated) { + // Extract default values from custom_fields and populate configuration + $configuration = []; + foreach ($settings['custom_fields'] as $field) { + if (isset($field['keyname']) && isset($field['default'])) { + $configuration[$field['keyname']] = $field['default']; + } + } + // set only if plugin is new + $plugin->update([ + 'configuration' => $configuration, + ]); + } + $plugin['trmnlp_yaml'] = $settingsYaml; + + return $plugin; + + } finally { + // Clean up temporary directory + Storage::deleteDirectory($tempDirName); + } + } + + private function findRequiredFiles(string $tempDir, ?string $zipEntryPath = null): array + { + $settingsYamlPath = null; + $fullLiquidPath = null; + $sharedLiquidPath = null; + $sharedBladePath = null; + + // If zipEntryPath is specified, look for files in that specific directory first + if ($zipEntryPath) { + $targetDir = $tempDir.'/'.$zipEntryPath; + if (File::exists($targetDir)) { + // Check if files are directly in the target directory + if (File::exists($targetDir.'/settings.yml')) { + $settingsYamlPath = $targetDir.'/settings.yml'; + + if (File::exists($targetDir.'/full.liquid')) { + $fullLiquidPath = $targetDir.'/full.liquid'; + } elseif (File::exists($targetDir.'/full.blade.php')) { + $fullLiquidPath = $targetDir.'/full.blade.php'; + } + + if (File::exists($targetDir.'/shared.liquid')) { + $sharedLiquidPath = $targetDir.'/shared.liquid'; + } elseif (File::exists($targetDir.'/shared.blade.php')) { + $sharedBladePath = $targetDir.'/shared.blade.php'; + } + } + + // Check if files are in src subdirectory of target directory + if (! $settingsYamlPath && File::exists($targetDir.'/src/settings.yml')) { + $settingsYamlPath = $targetDir.'/src/settings.yml'; + + if (File::exists($targetDir.'/src/full.liquid')) { + $fullLiquidPath = $targetDir.'/src/full.liquid'; + } elseif (File::exists($targetDir.'/src/full.blade.php')) { + $fullLiquidPath = $targetDir.'/src/full.blade.php'; + } + + 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 || $sharedLiquidPath || $sharedBladePath)) { + return [ + 'settingsYamlPath' => $settingsYamlPath, + 'fullLiquidPath' => $fullLiquidPath, + 'sharedLiquidPath' => $sharedLiquidPath, + 'sharedBladePath' => $sharedBladePath, + ]; + } + } + } + + // First, check if files are directly in the src folder + if (File::exists($tempDir.'/src/settings.yml')) { + $settingsYamlPath = $tempDir.'/src/settings.yml'; + + // Check for full.liquid or full.blade.php + if (File::exists($tempDir.'/src/full.liquid')) { + $fullLiquidPath = $tempDir.'/src/full.liquid'; + } elseif (File::exists($tempDir.'/src/full.blade.php')) { + $fullLiquidPath = $tempDir.'/src/full.blade.php'; + } + + // 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 + $directories = new RecursiveDirectoryIterator($tempDir, RecursiveDirectoryIterator::SKIP_DOTS); + $files = new RecursiveIteratorIterator($directories); + + foreach ($files as $file) { + $filename = $file->getFilename(); + $filepath = $file->getPathname(); + + if ($filename === 'settings.yml') { + $settingsYamlPath = $filepath; + } elseif ($filename === 'full.liquid' || $filename === 'full.blade.php') { + $fullLiquidPath = $filepath; + } elseif ($filename === 'shared.liquid') { + $sharedLiquidPath = $filepath; + } elseif ($filename === 'shared.blade.php') { + $sharedBladePath = $filepath; + } + } + + // 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 || $sharedLiquidPath || $sharedBladePath)) { + // If the files are in the root of the ZIP, create a src folder and move them there + $srcDir = dirname((string) $settingsYamlPath); + + // If the parent directory is not named 'src', create a src directory + if (basename($srcDir) !== 'src') { + $newSrcDir = $tempDir.'/src'; + File::makeDirectory($newSrcDir, 0755, true); + + // Copy the files to the src directory + File::copy($settingsYamlPath, $newSrcDir.'/settings.yml'); + + // 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'; + } + } + } + + return [ + '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/composer.json b/composer.json index a2c72e2..8903e17 100644 --- a/composer.json +++ b/composer.json @@ -4,16 +4,19 @@ "type": "project", "description": "TRMNL Server Implementation (BYOS) for Laravel", "keywords": [ - "laravel", - "framework", - "trmnl" + "trmnl", + "trmnl-server", + "trmnl-byos", + "laravel" ], "license": "MIT", "require": { "php": "^8.2", "ext-imagick": "*", - "bnussbau/laravel-trmnl-blade": "1.2.*", - "intervention/image": "^3.11", + "ext-simplexml": "*", + "ext-zip": "*", + "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", @@ -21,7 +24,11 @@ "laravel/tinker": "^2.10.1", "livewire/flux": "^2.0", "livewire/volt": "^1.7", + "om/icalparser": "^3.2", + "simplesoftwareio/simple-qrcode": "^4.2", "spatie/browsershot": "^5.0", + "stevebauman/purify": "^6.3", + "symfony/yaml": "^7.3", "wnx/sidecar-browsershot": "^2.6" }, "require-dev": { @@ -33,10 +40,10 @@ "laravel/sail": "^1.41", "mockery/mockery": "^1.6", "nunomaduro/collision": "^8.6", - "pestphp/pest": "^3.7", - "pestphp/pest-plugin-drift": "^3.0", - "pestphp/pest-plugin-laravel": "^3.1", - "spatie/pest-expectations": "^1.10" + "pestphp/pest": "^4.0", + "pestphp/pest-plugin-drift": "^4.0", + "pestphp/pest-plugin-laravel": "^4.0", + "rector/rector": "^2.1" }, "autoload": { "psr-4": { @@ -72,7 +79,10 @@ ], "test": "vendor/bin/pest", "test-coverage": "vendor/bin/pest --coverage", - "format": "vendor/bin/pint" + "format": "vendor/bin/pint", + "analyse": "vendor/bin/phpstan analyse", + "analyze": "vendor/bin/phpstan analyse", + "rector": "vendor/bin/rector process" }, "extra": { "laravel": { diff --git a/composer.lock b/composer.lock index 0c56e9b..a469e55 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": "9143c36674f3ae13a9e9bad15014d508", + "content-hash": "4de5f1df0160f59d08f428e36e81262e", "packages": [ { "name": "aws/aws-crt-php", @@ -62,16 +62,16 @@ }, { "name": "aws/aws-sdk-php", - "version": "3.354.1", + "version": "3.369.12", "source": { "type": "git", "url": "https://github.com/aws/aws-sdk-php.git", - "reference": "6aa524596cd83416085777a3bd037d06a70b5c65" + "reference": "36ee8894743a254ae2650bad4968c874b76bc7de" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/6aa524596cd83416085777a3bd037d06a70b5c65", - "reference": "6aa524596cd83416085777a3bd037d06a70b5c65", + "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/36ee8894743a254ae2650bad4968c874b76bc7de", + "reference": "36ee8894743a254ae2650bad4968c874b76bc7de", "shasum": "" }, "require": { @@ -84,7 +84,8 @@ "guzzlehttp/psr7": "^2.4.5", "mtdowling/jmespath.php": "^2.8.0", "php": ">=8.1", - "psr/http-message": "^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,76 @@ "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.354.1" + "source": "https://github.com/aws/aws-sdk-php/tree/3.369.12" }, - "time": "2025-08-15T18:05:41+00:00" + "time": "2026-01-13T19:12:08+00:00" }, { - "name": "bnussbau/laravel-trmnl-blade", - "version": "1.2.1", + "name": "bacon/bacon-qr-code", + "version": "2.0.8", "source": { "type": "git", - "url": "https://github.com/bnussbau/laravel-trmnl-blade.git", - "reference": "fe11d1d7d896d6f0ea44664c1c6b5f00f1bdab36" + "url": "https://github.com/Bacon/BaconQrCode.git", + "reference": "8674e51bb65af933a5ffaf1c308a660387c35c22" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/bnussbau/laravel-trmnl-blade/zipball/fe11d1d7d896d6f0ea44664c1c6b5f00f1bdab36", - "reference": "fe11d1d7d896d6f0ea44664c1c6b5f00f1bdab36", + "url": "https://api.github.com/repos/Bacon/BaconQrCode/zipball/8674e51bb65af933a5ffaf1c308a660387c35c22", + "reference": "8674e51bb65af933a5ffaf1c308a660387c35c22", + "shasum": "" + }, + "require": { + "dasprid/enum": "^1.0.3", + "ext-iconv": "*", + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "phly/keep-a-changelog": "^2.1", + "phpunit/phpunit": "^7 | ^8 | ^9", + "spatie/phpunit-snapshot-assertions": "^4.2.9", + "squizlabs/php_codesniffer": "^3.4" + }, + "suggest": { + "ext-imagick": "to generate QR code images" + }, + "type": "library", + "autoload": { + "psr-4": { + "BaconQrCode\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-2-Clause" + ], + "authors": [ + { + "name": "Ben Scholzen 'DASPRiD'", + "email": "mail@dasprids.de", + "homepage": "https://dasprids.de/", + "role": "Developer" + } + ], + "description": "BaconQrCode is a QR code generator for PHP.", + "homepage": "https://github.com/Bacon/BaconQrCode", + "support": { + "issues": "https://github.com/Bacon/BaconQrCode/issues", + "source": "https://github.com/Bacon/BaconQrCode/tree/2.0.8" + }, + "time": "2022-12-07T17:46:57+00:00" + }, + { + "name": "bnussbau/laravel-trmnl-blade", + "version": "2.1.0", + "source": { + "type": "git", + "url": "https://github.com/bnussbau/laravel-trmnl-blade.git", + "reference": "1e1cabfead00118d7a80c86ac6108aece2989bc7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/bnussbau/laravel-trmnl-blade/zipball/1e1cabfead00118d7a80c86ac6108aece2989bc7", + "reference": "1e1cabfead00118d7a80c86ac6108aece2989bc7", "shasum": "" }, "require": { @@ -223,7 +277,7 @@ ], "support": { "issues": "https://github.com/bnussbau/laravel-trmnl-blade/issues", - "source": "https://github.com/bnussbau/laravel-trmnl-blade/tree/1.2.1" + "source": "https://github.com/bnussbau/laravel-trmnl-blade/tree/2.1.0" }, "funding": [ { @@ -239,29 +293,100 @@ "type": "github" } ], - "time": "2025-08-11T16:14:12+00:00" + "time": "2026-01-02T20:38:51+00:00" }, { - "name": "brick/math", - "version": "0.13.1", + "name": "bnussbau/trmnl-pipeline-php", + "version": "0.6.0", "source": { "type": "git", - "url": "https://github.com/brick/math.git", - "reference": "fc7ed316430118cc7836bf45faff18d5dfc8de04" + "url": "https://github.com/bnussbau/trmnl-pipeline-php.git", + "reference": "228505afa8a39a5033916e9ae5ccbf1733092c1f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/brick/math/zipball/fc7ed316430118cc7836bf45faff18d5dfc8de04", - "reference": "fc7ed316430118cc7836bf45faff18d5dfc8de04", + "url": "https://api.github.com/repos/bnussbau/trmnl-pipeline-php/zipball/228505afa8a39a5033916e9ae5ccbf1733092c1f", + "reference": "228505afa8a39a5033916e9ae5ccbf1733092c1f", "shasum": "" }, "require": { - "php": "^8.1" + "ext-imagick": "*", + "league/pipeline": "^1.0", + "php": "^8.2", + "spatie/browsershot": "^5.0" + }, + "require-dev": { + "laravel/pint": "^1.0", + "pestphp/pest": "^4.0", + "phpstan/phpstan": "^1.10", + "rector/rector": "^1.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Bnussbau\\TrmnlPipeline\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "bnussbau", + "email": "bnussbau@users.noreply.github.com", + "role": "Developer" + } + ], + "description": "Convert HTML content into optimized images for a range of e-ink devices.", + "homepage": "https://github.com/bnussbau/trmnl-pipeline-php", + "keywords": [ + "TRMNL", + "bnussbau", + "e-ink", + "trmnl-pipeline-php" + ], + "support": { + "issues": "https://github.com/bnussbau/trmnl-pipeline-php/issues", + "source": "https://github.com/bnussbau/trmnl-pipeline-php/tree/0.6.0" + }, + "funding": [ + { + "url": "https://www.buymeacoffee.com/bnussbau", + "type": "buy_me_a_coffee" + }, + { + "url": "https://usetrmnl.com/?ref=laravel-trmnl", + "type": "custom" + }, + { + "url": "https://github.com/bnussbau", + "type": "github" + } + ], + "time": "2025-12-02T15:18:51+00:00" + }, + { + "name": "brick/math", + "version": "0.14.1", + "source": { + "type": "git", + "url": "https://github.com/brick/math.git", + "reference": "f05858549e5f9d7bb45875a75583240a38a281d0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/brick/math/zipball/f05858549e5f9d7bb45875a75583240a38a281d0", + "reference": "f05858549e5f9d7bb45875a75583240a38a281d0", + "shasum": "" + }, + "require": { + "php": "^8.2" }, "require-dev": { "php-coveralls/php-coveralls": "^2.2", - "phpunit/phpunit": "^10.1", - "vimeo/psalm": "6.8.8" + "phpstan/phpstan": "2.1.22", + "phpunit/phpunit": "^11.5" }, "type": "library", "autoload": { @@ -291,7 +416,7 @@ ], "support": { "issues": "https://github.com/brick/math/issues", - "source": "https://github.com/brick/math/tree/0.13.1" + "source": "https://github.com/brick/math/tree/0.14.1" }, "funding": [ { @@ -299,7 +424,7 @@ "type": "github" } ], - "time": "2025-03-29T13:50:30+00:00" + "time": "2025-11-24T14:40:29+00:00" }, { "name": "carbonphp/carbon-doctrine-types", @@ -370,6 +495,56 @@ ], "time": "2024-02-09T16:56:22+00:00" }, + { + "name": "dasprid/enum", + "version": "1.0.7", + "source": { + "type": "git", + "url": "https://github.com/DASPRiD/Enum.git", + "reference": "b5874fa9ed0043116c72162ec7f4fb50e02e7cce" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/DASPRiD/Enum/zipball/b5874fa9ed0043116c72162ec7f4fb50e02e7cce", + "reference": "b5874fa9ed0043116c72162ec7f4fb50e02e7cce", + "shasum": "" + }, + "require": { + "php": ">=7.1 <9.0" + }, + "require-dev": { + "phpunit/phpunit": "^7 || ^8 || ^9 || ^10 || ^11", + "squizlabs/php_codesniffer": "*" + }, + "type": "library", + "autoload": { + "psr-4": { + "DASPRiD\\Enum\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-2-Clause" + ], + "authors": [ + { + "name": "Ben Scholzen 'DASPRiD'", + "email": "mail@dasprids.de", + "homepage": "https://dasprids.de/", + "role": "Developer" + } + ], + "description": "PHP 7.1 enum implementation", + "keywords": [ + "enum", + "map" + ], + "support": { + "issues": "https://github.com/DASPRiD/Enum/issues", + "source": "https://github.com/DASPRiD/Enum/tree/1.0.7" + }, + "time": "2025-09-16T12:23:56+00:00" + }, { "name": "dflydev/dot-access-data", "version": "v3.0.3", @@ -614,29 +789,28 @@ }, { "name": "dragonmantank/cron-expression", - "version": "v3.4.0", + "version": "v3.6.0", "source": { "type": "git", "url": "https://github.com/dragonmantank/cron-expression.git", - "reference": "8c784d071debd117328803d86b2097615b457500" + "reference": "d61a8a9604ec1f8c3d150d09db6ce98b32675013" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/dragonmantank/cron-expression/zipball/8c784d071debd117328803d86b2097615b457500", - "reference": "8c784d071debd117328803d86b2097615b457500", + "url": "https://api.github.com/repos/dragonmantank/cron-expression/zipball/d61a8a9604ec1f8c3d150d09db6ce98b32675013", + "reference": "d61a8a9604ec1f8c3d150d09db6ce98b32675013", "shasum": "" }, "require": { - "php": "^7.2|^8.0", - "webmozart/assert": "^1.0" + "php": "^8.2|^8.3|^8.4|^8.5" }, "replace": { "mtdowling/cron-expression": "^1.0" }, "require-dev": { - "phpstan/extension-installer": "^1.0", - "phpstan/phpstan": "^1.0", - "phpunit/phpunit": "^7.0|^8.0|^9.0" + "phpstan/extension-installer": "^1.4.3", + "phpstan/phpstan": "^1.12.32|^2.1.31", + "phpunit/phpunit": "^8.5.48|^9.0" }, "type": "library", "extra": { @@ -667,7 +841,7 @@ ], "support": { "issues": "https://github.com/dragonmantank/cron-expression/issues", - "source": "https://github.com/dragonmantank/cron-expression/tree/v3.4.0" + "source": "https://github.com/dragonmantank/cron-expression/tree/v3.6.0" }, "funding": [ { @@ -675,7 +849,7 @@ "type": "github" } ], - "time": "2024-10-09T13:47:03+00:00" + "time": "2025-10-31T18:51:33+00:00" }, { "name": "egulias/email-validator", @@ -745,17 +919,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": { @@ -803,37 +1038,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": { @@ -864,7 +1099,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": [ { @@ -876,28 +1111,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": { @@ -926,7 +1161,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": [ { @@ -938,26 +1173,26 @@ "type": "tidelift" } ], - "time": "2024-07-20T21:45:45+00:00" + "time": "2025-12-27T19:43:20+00:00" }, { "name": "guzzlehttp/guzzle", - "version": "7.9.3", + "version": "7.10.0", "source": { "type": "git", "url": "https://github.com/guzzle/guzzle.git", - "reference": "7b2f29fe81dc4da0ca0ea7d42107a0845946ea77" + "reference": "b51ac707cfa420b7bfd4e4d5e510ba8008e822b4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/guzzle/zipball/7b2f29fe81dc4da0ca0ea7d42107a0845946ea77", - "reference": "7b2f29fe81dc4da0ca0ea7d42107a0845946ea77", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/b51ac707cfa420b7bfd4e4d5e510ba8008e822b4", + "reference": "b51ac707cfa420b7bfd4e4d5e510ba8008e822b4", "shasum": "" }, "require": { "ext-json": "*", - "guzzlehttp/promises": "^1.5.3 || ^2.0.3", - "guzzlehttp/psr7": "^2.7.0", + "guzzlehttp/promises": "^2.3", + "guzzlehttp/psr7": "^2.8", "php": "^7.2.5 || ^8.0", "psr/http-client": "^1.0", "symfony/deprecation-contracts": "^2.2 || ^3.0" @@ -1048,7 +1283,7 @@ ], "support": { "issues": "https://github.com/guzzle/guzzle/issues", - "source": "https://github.com/guzzle/guzzle/tree/7.9.3" + "source": "https://github.com/guzzle/guzzle/tree/7.10.0" }, "funding": [ { @@ -1064,20 +1299,20 @@ "type": "tidelift" } ], - "time": "2025-03-27T13:37:11+00:00" + "time": "2025-08-23T22:36:01+00:00" }, { "name": "guzzlehttp/promises", - "version": "2.2.0", + "version": "2.3.0", "source": { "type": "git", "url": "https://github.com/guzzle/promises.git", - "reference": "7c69f28996b0a6920945dd20b3857e499d9ca96c" + "reference": "481557b130ef3790cf82b713667b43030dc9c957" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/promises/zipball/7c69f28996b0a6920945dd20b3857e499d9ca96c", - "reference": "7c69f28996b0a6920945dd20b3857e499d9ca96c", + "url": "https://api.github.com/repos/guzzle/promises/zipball/481557b130ef3790cf82b713667b43030dc9c957", + "reference": "481557b130ef3790cf82b713667b43030dc9c957", "shasum": "" }, "require": { @@ -1085,7 +1320,7 @@ }, "require-dev": { "bamarni/composer-bin-plugin": "^1.8.2", - "phpunit/phpunit": "^8.5.39 || ^9.6.20" + "phpunit/phpunit": "^8.5.44 || ^9.6.25" }, "type": "library", "extra": { @@ -1131,7 +1366,7 @@ ], "support": { "issues": "https://github.com/guzzle/promises/issues", - "source": "https://github.com/guzzle/promises/tree/2.2.0" + "source": "https://github.com/guzzle/promises/tree/2.3.0" }, "funding": [ { @@ -1147,20 +1382,20 @@ "type": "tidelift" } ], - "time": "2025-03-27T13:27:01+00:00" + "time": "2025-08-22T14:34:08+00:00" }, { "name": "guzzlehttp/psr7", - "version": "2.7.1", + "version": "2.8.0", "source": { "type": "git", "url": "https://github.com/guzzle/psr7.git", - "reference": "c2270caaabe631b3b44c85f99e5a04bbb8060d16" + "reference": "21dc724a0583619cd1652f673303492272778051" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/psr7/zipball/c2270caaabe631b3b44c85f99e5a04bbb8060d16", - "reference": "c2270caaabe631b3b44c85f99e5a04bbb8060d16", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/21dc724a0583619cd1652f673303492272778051", + "reference": "21dc724a0583619cd1652f673303492272778051", "shasum": "" }, "require": { @@ -1176,7 +1411,7 @@ "require-dev": { "bamarni/composer-bin-plugin": "^1.8.2", "http-interop/http-factory-tests": "0.9.0", - "phpunit/phpunit": "^8.5.39 || ^9.6.20" + "phpunit/phpunit": "^8.5.44 || ^9.6.25" }, "suggest": { "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses" @@ -1247,7 +1482,7 @@ ], "support": { "issues": "https://github.com/guzzle/psr7/issues", - "source": "https://github.com/guzzle/psr7/tree/2.7.1" + "source": "https://github.com/guzzle/psr7/tree/2.8.0" }, "funding": [ { @@ -1263,20 +1498,20 @@ "type": "tidelift" } ], - "time": "2025-03-27T12:30:47+00:00" + "time": "2025-08-23T21:21:41+00:00" }, { "name": "guzzlehttp/uri-template", - "version": "v1.0.4", + "version": "v1.0.5", "source": { "type": "git", "url": "https://github.com/guzzle/uri-template.git", - "reference": "30e286560c137526eccd4ce21b2de477ab0676d2" + "reference": "4f4bbd4e7172148801e76e3decc1e559bdee34e1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/uri-template/zipball/30e286560c137526eccd4ce21b2de477ab0676d2", - "reference": "30e286560c137526eccd4ce21b2de477ab0676d2", + "url": "https://api.github.com/repos/guzzle/uri-template/zipball/4f4bbd4e7172148801e76e3decc1e559bdee34e1", + "reference": "4f4bbd4e7172148801e76e3decc1e559bdee34e1", "shasum": "" }, "require": { @@ -1285,7 +1520,7 @@ }, "require-dev": { "bamarni/composer-bin-plugin": "^1.8.2", - "phpunit/phpunit": "^8.5.36 || ^9.6.15", + "phpunit/phpunit": "^8.5.44 || ^9.6.25", "uri-template/tests": "1.0.0" }, "type": "library", @@ -1333,7 +1568,7 @@ ], "support": { "issues": "https://github.com/guzzle/uri-template/issues", - "source": "https://github.com/guzzle/uri-template/tree/v1.0.4" + "source": "https://github.com/guzzle/uri-template/tree/v1.0.5" }, "funding": [ { @@ -1349,20 +1584,20 @@ "type": "tidelift" } ], - "time": "2025-02-03T10:55:03+00:00" + "time": "2025-08-22T14:27:06+00:00" }, { "name": "hammerstone/sidecar", - "version": "v0.7.0", + "version": "v0.7.1", "source": { "type": "git", "url": "https://github.com/aarondfrancis/sidecar.git", - "reference": "91a7001be31b16b51536aad42b14797653c3d862" + "reference": "e30df1a441bd5a61d3da9342328926227c63610f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/aarondfrancis/sidecar/zipball/91a7001be31b16b51536aad42b14797653c3d862", - "reference": "91a7001be31b16b51536aad42b14797653c3d862", + "url": "https://api.github.com/repos/aarondfrancis/sidecar/zipball/e30df1a441bd5a61d3da9342328926227c63610f", + "reference": "e30df1a441bd5a61d3da9342328926227c63610f", "shasum": "" }, "require": { @@ -1405,153 +1640,9 @@ "description": "A Laravel package to deploy Lambda functions alongside your main application.", "support": { "issues": "https://github.com/aarondfrancis/sidecar/issues", - "source": "https://github.com/aarondfrancis/sidecar/tree/v0.7.0" + "source": "https://github.com/aarondfrancis/sidecar/tree/v0.7.1" }, - "time": "2025-05-07T23:03:51+00:00" - }, - { - "name": "intervention/gif", - "version": "4.2.2", - "source": { - "type": "git", - "url": "https://github.com/Intervention/gif.git", - "reference": "5999eac6a39aa760fb803bc809e8909ee67b451a" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/Intervention/gif/zipball/5999eac6a39aa760fb803bc809e8909ee67b451a", - "reference": "5999eac6a39aa760fb803bc809e8909ee67b451a", - "shasum": "" - }, - "require": { - "php": "^8.1" - }, - "require-dev": { - "phpstan/phpstan": "^2.1", - "phpunit/phpunit": "^10.0 || ^11.0 || ^12.0", - "slevomat/coding-standard": "~8.0", - "squizlabs/php_codesniffer": "^3.8" - }, - "type": "library", - "autoload": { - "psr-4": { - "Intervention\\Gif\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Oliver Vogel", - "email": "oliver@intervention.io", - "homepage": "https://intervention.io/" - } - ], - "description": "Native PHP GIF Encoder/Decoder", - "homepage": "https://github.com/intervention/gif", - "keywords": [ - "animation", - "gd", - "gif", - "image" - ], - "support": { - "issues": "https://github.com/Intervention/gif/issues", - "source": "https://github.com/Intervention/gif/tree/4.2.2" - }, - "funding": [ - { - "url": "https://paypal.me/interventionio", - "type": "custom" - }, - { - "url": "https://github.com/Intervention", - "type": "github" - }, - { - "url": "https://ko-fi.com/interventionphp", - "type": "ko_fi" - } - ], - "time": "2025-03-29T07:46:21+00:00" - }, - { - "name": "intervention/image", - "version": "3.11.4", - "source": { - "type": "git", - "url": "https://github.com/Intervention/image.git", - "reference": "8c49eb21a6d2572532d1bc425964264f3e496846" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/Intervention/image/zipball/8c49eb21a6d2572532d1bc425964264f3e496846", - "reference": "8c49eb21a6d2572532d1bc425964264f3e496846", - "shasum": "" - }, - "require": { - "ext-mbstring": "*", - "intervention/gif": "^4.2", - "php": "^8.1" - }, - "require-dev": { - "mockery/mockery": "^1.6", - "phpstan/phpstan": "^2.1", - "phpunit/phpunit": "^10.0 || ^11.0 || ^12.0", - "slevomat/coding-standard": "~8.0", - "squizlabs/php_codesniffer": "^3.8" - }, - "suggest": { - "ext-exif": "Recommended to be able to read EXIF data properly." - }, - "type": "library", - "autoload": { - "psr-4": { - "Intervention\\Image\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Oliver Vogel", - "email": "oliver@intervention.io", - "homepage": "https://intervention.io/" - } - ], - "description": "PHP image manipulation", - "homepage": "https://image.intervention.io/", - "keywords": [ - "gd", - "image", - "imagick", - "resize", - "thumbnail", - "watermark" - ], - "support": { - "issues": "https://github.com/Intervention/image/issues", - "source": "https://github.com/Intervention/image/tree/3.11.4" - }, - "funding": [ - { - "url": "https://paypal.me/interventionio", - "type": "custom" - }, - { - "url": "https://github.com/Intervention", - "type": "github" - }, - { - "url": "https://ko-fi.com/interventionphp", - "type": "ko_fi" - } - ], - "time": "2025-07-30T13:13:19+00:00" + "time": "2025-08-22T14:58:51+00:00" }, { "name": "keepsuit/laravel-liquid", @@ -1630,16 +1721,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": { @@ -1648,17 +1739,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": { @@ -1685,26 +1776,26 @@ ], "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.24.0", + "version": "v12.47.0", "source": { "type": "git", "url": "https://github.com/laravel/framework.git", - "reference": "6dcf2c46da23d159f35d6246234953a74b740d83" + "reference": "ab8114c2e78f32e64eb238fc4b495bea3f8b80ec" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/framework/zipball/6dcf2c46da23d159f35d6246234953a74b740d83", - "reference": "6dcf2c46da23d159f35d6246234953a74b740d83", + "url": "https://api.github.com/repos/laravel/framework/zipball/ab8114c2e78f32e64eb238fc4b495bea3f8b80ec", + "reference": "ab8114c2e78f32e64eb238fc4b495bea3f8b80ec", "shasum": "" }, "require": { - "brick/math": "^0.11|^0.12|^0.13", + "brick/math": "^0.11|^0.12|^0.13|^0.14", "composer-runtime-api": "^2.2", "doctrine/inflector": "^2.0.5", "dragonmantank/cron-expression": "^3.4", @@ -1740,9 +1831,9 @@ "symfony/http-kernel": "^7.2.0", "symfony/mailer": "^7.2.0", "symfony/mime": "^7.2.0", - "symfony/polyfill-php83": "^1.31", - "symfony/polyfill-php84": "^1.31", - "symfony/polyfill-php85": "^1.31", + "symfony/polyfill-php83": "^1.33", + "symfony/polyfill-php84": "^1.33", + "symfony/polyfill-php85": "^1.33", "symfony/process": "^7.2.0", "symfony/routing": "^7.2.0", "symfony/uid": "^7.2.0", @@ -1778,6 +1869,7 @@ "illuminate/filesystem": "self.version", "illuminate/hashing": "self.version", "illuminate/http": "self.version", + "illuminate/json-schema": "self.version", "illuminate/log": "self.version", "illuminate/macroable": "self.version", "illuminate/mail": "self.version", @@ -1787,6 +1879,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", @@ -1810,13 +1903,14 @@ "league/flysystem-read-only": "^3.25.1", "league/flysystem-sftp-v3": "^3.25.1", "mockery/mockery": "^1.6.10", - "orchestra/testbench-core": "^10.6.0", + "opis/json-schema": "^2.4.1", + "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", @@ -1835,7 +1929,7 @@ "ext-pdo": "Required to use all database features.", "ext-posix": "Required to use all features of the queue worker.", "ext-redis": "Required to use the Redis cache and queue drivers (^4.0|^5.0|^6.0).", - "fakerphp/faker": "Required to use the eloquent factory builder (^1.9.1).", + "fakerphp/faker": "Required to generate fake data using the fake() helper (^1.23).", "filp/whoops": "Required for friendly error pages in development (^2.14.3).", "laravel/tinker": "Required to use the tinker console command (^2.0).", "league/flysystem-aws-s3-v3": "Required to use the Flysystem S3 driver (^3.25.1).", @@ -1850,7 +1944,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).", @@ -1872,6 +1966,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" ], @@ -1880,7 +1975,8 @@ "Illuminate\\Support\\": [ "src/Illuminate/Macroable/", "src/Illuminate/Collections/", - "src/Illuminate/Conditionable/" + "src/Illuminate/Conditionable/", + "src/Illuminate/Reflection/" ] } }, @@ -1904,20 +2000,20 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2025-08-13T20:30:36+00:00" + "time": "2026-01-13T15:29:06+00:00" }, { "name": "laravel/prompts", - "version": "v0.3.6", + "version": "v0.3.9", "source": { "type": "git", "url": "https://github.com/laravel/prompts.git", - "reference": "86a8b692e8661d0fb308cec64f3d176821323077" + "reference": "5c41bf0555b7cfefaad4e66d3046675829581ac4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/prompts/zipball/86a8b692e8661d0fb308cec64f3d176821323077", - "reference": "86a8b692e8661d0fb308cec64f3d176821323077", + "url": "https://api.github.com/repos/laravel/prompts/zipball/5c41bf0555b7cfefaad4e66d3046675829581ac4", + "reference": "5c41bf0555b7cfefaad4e66d3046675829581ac4", "shasum": "" }, "require": { @@ -1933,9 +2029,9 @@ "require-dev": { "illuminate/collections": "^10.0|^11.0|^12.0", "mockery/mockery": "^1.5", - "pestphp/pest": "^2.3|^3.4", - "phpstan/phpstan": "^1.11", - "phpstan/phpstan-mockery": "^1.1" + "pestphp/pest": "^2.3|^3.4|^4.0", + "phpstan/phpstan": "^1.12.28", + "phpstan/phpstan-mockery": "^1.1.3" }, "suggest": { "ext-pcntl": "Required for the spinner to be animated." @@ -1961,22 +2057,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.6" + "source": "https://github.com/laravel/prompts/tree/v0.3.9" }, - "time": "2025-07-07T14:17:42+00:00" + "time": "2026-01-07T21:00:29+00:00" }, { "name": "laravel/sanctum", - "version": "v4.2.0", + "version": "v4.2.3", "source": { "type": "git", "url": "https://github.com/laravel/sanctum.git", - "reference": "fd6df4f79f48a72992e8d29a9c0ee25422a0d677" + "reference": "47d26f1d310879ff757b971f5a6fc631d18663fd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/sanctum/zipball/fd6df4f79f48a72992e8d29a9c0ee25422a0d677", - "reference": "fd6df4f79f48a72992e8d29a9c0ee25422a0d677", + "url": "https://api.github.com/repos/laravel/sanctum/zipball/47d26f1d310879ff757b971f5a6fc631d18663fd", + "reference": "47d26f1d310879ff757b971f5a6fc631d18663fd", "shasum": "" }, "require": { @@ -1990,9 +2086,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": { @@ -2027,20 +2122,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-11T18:20:25+00:00" }, { "name": "laravel/serializable-closure", - "version": "v2.0.4", + "version": "v2.0.8", "source": { "type": "git", "url": "https://github.com/laravel/serializable-closure.git", - "reference": "b352cf0534aa1ae6b4d825d1e762e35d43f8a841" + "reference": "7581a4407012f5f53365e11bafc520fd7f36bc9b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/serializable-closure/zipball/b352cf0534aa1ae6b4d825d1e762e35d43f8a841", - "reference": "b352cf0534aa1ae6b4d825d1e762e35d43f8a841", + "url": "https://api.github.com/repos/laravel/serializable-closure/zipball/7581a4407012f5f53365e11bafc520fd7f36bc9b", + "reference": "7581a4407012f5f53365e11bafc520fd7f36bc9b", "shasum": "" }, "require": { @@ -2049,7 +2144,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" }, @@ -2088,25 +2183,25 @@ "issues": "https://github.com/laravel/serializable-closure/issues", "source": "https://github.com/laravel/serializable-closure" }, - "time": "2025-03-19T13:51:03+00:00" + "time": "2026-01-08T16:22:46+00:00" }, { "name": "laravel/socialite", - "version": "v5.23.0", + "version": "v5.24.2", "source": { "type": "git", "url": "https://github.com/laravel/socialite.git", - "reference": "e9e0fc83b9d8d71c8385a5da20e5b95ca6234cf5" + "reference": "5cea2eebf11ca4bc6c2f20495c82a70a9b3d1613" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/socialite/zipball/e9e0fc83b9d8d71c8385a5da20e5b95ca6234cf5", - "reference": "e9e0fc83b9d8d71c8385a5da20e5b95ca6234cf5", + "url": "https://api.github.com/repos/laravel/socialite/zipball/5cea2eebf11ca4bc6c2f20495c82a70a9b3d1613", + "reference": "5cea2eebf11ca4bc6c2f20495c82a70a9b3d1613", "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", @@ -2117,9 +2212,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": { @@ -2160,20 +2255,20 @@ "issues": "https://github.com/laravel/socialite/issues", "source": "https://github.com/laravel/socialite" }, - "time": "2025-07-23T14:16:08+00:00" + "time": "2026-01-10T16:07:28+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": { @@ -2182,7 +2277,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", @@ -2224,22 +2319,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": { @@ -2276,7 +2371,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "2.8-dev" + "dev-main": "2.9-dev" } }, "autoload": { @@ -2333,7 +2428,7 @@ "type": "tidelift" } ], - "time": "2025-07-20T12:47:49+00:00" + "time": "2025-11-26T21:48:24+00:00" }, { "name": "league/config", @@ -2419,16 +2514,16 @@ }, { "name": "league/flysystem", - "version": "3.30.0", + "version": "3.30.2", "source": { "type": "git", "url": "https://github.com/thephpleague/flysystem.git", - "reference": "2203e3151755d874bb2943649dae1eb8533ac93e" + "reference": "5966a8ba23e62bdb518dd9e0e665c2dbd4b5b277" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/2203e3151755d874bb2943649dae1eb8533ac93e", - "reference": "2203e3151755d874bb2943649dae1eb8533ac93e", + "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/5966a8ba23e62bdb518dd9e0e665c2dbd4b5b277", + "reference": "5966a8ba23e62bdb518dd9e0e665c2dbd4b5b277", "shasum": "" }, "require": { @@ -2496,22 +2591,22 @@ ], "support": { "issues": "https://github.com/thephpleague/flysystem/issues", - "source": "https://github.com/thephpleague/flysystem/tree/3.30.0" + "source": "https://github.com/thephpleague/flysystem/tree/3.30.2" }, - "time": "2025-06-25T13:29:59+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": { @@ -2545,9 +2640,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", @@ -2682,34 +2777,95 @@ "time": "2024-12-10T19:59:05+00:00" }, { - "name": "league/uri", - "version": "7.5.1", + "name": "league/pipeline", + "version": "1.1.0", "source": { "type": "git", - "url": "https://github.com/thephpleague/uri.git", - "reference": "81fb5145d2644324614cc532b28efd0215bda430" + "url": "https://github.com/thephpleague/pipeline.git", + "reference": "9069ddfdbd5582f8a563e00cffdbeffb9a0acd01" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/uri/zipball/81fb5145d2644324614cc532b28efd0215bda430", - "reference": "81fb5145d2644324614cc532b28efd0215bda430", + "url": "https://api.github.com/repos/thephpleague/pipeline/zipball/9069ddfdbd5582f8a563e00cffdbeffb9a0acd01", + "reference": "9069ddfdbd5582f8a563e00cffdbeffb9a0acd01", "shasum": "" }, "require": { - "league/uri-interfaces": "^7.5", - "php": "^8.1" + "php": "^8.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.0 || ^10.0 || ^11.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "League\\Pipeline\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Frank de Jonge", + "email": "info@frenky.net", + "role": "Author" + }, + { + "name": "Woody Gilk", + "email": "woody.gilk@gmail.com", + "role": "Maintainer" + } + ], + "description": "A plug and play pipeline implementation.", + "keywords": [ + "composition", + "design pattern", + "pattern", + "pipeline", + "sequential" + ], + "support": { + "issues": "https://github.com/thephpleague/pipeline/issues", + "source": "https://github.com/thephpleague/pipeline/tree/1.1.0" + }, + "time": "2025-02-06T08:48:15+00:00" + }, + { + "name": "league/uri", + "version": "7.7.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/uri.git", + "reference": "8d587cddee53490f9b82bf203d3a9aa7ea4f9807" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/uri/zipball/8d587cddee53490f9b82bf203d3a9aa7ea4f9807", + "reference": "8d587cddee53490f9b82bf203d3a9aa7ea4f9807", + "shasum": "" + }, + "require": { + "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", @@ -2737,6 +2893,7 @@ "description": "URI manipulation library", "homepage": "https://uri.thephpleague.com", "keywords": [ + "URN", "data-uri", "file-uri", "ftp", @@ -2749,9 +2906,11 @@ "psr-7", "query-string", "querystring", + "rfc2141", "rfc3986", "rfc3987", "rfc6570", + "rfc8141", "uri", "uri-template", "url", @@ -2761,7 +2920,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": [ { @@ -2769,26 +2928,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": { @@ -2796,6 +2954,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", @@ -2820,7 +2979,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", @@ -2845,7 +3004,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": [ { @@ -2853,20 +3012,20 @@ "type": "github" } ], - "time": "2024-12-08T08:18:47+00:00" + "time": "2025-12-07T16:03:21+00:00" }, { "name": "livewire/flux", - "version": "v2.2.4", + "version": "v2.10.2", "source": { "type": "git", "url": "https://github.com/livewire/flux.git", - "reference": "af81b5fd34c6490d5b5e05ed0f8140c0250e5069" + "reference": "e7a93989788429bb6c0a908a056d22ea3a6c7975" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/livewire/flux/zipball/af81b5fd34c6490d5b5e05ed0f8140c0250e5069", - "reference": "af81b5fd34c6490d5b5e05ed0f8140c0250e5069", + "url": "https://api.github.com/repos/livewire/flux/zipball/e7a93989788429bb6c0a908a056d22ea3a6c7975", + "reference": "e7a93989788429bb6c0a908a056d22ea3a6c7975", "shasum": "" }, "require": { @@ -2874,10 +3033,13 @@ "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", + "livewire/livewire": "^3.7.3|^4.0", "php": "^8.1", "symfony/console": "^6.0|^7.0" }, + "conflict": { + "livewire/blaze": "<1.0.0" + }, "type": "library", "extra": { "laravel": { @@ -2914,22 +3076,22 @@ ], "support": { "issues": "https://github.com/livewire/flux/issues", - "source": "https://github.com/livewire/flux/tree/v2.2.4" + "source": "https://github.com/livewire/flux/tree/v2.10.2" }, - "time": "2025-08-09T01:46:51+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": { @@ -2984,7 +3146,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": [ { @@ -2992,32 +3154,31 @@ "type": "github" } ], - "time": "2025-07-17T05:12:15+00:00" + "time": "2025-12-19T02:00:29+00:00" }, { "name": "livewire/volt", - "version": "v1.7.2", + "version": "v1.10.1", "source": { "type": "git", "url": "https://github.com/livewire/volt.git", - "reference": "91ba934e72bbd162442840862959ade24dbe728a" + "reference": "48cff133990c6261c63ee279fc091af6f6c6654e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/livewire/volt/zipball/91ba934e72bbd162442840862959ade24dbe728a", - "reference": "91ba934e72bbd162442840862959ade24dbe728a", + "url": "https://api.github.com/repos/livewire/volt/zipball/48cff133990c6261c63ee279fc091af6f6c6654e", + "reference": "48cff133990c6261c63ee279fc091af6f6c6654e", "shasum": "" }, "require": { "laravel/framework": "^10.38.2|^11.0|^12.0", - "livewire/livewire": "^3.6.1", + "livewire/livewire": "^3.6.1|^4.0", "php": "^8.1" }, "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", @@ -3064,20 +3225,20 @@ "issues": "https://github.com/livewire/volt/issues", "source": "https://github.com/livewire/volt" }, - "time": "2025-08-06T15:40:50+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": { @@ -3088,7 +3249,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", @@ -3134,7 +3295,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": [ { @@ -3142,20 +3303,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": { @@ -3173,7 +3334,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", @@ -3233,7 +3394,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": [ { @@ -3245,7 +3406,7 @@ "type": "tidelift" } ], - "time": "2025-03-24T10:02:05+00:00" + "time": "2026-01-02T08:56:05+00:00" }, { "name": "mtdowling/jmespath.php", @@ -3315,16 +3476,16 @@ }, { "name": "nesbot/carbon", - "version": "3.10.2", + "version": "3.11.0", "source": { "type": "git", "url": "https://github.com/CarbonPHP/carbon.git", - "reference": "76b5c07b8a9d2025ed1610e14cef1f3fd6ad2c24" + "reference": "bdb375400dcd162624531666db4799b36b64e4a1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/CarbonPHP/carbon/zipball/76b5c07b8a9d2025ed1610e14cef1f3fd6ad2c24", - "reference": "76b5c07b8a9d2025ed1610e14cef1f3fd6ad2c24", + "url": "https://api.github.com/repos/CarbonPHP/carbon/zipball/bdb375400dcd162624531666db4799b36b64e4a1", + "reference": "bdb375400dcd162624531666db4799b36b64e4a1", "shasum": "" }, "require": { @@ -3332,9 +3493,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" @@ -3342,13 +3503,13 @@ "require-dev": { "doctrine/dbal": "^3.6.3 || ^4.0", "doctrine/orm": "^2.15.2 || ^3.0", - "friendsofphp/php-cs-fixer": "^3.75.0", + "friendsofphp/php-cs-fixer": "^v3.87.1", "kylekatarnls/multi-tester": "^2.5.3", "phpmd/phpmd": "^2.15.0", "phpstan/extension-installer": "^1.4.3", - "phpstan/phpstan": "^2.1.17", - "phpunit/phpunit": "^10.5.46", - "squizlabs/php_codesniffer": "^3.13.0" + "phpstan/phpstan": "^2.1.22", + "phpunit/phpunit": "^10.5.53", + "squizlabs/php_codesniffer": "^3.13.4" }, "bin": [ "bin/carbon" @@ -3416,29 +3577,29 @@ "type": "tidelift" } ], - "time": "2025-08-02T09:36:06+00:00" + "time": "2025-12-02T21:04:28+00:00" }, { "name": "nette/schema", - "version": "v1.3.2", + "version": "v1.3.3", "source": { "type": "git", "url": "https://github.com/nette/schema.git", - "reference": "da801d52f0354f70a638673c4a0f04e16529431d" + "reference": "2befc2f42d7c715fd9d95efc31b1081e5d765004" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nette/schema/zipball/da801d52f0354f70a638673c4a0f04e16529431d", - "reference": "da801d52f0354f70a638673c4a0f04e16529431d", + "url": "https://api.github.com/repos/nette/schema/zipball/2befc2f42d7c715fd9d95efc31b1081e5d765004", + "reference": "2befc2f42d7c715fd9d95efc31b1081e5d765004", "shasum": "" }, "require": { "nette/utils": "^4.0", - "php": "8.1 - 8.4" + "php": "8.1 - 8.5" }, "require-dev": { "nette/tester": "^2.5.2", - "phpstan/phpstan-nette": "^1.0", + "phpstan/phpstan-nette": "^2.0@stable", "tracy/tracy": "^2.8" }, "type": "library", @@ -3448,6 +3609,9 @@ } }, "autoload": { + "psr-4": { + "Nette\\": "src" + }, "classmap": [ "src/" ] @@ -3476,26 +3640,26 @@ ], "support": { "issues": "https://github.com/nette/schema/issues", - "source": "https://github.com/nette/schema/tree/v1.3.2" + "source": "https://github.com/nette/schema/tree/v1.3.3" }, - "time": "2024-10-06T23:10:23+00:00" + "time": "2025-10-30T22:57:59+00:00" }, { "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", @@ -3518,7 +3682,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "4.0-dev" + "dev-master": "4.1-dev" } }, "autoload": { @@ -3565,22 +3729,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.1", + "version": "v5.7.0", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "f103601b29efebd7ff4a1ca7b3eeea9e3336a2a2" + "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/f103601b29efebd7ff4a1ca7b3eeea9e3336a2a2", - "reference": "f103601b29efebd7ff4a1ca7b3eeea9e3336a2a2", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/dca41cd15c2ac9d055ad70dbfd011130757d1f82", + "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82", "shasum": "" }, "require": { @@ -3623,37 +3787,37 @@ ], "support": { "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v5.6.1" + "source": "https://github.com/nikic/PHP-Parser/tree/v5.7.0" }, - "time": "2025-08-13T20:13:15+00:00" + "time": "2025-12-06T11:56:16+00:00" }, { "name": "nunomaduro/termwind", - "version": "v2.3.1", + "version": "v2.3.3", "source": { "type": "git", "url": "https://github.com/nunomaduro/termwind.git", - "reference": "dfa08f390e509967a15c22493dc0bac5733d9123" + "reference": "6fb2a640ff502caace8e05fd7be3b503a7e1c017" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nunomaduro/termwind/zipball/dfa08f390e509967a15c22493dc0bac5733d9123", - "reference": "dfa08f390e509967a15c22493dc0bac5733d9123", + "url": "https://api.github.com/repos/nunomaduro/termwind/zipball/6fb2a640ff502caace8e05fd7be3b503a7e1c017", + "reference": "6fb2a640ff502caace8e05fd7be3b503a7e1c017", "shasum": "" }, "require": { "ext-mbstring": "*", "php": "^8.2", - "symfony/console": "^7.2.6" + "symfony/console": "^7.3.6" }, "require-dev": { - "illuminate/console": "^11.44.7", - "laravel/pint": "^1.22.0", + "illuminate/console": "^11.46.1", + "laravel/pint": "^1.25.1", "mockery/mockery": "^1.6.12", - "pestphp/pest": "^2.36.0 || ^3.8.2", - "phpstan/phpstan": "^1.12.25", + "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.2.6", + "symfony/var-dumper": "^7.3.5", "thecodingmachine/phpstan-strict-rules": "^1.0.0" }, "type": "library", @@ -3696,7 +3860,7 @@ ], "support": { "issues": "https://github.com/nunomaduro/termwind/issues", - "source": "https://github.com/nunomaduro/termwind/tree/v2.3.1" + "source": "https://github.com/nunomaduro/termwind/tree/v2.3.3" }, "funding": [ { @@ -3712,28 +3876,81 @@ "type": "github" } ], - "time": "2025-05-08T08:14:37+00:00" + "time": "2025-11-20T02:34:59+00:00" }, { - "name": "paragonie/constant_time_encoding", - "version": "v3.0.0", + "name": "om/icalparser", + "version": "v3.2.1", "source": { "type": "git", - "url": "https://github.com/paragonie/constant_time_encoding.git", - "reference": "df1e7fde177501eee2037dd159cf04f5f301a512" + "url": "https://github.com/OzzyCzech/icalparser.git", + "reference": "bc7a82b12455ae9b62ce8e7f2d0273e86c931ecc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/paragonie/constant_time_encoding/zipball/df1e7fde177501eee2037dd159cf04f5f301a512", - "reference": "df1e7fde177501eee2037dd159cf04f5f301a512", + "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", + "version": "v3.1.3", + "source": { + "type": "git", + "url": "https://github.com/paragonie/constant_time_encoding.git", + "reference": "d5b01a39b3415c2cd581d3bd3a3575c1ebbd8e77" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/paragonie/constant_time_encoding/zipball/d5b01a39b3415c2cd581d3bd3a3575c1ebbd8e77", + "reference": "d5b01a39b3415c2cd581d3bd3a3575c1ebbd8e77", "shasum": "" }, "require": { "php": "^8" }, "require-dev": { - "phpunit/phpunit": "^9", - "vimeo/psalm": "^4|^5" + "infection/infection": "^0", + "nikic/php-fuzzer": "^0", + "phpunit/phpunit": "^9|^10|^11", + "vimeo/psalm": "^4|^5|^6" }, "type": "library", "autoload": { @@ -3779,7 +3996,7 @@ "issues": "https://github.com/paragonie/constant_time_encoding/issues", "source": "https://github.com/paragonie/constant_time_encoding" }, - "time": "2024-05-08T12:36:18+00:00" + "time": "2025-09-24T15:06:41+00:00" }, { "name": "paragonie/random_compat", @@ -3833,16 +4050,16 @@ }, { "name": "phpoption/phpoption", - "version": "1.9.3", + "version": "1.9.5", "source": { "type": "git", "url": "https://github.com/schmittjoh/php-option.git", - "reference": "e3fac8b24f56113f7cb96af14958c0dd16330f54" + "reference": "75365b91986c2405cf5e1e012c5595cd487a98be" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/schmittjoh/php-option/zipball/e3fac8b24f56113f7cb96af14958c0dd16330f54", - "reference": "e3fac8b24f56113f7cb96af14958c0dd16330f54", + "url": "https://api.github.com/repos/schmittjoh/php-option/zipball/75365b91986c2405cf5e1e012c5595cd487a98be", + "reference": "75365b91986c2405cf5e1e012c5595cd487a98be", "shasum": "" }, "require": { @@ -3850,7 +4067,7 @@ }, "require-dev": { "bamarni/composer-bin-plugin": "^1.8.2", - "phpunit/phpunit": "^8.5.39 || ^9.6.20 || ^10.5.28" + "phpunit/phpunit": "^8.5.44 || ^9.6.25 || ^10.5.53 || ^11.5.34" }, "type": "library", "extra": { @@ -3892,7 +4109,7 @@ ], "support": { "issues": "https://github.com/schmittjoh/php-option/issues", - "source": "https://github.com/schmittjoh/php-option/tree/1.9.3" + "source": "https://github.com/schmittjoh/php-option/tree/1.9.5" }, "funding": [ { @@ -3904,20 +4121,20 @@ "type": "tidelift" } ], - "time": "2024-07-20T21:41:07+00:00" + "time": "2025-12-27T19:41:33+00:00" }, { "name": "phpseclib/phpseclib", - "version": "3.0.46", + "version": "3.0.48", "source": { "type": "git", "url": "https://github.com/phpseclib/phpseclib.git", - "reference": "56483a7de62a6c2a6635e42e93b8a9e25d4f0ec6" + "reference": "64065a5679c50acb886e82c07aa139b0f757bb89" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/56483a7de62a6c2a6635e42e93b8a9e25d4f0ec6", - "reference": "56483a7de62a6c2a6635e42e93b8a9e25d4f0ec6", + "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/64065a5679c50acb886e82c07aa139b0f757bb89", + "reference": "64065a5679c50acb886e82c07aa139b0f757bb89", "shasum": "" }, "require": { @@ -3998,7 +4215,7 @@ ], "support": { "issues": "https://github.com/phpseclib/phpseclib/issues", - "source": "https://github.com/phpseclib/phpseclib/tree/3.0.46" + "source": "https://github.com/phpseclib/phpseclib/tree/3.0.48" }, "funding": [ { @@ -4014,7 +4231,7 @@ "type": "tidelift" } ], - "time": "2025-06-26T16:29:55+00:00" + "time": "2025-12-15T11:51:42+00:00" }, { "name": "psr/clock", @@ -4430,16 +4647,16 @@ }, { "name": "psy/psysh", - "version": "v0.12.10", + "version": "v0.12.18", "source": { "type": "git", "url": "https://github.com/bobthecow/psysh.git", - "reference": "6e80abe6f2257121f1eb9a4c55bf29d921025b22" + "reference": "ddff0ac01beddc251786fe70367cd8bbdb258196" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/bobthecow/psysh/zipball/6e80abe6f2257121f1eb9a4c55bf29d921025b22", - "reference": "6e80abe6f2257121f1eb9a4c55bf29d921025b22", + "url": "https://api.github.com/repos/bobthecow/psysh/zipball/ddff0ac01beddc251786fe70367cd8bbdb258196", + "reference": "ddff0ac01beddc251786fe70367cd8bbdb258196", "shasum": "" }, "require": { @@ -4447,18 +4664,19 @@ "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" }, "require-dev": { - "bamarni/composer-bin-plugin": "^1.2" + "bamarni/composer-bin-plugin": "^1.2", + "composer/class-map-generator": "^1.6" }, "suggest": { + "composer/class-map-generator": "Improved tab completion performance with better class discovery.", "ext-pcntl": "Enabling the PCNTL extension makes PsySH a lot happier :)", - "ext-pdo-sqlite": "The doc command requires SQLite to work.", "ext-posix": "If you have PCNTL, you'll want the POSIX extension as well." }, "bin": [ @@ -4502,9 +4720,9 @@ ], "support": { "issues": "https://github.com/bobthecow/psysh/issues", - "source": "https://github.com/bobthecow/psysh/tree/v0.12.10" + "source": "https://github.com/bobthecow/psysh/tree/v0.12.18" }, - "time": "2025-08-04T12:39:37+00:00" + "time": "2025-12-17T14:35:46+00:00" }, { "name": "ralouphie/getallheaders", @@ -4628,20 +4846,20 @@ }, { "name": "ramsey/uuid", - "version": "4.9.0", + "version": "4.9.2", "source": { "type": "git", "url": "https://github.com/ramsey/uuid.git", - "reference": "4e0e23cc785f0724a0e838279a9eb03f28b092a0" + "reference": "8429c78ca35a09f27565311b98101e2826affde0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/ramsey/uuid/zipball/4e0e23cc785f0724a0e838279a9eb03f28b092a0", - "reference": "4e0e23cc785f0724a0e838279a9eb03f28b092a0", + "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", + "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" }, @@ -4700,22 +4918,90 @@ ], "support": { "issues": "https://github.com/ramsey/uuid/issues", - "source": "https://github.com/ramsey/uuid/tree/4.9.0" + "source": "https://github.com/ramsey/uuid/tree/4.9.2" }, - "time": "2025-06-25T14:20:11+00:00" + "time": "2025-12-14T04:43:48+00:00" }, { - "name": "spatie/browsershot", - "version": "5.0.10", + "name": "simplesoftwareio/simple-qrcode", + "version": "4.2.0", "source": { "type": "git", - "url": "https://github.com/spatie/browsershot.git", - "reference": "9e5ae15487b3cdc3eb03318c1c8ac38971f60e58" + "url": "https://github.com/SimpleSoftwareIO/simple-qrcode.git", + "reference": "916db7948ca6772d54bb617259c768c9cdc8d537" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/spatie/browsershot/zipball/9e5ae15487b3cdc3eb03318c1c8ac38971f60e58", - "reference": "9e5ae15487b3cdc3eb03318c1c8ac38971f60e58", + "url": "https://api.github.com/repos/SimpleSoftwareIO/simple-qrcode/zipball/916db7948ca6772d54bb617259c768c9cdc8d537", + "reference": "916db7948ca6772d54bb617259c768c9cdc8d537", + "shasum": "" + }, + "require": { + "bacon/bacon-qr-code": "^2.0", + "ext-gd": "*", + "php": ">=7.2|^8.0" + }, + "require-dev": { + "mockery/mockery": "~1", + "phpunit/phpunit": "~9" + }, + "suggest": { + "ext-imagick": "Allows the generation of PNG QrCodes.", + "illuminate/support": "Allows for use within Laravel." + }, + "type": "library", + "extra": { + "laravel": { + "aliases": { + "QrCode": "SimpleSoftwareIO\\QrCode\\Facades\\QrCode" + }, + "providers": [ + "SimpleSoftwareIO\\QrCode\\QrCodeServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "SimpleSoftwareIO\\QrCode\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Simple Software LLC", + "email": "support@simplesoftware.io" + } + ], + "description": "Simple QrCode is a QR code generator made for Laravel.", + "homepage": "https://www.simplesoftware.io/#/docs/simple-qrcode", + "keywords": [ + "Simple", + "generator", + "laravel", + "qrcode", + "wrapper" + ], + "support": { + "issues": "https://github.com/SimpleSoftwareIO/simple-qrcode/issues", + "source": "https://github.com/SimpleSoftwareIO/simple-qrcode/tree/4.2.0" + }, + "time": "2021-02-08T20:43:55+00:00" + }, + { + "name": "spatie/browsershot", + "version": "5.2.0", + "source": { + "type": "git", + "url": "https://github.com/spatie/browsershot.git", + "reference": "9bc6b8d67175810d7a399b2588c3401efe2d02a8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/spatie/browsershot/zipball/9bc6b8d67175810d7a399b2588c3401efe2d02a8", + "reference": "9bc6b8d67175810d7a399b2588c3401efe2d02a8", "shasum": "" }, "require": { @@ -4723,13 +5009,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": { @@ -4762,7 +5048,7 @@ "webpage" ], "support": { - "source": "https://github.com/spatie/browsershot/tree/5.0.10" + "source": "https://github.com/spatie/browsershot/tree/5.2.0" }, "funding": [ { @@ -4770,7 +5056,7 @@ "type": "github" } ], - "time": "2025-05-15T07:10:57+00:00" + "time": "2025-12-22T10:02:16+00:00" }, { "name": "spatie/laravel-package-tools", @@ -4835,16 +5121,16 @@ }, { "name": "spatie/temporary-directory", - "version": "2.3.0", + "version": "2.3.1", "source": { "type": "git", "url": "https://github.com/spatie/temporary-directory.git", - "reference": "580eddfe9a0a41a902cac6eeb8f066b42e65a32b" + "reference": "662e481d6ec07ef29fd05010433428851a42cd07" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/spatie/temporary-directory/zipball/580eddfe9a0a41a902cac6eeb8f066b42e65a32b", - "reference": "580eddfe9a0a41a902cac6eeb8f066b42e65a32b", + "url": "https://api.github.com/repos/spatie/temporary-directory/zipball/662e481d6ec07ef29fd05010433428851a42cd07", + "reference": "662e481d6ec07ef29fd05010433428851a42cd07", "shasum": "" }, "require": { @@ -4880,7 +5166,7 @@ ], "support": { "issues": "https://github.com/spatie/temporary-directory/issues", - "source": "https://github.com/spatie/temporary-directory/tree/2.3.0" + "source": "https://github.com/spatie/temporary-directory/tree/2.3.1" }, "funding": [ { @@ -4892,26 +5178,91 @@ "type": "github" } ], - "time": "2025-01-13T13:04:43+00:00" + "time": "2026-01-12T07:42:22+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" @@ -4950,7 +5301,7 @@ "time" ], "support": { - "source": "https://github.com/symfony/clock/tree/v7.3.0" + "source": "https://github.com/symfony/clock/tree/v8.0.0" }, "funding": [ { @@ -4961,25 +5312,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.2", + "version": "v7.4.3", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "5f360ebc65c55265a74d23d7fe27f957870158a1" + "reference": "732a9ca6cd9dfd940c639062d5edbde2f6727fb6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/5f360ebc65c55265a74d23d7fe27f957870158a1", - "reference": "5f360ebc65c55265a74d23d7fe27f957870158a1", + "url": "https://api.github.com/repos/symfony/console/zipball/732a9ca6cd9dfd940c639062d5edbde2f6727fb6", + "reference": "732a9ca6cd9dfd940c639062d5edbde2f6727fb6", "shasum": "" }, "require": { @@ -4987,7 +5342,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", @@ -5001,16 +5356,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": { @@ -5044,7 +5399,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v7.3.2" + "source": "https://github.com/symfony/console/tree/v7.4.3" }, "funding": [ { @@ -5064,24 +5419,24 @@ "type": "tidelift" } ], - "time": "2025-07-30T17:13:41+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": { @@ -5113,7 +5468,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": [ { @@ -5124,12 +5479,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", @@ -5200,32 +5559,33 @@ }, { "name": "symfony/error-handler", - "version": "v7.3.2", + "version": "v7.4.0", "source": { "type": "git", "url": "https://github.com/symfony/error-handler.git", - "reference": "0b31a944fcd8759ae294da4d2808cbc53aebd0c3" + "reference": "48be2b0653594eea32dcef130cca1c811dcf25c2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/error-handler/zipball/0b31a944fcd8759ae294da4d2808cbc53aebd0c3", - "reference": "0b31a944fcd8759ae294da4d2808cbc53aebd0c3", + "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": [ @@ -5257,7 +5617,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.2" + "source": "https://github.com/symfony/error-handler/tree/v7.4.0" }, "funding": [ { @@ -5277,28 +5637,28 @@ "type": "tidelift" } ], - "time": "2025-07-07T08:17:57+00:00" + "time": "2025-11-05T14:29:59+00:00" }, { "name": "symfony/event-dispatcher", - "version": "v7.3.0", + "version": "v8.0.0", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "497f73ac996a598c92409b44ac43b6690c4f666d" + "reference": "573f95783a2ec6e38752979db139f09fec033f03" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/497f73ac996a598c92409b44ac43b6690c4f666d", - "reference": "497f73ac996a598c92409b44ac43b6690c4f666d", + "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": { @@ -5307,13 +5667,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": { @@ -5341,7 +5702,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.0" + "source": "https://github.com/symfony/event-dispatcher/tree/v8.0.0" }, "funding": [ { @@ -5352,12 +5713,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": "2025-04-22T09:11:45+00:00" + "time": "2025-10-30T14:17:19+00:00" }, { "name": "symfony/event-dispatcher-contracts", @@ -5436,24 +5801,94 @@ "time": "2024-09-25T14:21:43+00:00" }, { - "name": "symfony/finder", - "version": "v7.3.2", + "name": "symfony/filesystem", + "version": "v8.0.1", "source": { "type": "git", - "url": "https://github.com/symfony/finder.git", - "reference": "2a6614966ba1074fa93dae0bc804227422df4dfe" + "url": "https://github.com/symfony/filesystem.git", + "reference": "d937d400b980523dc9ee946bb69972b5e619058d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/2a6614966ba1074fa93dae0bc804227422df4dfe", - "reference": "2a6614966ba1074fa93dae0bc804227422df4dfe", + "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": { @@ -5481,7 +5916,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.2" + "source": "https://github.com/symfony/finder/tree/v7.4.3" }, "funding": [ { @@ -5501,27 +5936,26 @@ "type": "tidelift" } ], - "time": "2025-07-15T13:41:35+00:00" + "time": "2025-12-23T14:50:43+00:00" }, { "name": "symfony/http-foundation", - "version": "v7.3.2", + "version": "v7.4.3", "source": { "type": "git", "url": "https://github.com/symfony/http-foundation.git", - "reference": "6877c122b3a6cc3695849622720054f6e6fa5fa6" + "reference": "a70c745d4cea48dbd609f4075e5f5cbce453bd52" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-foundation/zipball/6877c122b3a6cc3695849622720054f6e6fa5fa6", - "reference": "6877c122b3a6cc3695849622720054f6e6fa5fa6", + "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", @@ -5530,13 +5964,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": { @@ -5564,7 +5998,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.2" + "source": "https://github.com/symfony/http-foundation/tree/v7.4.3" }, "funding": [ { @@ -5584,29 +6018,29 @@ "type": "tidelift" } ], - "time": "2025-07-10T08:47:49+00:00" + "time": "2025-12-23T14:23:49+00:00" }, { "name": "symfony/http-kernel", - "version": "v7.3.2", + "version": "v7.4.3", "source": { "type": "git", "url": "https://github.com/symfony/http-kernel.git", - "reference": "6ecc895559ec0097e221ed2fd5eb44d5fede083c" + "reference": "885211d4bed3f857b8c964011923528a55702aa5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-kernel/zipball/6ecc895559ec0097e221ed2fd5eb44d5fede083c", - "reference": "6ecc895559ec0097e221ed2fd5eb44d5fede083c", + "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": { @@ -5616,6 +6050,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", @@ -5633,27 +6068,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", @@ -5682,7 +6117,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.2" + "source": "https://github.com/symfony/http-kernel/tree/v7.4.3" }, "funding": [ { @@ -5702,20 +6137,20 @@ "type": "tidelift" } ], - "time": "2025-07-31T10:45:04+00:00" + "time": "2025-12-31T08:43:57+00:00" }, { "name": "symfony/mailer", - "version": "v7.3.2", + "version": "v7.4.3", "source": { "type": "git", "url": "https://github.com/symfony/mailer.git", - "reference": "d43e84d9522345f96ad6283d5dfccc8c1cfc299b" + "reference": "e472d35e230108231ccb7f51eb6b2100cac02ee4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/mailer/zipball/d43e84d9522345f96ad6283d5dfccc8c1cfc299b", - "reference": "d43e84d9522345f96ad6283d5dfccc8c1cfc299b", + "url": "https://api.github.com/repos/symfony/mailer/zipball/e472d35e230108231ccb7f51eb6b2100cac02ee4", + "reference": "e472d35e230108231ccb7f51eb6b2100cac02ee4", "shasum": "" }, "require": { @@ -5723,8 +6158,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": { @@ -5735,10 +6170,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": { @@ -5766,7 +6201,7 @@ "description": "Helps sending emails", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/mailer/tree/v7.3.2" + "source": "https://github.com/symfony/mailer/tree/v7.4.3" }, "funding": [ { @@ -5786,24 +6221,25 @@ "type": "tidelift" } ], - "time": "2025-07-15T11:36:08+00:00" + "time": "2025-12-16T08:02:06+00:00" }, { "name": "symfony/mime", - "version": "v7.3.2", + "version": "v7.4.0", "source": { "type": "git", "url": "https://github.com/symfony/mime.git", - "reference": "e0a0f859148daf1edf6c60b398eb40bfc96697d1" + "reference": "bdb02729471be5d047a3ac4a69068748f1a6be7a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/mime/zipball/e0a0f859148daf1edf6c60b398eb40bfc96697d1", - "reference": "e0a0f859148daf1edf6c60b398eb40bfc96697d1", + "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" }, @@ -5818,11 +6254,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": { @@ -5854,7 +6290,7 @@ "mime-type" ], "support": { - "source": "https://github.com/symfony/mime/tree/v7.3.2" + "source": "https://github.com/symfony/mime/tree/v7.4.0" }, "funding": [ { @@ -5874,11 +6310,11 @@ "type": "tidelift" } ], - "time": "2025-07-15T13:41:35+00:00" + "time": "2025-11-16T10:14:42+00:00" }, { "name": "symfony/polyfill-ctype", - "version": "v1.32.0", + "version": "v1.33.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-ctype.git", @@ -5937,7 +6373,7 @@ "portable" ], "support": { - "source": "https://github.com/symfony/polyfill-ctype/tree/v1.32.0" + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.33.0" }, "funding": [ { @@ -5948,6 +6384,10 @@ "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" @@ -5957,16 +6397,16 @@ }, { "name": "symfony/polyfill-intl-grapheme", - "version": "v1.32.0", + "version": "v1.33.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-grapheme.git", - "reference": "b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe" + "reference": "380872130d3a5dd3ace2f4010d95125fde5d5c70" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe", - "reference": "b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/380872130d3a5dd3ace2f4010d95125fde5d5c70", + "reference": "380872130d3a5dd3ace2f4010d95125fde5d5c70", "shasum": "" }, "require": { @@ -6015,7 +6455,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.32.0" + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.33.0" }, "funding": [ { @@ -6026,16 +6466,20 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-09-09T11:45:10+00:00" + "time": "2025-06-27T09:58:17+00:00" }, { "name": "symfony/polyfill-intl-idn", - "version": "v1.32.0", + "version": "v1.33.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-idn.git", @@ -6098,7 +6542,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.32.0" + "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.33.0" }, "funding": [ { @@ -6109,6 +6553,10 @@ "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" @@ -6118,7 +6566,7 @@ }, { "name": "symfony/polyfill-intl-normalizer", - "version": "v1.32.0", + "version": "v1.33.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-normalizer.git", @@ -6179,7 +6627,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.32.0" + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.33.0" }, "funding": [ { @@ -6190,6 +6638,10 @@ "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" @@ -6199,7 +6651,7 @@ }, { "name": "symfony/polyfill-mbstring", - "version": "v1.32.0", + "version": "v1.33.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", @@ -6260,7 +6712,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.32.0" + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.33.0" }, "funding": [ { @@ -6271,6 +6723,10 @@ "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" @@ -6280,7 +6736,7 @@ }, { "name": "symfony/polyfill-php80", - "version": "v1.32.0", + "version": "v1.33.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php80.git", @@ -6340,7 +6796,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php80/tree/v1.32.0" + "source": "https://github.com/symfony/polyfill-php80/tree/v1.33.0" }, "funding": [ { @@ -6351,6 +6807,10 @@ "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" @@ -6360,16 +6820,16 @@ }, { "name": "symfony/polyfill-php83", - "version": "v1.32.0", + "version": "v1.33.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php83.git", - "reference": "2fb86d65e2d424369ad2905e83b236a8805ba491" + "reference": "17f6f9a6b1735c0f163024d959f700cfbc5155e5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/2fb86d65e2d424369ad2905e83b236a8805ba491", - "reference": "2fb86d65e2d424369ad2905e83b236a8805ba491", + "url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/17f6f9a6b1735c0f163024d959f700cfbc5155e5", + "reference": "17f6f9a6b1735c0f163024d959f700cfbc5155e5", "shasum": "" }, "require": { @@ -6416,7 +6876,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php83/tree/v1.32.0" + "source": "https://github.com/symfony/polyfill-php83/tree/v1.33.0" }, "funding": [ { @@ -6427,25 +6887,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-09T11:45:10+00:00" + "time": "2025-07-08T02:45:35+00:00" }, { "name": "symfony/polyfill-php84", - "version": "v1.32.0", + "version": "v1.33.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php84.git", - "reference": "000df7860439609837bbe28670b0be15783b7fbf" + "reference": "d8ced4d875142b6a7426000426b8abc631d6b191" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php84/zipball/000df7860439609837bbe28670b0be15783b7fbf", - "reference": "000df7860439609837bbe28670b0be15783b7fbf", + "url": "https://api.github.com/repos/symfony/polyfill-php84/zipball/d8ced4d875142b6a7426000426b8abc631d6b191", + "reference": "d8ced4d875142b6a7426000426b8abc631d6b191", "shasum": "" }, "require": { @@ -6492,7 +6956,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php84/tree/v1.32.0" + "source": "https://github.com/symfony/polyfill-php84/tree/v1.33.0" }, "funding": [ { @@ -6503,25 +6967,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-02-20T12:04:08+00:00" + "time": "2025-06-24T13:30:11+00:00" }, { "name": "symfony/polyfill-php85", - "version": "v1.32.0", + "version": "v1.33.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php85.git", - "reference": "6fedf31ce4e3648f4ff5ca58bfd53127d38f05fd" + "reference": "d4e5fcd4ab3d998ab16c0db48e6cbb9a01993f91" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php85/zipball/6fedf31ce4e3648f4ff5ca58bfd53127d38f05fd", - "reference": "6fedf31ce4e3648f4ff5ca58bfd53127d38f05fd", + "url": "https://api.github.com/repos/symfony/polyfill-php85/zipball/d4e5fcd4ab3d998ab16c0db48e6cbb9a01993f91", + "reference": "d4e5fcd4ab3d998ab16c0db48e6cbb9a01993f91", "shasum": "" }, "require": { @@ -6568,7 +7036,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php85/tree/v1.32.0" + "source": "https://github.com/symfony/polyfill-php85/tree/v1.33.0" }, "funding": [ { @@ -6579,16 +7047,20 @@ "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-05-02T08:40:52+00:00" + "time": "2025-06-23T16:12:55+00:00" }, { "name": "symfony/polyfill-uuid", - "version": "v1.32.0", + "version": "v1.33.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-uuid.git", @@ -6647,7 +7119,7 @@ "uuid" ], "support": { - "source": "https://github.com/symfony/polyfill-uuid/tree/v1.32.0" + "source": "https://github.com/symfony/polyfill-uuid/tree/v1.33.0" }, "funding": [ { @@ -6658,6 +7130,10 @@ "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" @@ -6667,16 +7143,16 @@ }, { "name": "symfony/process", - "version": "v7.3.0", + "version": "v7.4.3", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "40c295f2deb408d5e9d2d32b8ba1dd61e36f05af" + "reference": "2f8e1a6cdf590ca63715da4d3a7a3327404a523f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/40c295f2deb408d5e9d2d32b8ba1dd61e36f05af", - "reference": "40c295f2deb408d5e9d2d32b8ba1dd61e36f05af", + "url": "https://api.github.com/repos/symfony/process/zipball/2f8e1a6cdf590ca63715da4d3a7a3327404a523f", + "reference": "2f8e1a6cdf590ca63715da4d3a7a3327404a523f", "shasum": "" }, "require": { @@ -6708,7 +7184,7 @@ "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/process/tree/v7.3.0" + "source": "https://github.com/symfony/process/tree/v7.4.3" }, "funding": [ { @@ -6719,25 +7195,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-04-17T09:11:12+00:00" + "time": "2025-12-19T10:00:43+00:00" }, { "name": "symfony/routing", - "version": "v7.3.2", + "version": "v7.4.3", "source": { "type": "git", "url": "https://github.com/symfony/routing.git", - "reference": "7614b8ca5fa89b9cd233e21b627bfc5774f586e4" + "reference": "5d3fd7adf8896c2fdb54e2f0f35b1bcbd9e45090" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/routing/zipball/7614b8ca5fa89b9cd233e21b627bfc5774f586e4", - "reference": "7614b8ca5fa89b9cd233e21b627bfc5774f586e4", + "url": "https://api.github.com/repos/symfony/routing/zipball/5d3fd7adf8896c2fdb54e2f0f35b1bcbd9e45090", + "reference": "5d3fd7adf8896c2fdb54e2f0f35b1bcbd9e45090", "shasum": "" }, "require": { @@ -6751,11 +7231,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": { @@ -6789,7 +7269,7 @@ "url" ], "support": { - "source": "https://github.com/symfony/routing/tree/v7.3.2" + "source": "https://github.com/symfony/routing/tree/v7.4.3" }, "funding": [ { @@ -6809,20 +7289,20 @@ "type": "tidelift" } ], - "time": "2025-07-15T11:36:08+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": { @@ -6876,7 +7356,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": [ { @@ -6887,44 +7367,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.2", + "version": "v8.0.1", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "42f505aff654e62ac7ac2ce21033818297ca89ca" + "reference": "ba65a969ac918ce0cc3edfac6cdde847eba231dc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/42f505aff654e62ac7ac2ce21033818297ca89ca", - "reference": "42f505aff654e62ac7ac2ce21033818297ca89ca", + "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/error-handler": "^6.4|^7.0", - "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": { @@ -6963,7 +7446,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v7.3.2" + "source": "https://github.com/symfony/string/tree/v8.0.1" }, "funding": [ { @@ -6983,38 +7466,31 @@ "type": "tidelift" } ], - "time": "2025-07-10T08:47:49+00:00" + "time": "2025-12-01T09:13:36+00:00" }, { "name": "symfony/translation", - "version": "v7.3.2", + "version": "v8.0.3", "source": { "type": "git", "url": "https://github.com/symfony/translation.git", - "reference": "81b48f4daa96272efcce9c7a6c4b58e629df3c90" + "reference": "60a8f11f0e15c48f2cc47c4da53873bb5b62135d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/translation/zipball/81b48f4daa96272efcce9c7a6c4b58e629df3c90", - "reference": "81b48f4daa96272efcce9c7a6c4b58e629df3c90", + "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" @@ -7022,17 +7498,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": { @@ -7063,7 +7539,7 @@ "description": "Provides tools to internationalize your application", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/translation/tree/v7.3.2" + "source": "https://github.com/symfony/translation/tree/v8.0.3" }, "funding": [ { @@ -7083,20 +7559,20 @@ "type": "tidelift" } ], - "time": "2025-07-30T17:31:46+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": { @@ -7145,7 +7621,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": [ { @@ -7156,25 +7632,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": { @@ -7182,7 +7662,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": { @@ -7219,7 +7699,7 @@ "uuid" ], "support": { - "source": "https://github.com/symfony/uid/tree/v7.3.1" + "source": "https://github.com/symfony/uid/tree/v7.4.0" }, "funding": [ { @@ -7230,25 +7710,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.2", + "version": "v7.4.3", "source": { "type": "git", "url": "https://github.com/symfony/var-dumper.git", - "reference": "53205bea27450dc5c65377518b3275e126d45e75" + "reference": "7e99bebcb3f90d8721890f2963463280848cba92" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-dumper/zipball/53205bea27450dc5c65377518b3275e126d45e75", - "reference": "53205bea27450dc5c65377518b3275e126d45e75", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/7e99bebcb3f90d8721890f2963463280848cba92", + "reference": "7e99bebcb3f90d8721890f2963463280848cba92", "shasum": "" }, "require": { @@ -7260,10 +7744,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": [ @@ -7302,7 +7786,7 @@ "dump" ], "support": { - "source": "https://github.com/symfony/var-dumper/tree/v7.3.2" + "source": "https://github.com/symfony/var-dumper/tree/v7.4.3" }, "funding": [ { @@ -7322,20 +7806,20 @@ "type": "tidelift" } ], - "time": "2025-07-29T20:02:46+00:00" + "time": "2025-12-18T07:04:31+00:00" }, { "name": "symfony/var-exporter", - "version": "v7.3.2", + "version": "v7.4.0", "source": { "type": "git", "url": "https://github.com/symfony/var-exporter.git", - "reference": "05b3e90654c097817325d6abd284f7938b05f467" + "reference": "03a60f169c79a28513a78c967316fbc8bf17816f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-exporter/zipball/05b3e90654c097817325d6abd284f7938b05f467", - "reference": "05b3e90654c097817325d6abd284f7938b05f467", + "url": "https://api.github.com/repos/symfony/var-exporter/zipball/03a60f169c79a28513a78c967316fbc8bf17816f", + "reference": "03a60f169c79a28513a78c967316fbc8bf17816f", "shasum": "" }, "require": { @@ -7343,9 +7827,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": { @@ -7383,7 +7867,7 @@ "serialize" ], "support": { - "source": "https://github.com/symfony/var-exporter/tree/v7.3.2" + "source": "https://github.com/symfony/var-exporter/tree/v7.4.0" }, "funding": [ { @@ -7403,27 +7887,103 @@ "type": "tidelift" } ], - "time": "2025-07-10T08:47:49+00:00" + "time": "2025-09-11T10:15:23+00:00" }, { - "name": "tijsverkoyen/css-to-inline-styles", - "version": "v2.3.0", + "name": "symfony/yaml", + "version": "v7.4.1", "source": { "type": "git", - "url": "https://github.com/tijsverkoyen/CssToInlineStyles.git", - "reference": "0d72ac1c00084279c1816675284073c5a337c20d" + "url": "https://github.com/symfony/yaml.git", + "reference": "24dd4de28d2e3988b311751ac49e684d783e2345" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/tijsverkoyen/CssToInlineStyles/zipball/0d72ac1c00084279c1816675284073c5a337c20d", - "reference": "0d72ac1c00084279c1816675284073c5a337c20d", + "url": "https://api.github.com/repos/symfony/yaml/zipball/24dd4de28d2e3988b311751ac49e684d783e2345", + "reference": "24dd4de28d2e3988b311751ac49e684d783e2345", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-ctype": "^1.8" + }, + "conflict": { + "symfony/console": "<6.4" + }, + "require-dev": { + "symfony/console": "^6.4|^7.0|^8.0" + }, + "bin": [ + "Resources/bin/yaml-lint" + ], + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Yaml\\": "" + }, + "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": "Loads and dumps YAML files", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/yaml/tree/v7.4.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-04T18:11:45+00:00" + }, + { + "name": "tijsverkoyen/css-to-inline-styles", + "version": "v2.4.0", + "source": { + "type": "git", + "url": "https://github.com/tijsverkoyen/CssToInlineStyles.git", + "reference": "f0292ccf0ec75843d65027214426b6b163b48b41" + }, + "dist": { + "type": "zip", + "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", @@ -7456,32 +8016,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", @@ -7530,7 +8090,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": [ { @@ -7542,7 +8102,7 @@ "type": "tidelift" } ], - "time": "2025-04-30T23:37:27+00:00" + "time": "2025-12-27T19:49:13+00:00" }, { "name": "voku/portable-ascii", @@ -7618,82 +8178,24 @@ ], "time": "2024-11-21T01:49:47+00:00" }, - { - "name": "webmozart/assert", - "version": "1.11.0", - "source": { - "type": "git", - "url": "https://github.com/webmozarts/assert.git", - "reference": "11cb2199493b2f8a3b53e7f19068fc6aac760991" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/webmozarts/assert/zipball/11cb2199493b2f8a3b53e7f19068fc6aac760991", - "reference": "11cb2199493b2f8a3b53e7f19068fc6aac760991", - "shasum": "" - }, - "require": { - "ext-ctype": "*", - "php": "^7.2 || ^8.0" - }, - "conflict": { - "phpstan/phpstan": "<0.12.20", - "vimeo/psalm": "<4.6.1 || 4.6.2" - }, - "require-dev": { - "phpunit/phpunit": "^8.5.13" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.10-dev" - } - }, - "autoload": { - "psr-4": { - "Webmozart\\Assert\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Bernhard Schussek", - "email": "bschussek@gmail.com" - } - ], - "description": "Assertions to validate method input/output with nice error messages.", - "keywords": [ - "assert", - "check", - "validate" - ], - "support": { - "issues": "https://github.com/webmozarts/assert/issues", - "source": "https://github.com/webmozarts/assert/tree/1.11.0" - }, - "time": "2022-06-03T18:03:27+00:00" - }, { "name": "wnx/sidecar-browsershot", - "version": "v2.6.0", + "version": "v2.7.0", "source": { "type": "git", "url": "https://github.com/stefanzweifel/sidecar-browsershot.git", - "reference": "20c5a56c34298f7edb7334890e919c0521a7f467" + "reference": "e42a996c6fab4357919cd5e3f3fab33f019cdd80" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/stefanzweifel/sidecar-browsershot/zipball/20c5a56c34298f7edb7334890e919c0521a7f467", - "reference": "20c5a56c34298f7edb7334890e919c0521a7f467", + "url": "https://api.github.com/repos/stefanzweifel/sidecar-browsershot/zipball/e42a996c6fab4357919cd5e3f3fab33f019cdd80", + "reference": "e42a996c6fab4357919cd5e3f3fab33f019cdd80", "shasum": "" }, "require": { - "hammerstone/sidecar": "^0.4 || ^0.5 || ^0.6 || ^0.7", - "illuminate/contracts": "^10.0 || ^11.0 || ^12.0", - "php": "^8.2", + "hammerstone/sidecar": "^0.7", + "illuminate/contracts": "^12.0", + "php": "^8.4", "spatie/browsershot": "^4.0 || ^5.0", "spatie/laravel-package-tools": "^1.9.2" }, @@ -7702,15 +8204,15 @@ "laravel/pint": "^1.13", "league/flysystem-aws-s3-v3": "^1.0|^2.0|^3.0", "nunomaduro/collision": "^7.0|^8.0", - "orchestra/testbench": "^8.0|^9.0|^10.0", - "pestphp/pest": "^2.0|^3.0", - "pestphp/pest-plugin-laravel": "^2.0|^3.0", + "orchestra/testbench": "^10.0", + "pestphp/pest": "^3.0|^4.0", + "pestphp/pest-plugin-laravel": "^3.0|^4.0", "phpstan/extension-installer": "^1.1", - "phpstan/phpstan-deprecation-rules": "^1.0", - "phpstan/phpstan-phpunit": "^1.0", - "phpunit/phpunit": "^10 | ^11.0", + "phpstan/phpstan-deprecation-rules": "^1.0|^2.0", + "phpstan/phpstan-phpunit": "^1.0|^2.0", + "phpunit/phpunit": "^11.0 | ^12.0", "spatie/image": "^3.3", - "spatie/pixelmatch-php": "dev-main" + "spatie/pixelmatch-php": "^1.0" }, "type": "library", "extra": { @@ -7752,7 +8254,7 @@ ], "support": { "issues": "https://github.com/stefanzweifel/sidecar-browsershot/issues", - "source": "https://github.com/stefanzweifel/sidecar-browsershot/tree/v2.6.0" + "source": "https://github.com/stefanzweifel/sidecar-browsershot/tree/v2.7.0" }, "funding": [ { @@ -7760,22 +8262,22 @@ "type": "github" } ], - "time": "2025-05-08T06:40:32+00:00" + "time": "2025-11-22T08:49:08+00:00" } ], "packages-dev": [ { "name": "brianium/paratest", - "version": "v7.8.3", + "version": "v7.16.1", "source": { "type": "git", "url": "https://github.com/paratestphp/paratest.git", - "reference": "a585c346ddf1bec22e51e20b5387607905604a71" + "reference": "f0fdfd8e654e0d38bc2ba756a6cabe7be287390b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/paratestphp/paratest/zipball/a585c346ddf1bec22e51e20b5387607905604a71", - "reference": "a585c346ddf1bec22e51e20b5387607905604a71", + "url": "https://api.github.com/repos/paratestphp/paratest/zipball/f0fdfd8e654e0d38bc2ba756a6cabe7be287390b", + "reference": "f0fdfd8e654e0d38bc2ba756a6cabe7be287390b", "shasum": "" }, "require": { @@ -7783,27 +8285,27 @@ "ext-pcre": "*", "ext-reflection": "*", "ext-simplexml": "*", - "fidry/cpu-core-counter": "^1.2.0", - "jean85/pretty-package-versions": "^2.1.0", - "php": "~8.2.0 || ~8.3.0 || ~8.4.0", - "phpunit/php-code-coverage": "^11.0.9 || ^12.0.4", - "phpunit/php-file-iterator": "^5.1.0 || ^6", - "phpunit/php-timer": "^7.0.1 || ^8", - "phpunit/phpunit": "^11.5.11 || ^12.0.6", - "sebastian/environment": "^7.2.0 || ^8", - "symfony/console": "^6.4.17 || ^7.2.1", - "symfony/process": "^6.4.19 || ^7.2.4" + "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.5.2", + "phpunit/php-file-iterator": "^6", + "phpunit/php-timer": "^8", + "phpunit/phpunit": "^12.5.4", + "sebastian/environment": "^8.0.3", + "symfony/console": "^7.3.4 || ^8.0.0", + "symfony/process": "^7.3.4 || ^8.0.0" }, "require-dev": { - "doctrine/coding-standard": "^12.0.0", + "doctrine/coding-standard": "^14.0.0", + "ext-pcntl": "*", "ext-pcov": "*", "ext-posix": "*", - "phpstan/phpstan": "^2.1.6", - "phpstan/phpstan-deprecation-rules": "^2.0.1", - "phpstan/phpstan-phpunit": "^2.0.4", - "phpstan/phpstan-strict-rules": "^2.0.3", - "squizlabs/php_codesniffer": "^3.11.3", - "symfony/filesystem": "^6.4.13 || ^7.2.0" + "phpstan/phpstan": "^2.1.33", + "phpstan/phpstan-deprecation-rules": "^2.0.3", + "phpstan/phpstan-phpunit": "^2.0.11", + "phpstan/phpstan-strict-rules": "^2.0.7", + "symfony/filesystem": "^7.3.2 || ^8.0.0" }, "bin": [ "bin/paratest", @@ -7843,7 +8345,7 @@ ], "support": { "issues": "https://github.com/paratestphp/paratest/issues", - "source": "https://github.com/paratestphp/paratest/tree/v7.8.3" + "source": "https://github.com/paratestphp/paratest/tree/v7.16.1" }, "funding": [ { @@ -7855,7 +8357,7 @@ "type": "paypal" } ], - "time": "2025-03-05T08:29:11+00:00" + "time": "2026-01-08T07:23:06+00:00" }, { "name": "doctrine/deprecations", @@ -8254,16 +8756,16 @@ }, { "name": "larastan/larastan", - "version": "v3.6.0", + "version": "v3.8.1", "source": { "type": "git", "url": "https://github.com/larastan/larastan.git", - "reference": "6431d010dd383a9279eb8874a76ddb571738564a" + "reference": "ff3725291bc4c7e6032b5a54776e3e5104c86db9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/larastan/larastan/zipball/6431d010dd383a9279eb8874a76ddb571738564a", - "reference": "6431d010dd383a9279eb8874a76ddb571738564a", + "url": "https://api.github.com/repos/larastan/larastan/zipball/ff3725291bc4c7e6032b5a54776e3e5104c86db9", + "reference": "ff3725291bc4c7e6032b5a54776e3e5104c86db9", "shasum": "" }, "require": { @@ -8277,7 +8779,7 @@ "illuminate/pipeline": "^11.44.2 || ^12.4.1", "illuminate/support": "^11.44.2 || ^12.4.1", "php": "^8.2", - "phpstan/phpstan": "^2.1.11" + "phpstan/phpstan": "^2.1.32" }, "require-dev": { "doctrine/coding-standard": "^13", @@ -8290,7 +8792,8 @@ "phpunit/phpunit": "^10.5.35 || ^11.5.15" }, "suggest": { - "orchestra/testbench": "Using Larastan for analysing a package needs Testbench" + "orchestra/testbench": "Using Larastan for analysing a package needs Testbench", + "phpmyadmin/sql-parser": "Install to enable Larastan's optional phpMyAdmin-based SQL parser automatically" }, "type": "phpstan-extension", "extra": { @@ -8331,7 +8834,7 @@ ], "support": { "issues": "https://github.com/larastan/larastan/issues", - "source": "https://github.com/larastan/larastan/tree/v3.6.0" + "source": "https://github.com/larastan/larastan/tree/v3.8.1" }, "funding": [ { @@ -8339,39 +8842,40 @@ "type": "github" } ], - "time": "2025-07-11T06:52:52+00:00" + "time": "2025-12-11T16:37:35+00:00" }, { "name": "laravel/boost", - "version": "v1.0.17", + "version": "v1.8.9", "source": { "type": "git", "url": "https://github.com/laravel/boost.git", - "reference": "3d1121561f793d027b76cb02b4ef9e654f8870fb" + "reference": "1f2c2d41b5216618170fb6730ec13bf894c5bffd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/boost/zipball/3d1121561f793d027b76cb02b4ef9e654f8870fb", - "reference": "3d1121561f793d027b76cb02b4ef9e654f8870fb", + "url": "https://api.github.com/repos/laravel/boost/zipball/1f2c2d41b5216618170fb6730ec13bf894c5bffd", + "reference": "1f2c2d41b5216618170fb6730ec13bf894c5bffd", "shasum": "" }, "require": { "guzzlehttp/guzzle": "^7.9", - "illuminate/console": "^10.0|^11.0|^12.0", - "illuminate/contracts": "^10.0|^11.0|^12.0", - "illuminate/routing": "^10.0|^11.0|^12.0", - "illuminate/support": "^10.0|^11.0|^12.0", - "laravel/mcp": "^0.1.0", - "laravel/prompts": "^0.1.9|^0.3", - "laravel/roster": "^0.2", - "php": "^8.1|^8.2" + "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.14|^1.23", - "mockery/mockery": "^1.6", - "orchestra/testbench": "^8.22.0|^9.0|^10.0", - "pestphp/pest": "^2.0|^3.0", - "phpstan/phpstan": "^2.0" + "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|^4.1.5", + "phpstan/phpstan": "^2.1.27", + "rector/rector": "^2.1" }, "type": "library", "extra": { @@ -8393,7 +8897,7 @@ "license": [ "MIT" ], - "description": "Laravel Boost accelerates AI-assisted development to generate high-quality, Laravel-specific code.", + "description": "Laravel Boost accelerates AI-assisted development by providing the essential context and structure that AI needs to generate high-quality, Laravel-specific code.", "homepage": "https://github.com/laravel/boost", "keywords": [ "ai", @@ -8404,35 +8908,41 @@ "issues": "https://github.com/laravel/boost/issues", "source": "https://github.com/laravel/boost" }, - "time": "2025-08-14T17:31:57+00:00" + "time": "2026-01-07T18:43:11+00:00" }, { "name": "laravel/mcp", - "version": "v0.1.0", + "version": "v0.5.2", "source": { "type": "git", "url": "https://github.com/laravel/mcp.git", - "reference": "417890c0d8032af9a46a86d16651bbe13946cddf" + "reference": "b9bdd8d6f8b547c8733fe6826b1819341597ba3c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/mcp/zipball/417890c0d8032af9a46a86d16651bbe13946cddf", - "reference": "417890c0d8032af9a46a86d16651bbe13946cddf", + "url": "https://api.github.com/repos/laravel/mcp/zipball/b9bdd8d6f8b547c8733fe6826b1819341597ba3c", + "reference": "b9bdd8d6f8b547c8733fe6826b1819341597ba3c", "shasum": "" }, "require": { - "illuminate/console": "^10.0|^11.0|^12.0", - "illuminate/contracts": "^10.0|^11.0|^12.0", - "illuminate/http": "^10.0|^11.0|^12.0", - "illuminate/routing": "^10.0|^11.0|^12.0", - "illuminate/support": "^10.0|^11.0|^12.0", - "illuminate/validation": "^10.0|^11.0|^12.0", - "php": "^8.1|^8.2" + "ext-json": "*", + "ext-mbstring": "*", + "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.14", - "orchestra/testbench": "^8.22.0|^9.0|^10.0", - "phpstan/phpstan": "^2.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" }, "type": "library", "extra": { @@ -8448,8 +8958,6 @@ "autoload": { "psr-4": { "Laravel\\Mcp\\": "src/", - "Workbench\\App\\": "workbench/app/", - "Laravel\\Mcp\\Tests\\": "tests/", "Laravel\\Mcp\\Server\\": "src/Server/" } }, @@ -8457,10 +8965,15 @@ "license": [ "MIT" ], - "description": "The easiest way to add MCP servers to your Laravel app.", + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "Rapidly build MCP servers for your Laravel applications.", "homepage": "https://github.com/laravel/mcp", "keywords": [ - "dev", "laravel", "mcp" ], @@ -8468,20 +8981,20 @@ "issues": "https://github.com/laravel/mcp/issues", "source": "https://github.com/laravel/mcp" }, - "time": "2025-08-12T07:09:39+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": { @@ -8498,9 +9011,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" }, @@ -8547,20 +9060,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.24.0", + "version": "v1.27.0", "source": { "type": "git", "url": "https://github.com/laravel/pint.git", - "reference": "0345f3b05f136801af8c339f9d16ef29e6b4df8a" + "reference": "c67b4195b75491e4dfc6b00b1c78b68d86f54c90" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/pint/zipball/0345f3b05f136801af8c339f9d16ef29e6b4df8a", - "reference": "0345f3b05f136801af8c339f9d16ef29e6b4df8a", + "url": "https://api.github.com/repos/laravel/pint/zipball/c67b4195b75491e4dfc6b00b1c78b68d86f54c90", + "reference": "c67b4195b75491e4dfc6b00b1c78b68d86f54c90", "shasum": "" }, "require": { @@ -8571,22 +9084,19 @@ "php": "^8.2.0" }, "require-dev": { - "friendsofphp/php-cs-fixer": "^3.82.2", - "illuminate/view": "^11.45.1", - "larastan/larastan": "^3.5.0", - "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" ], "type": "project", "autoload": { - "files": [ - "overrides/Runner/Parallel/ProcessFactory.php" - ], "psr-4": { "App\\": "app/", "Database\\Seeders\\": "database/seeders/", @@ -8606,6 +9116,7 @@ "description": "An opinionated code formatter for PHP.", "homepage": "https://laravel.com", "keywords": [ + "dev", "format", "formatter", "lint", @@ -8616,20 +9127,20 @@ "issues": "https://github.com/laravel/pint/issues", "source": "https://github.com/laravel/pint" }, - "time": "2025-07-10T18:09:32+00:00" + "time": "2026-01-05T16:49:17+00:00" }, { "name": "laravel/roster", - "version": "v0.2.3", + "version": "v0.2.9", "source": { "type": "git", "url": "https://github.com/laravel/roster.git", - "reference": "caeed7609b02c00c3f1efec52812d8d87c5d4096" + "reference": "82bbd0e2de614906811aebdf16b4305956816fa6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/roster/zipball/caeed7609b02c00c3f1efec52812d8d87c5d4096", - "reference": "caeed7609b02c00c3f1efec52812d8d87c5d4096", + "url": "https://api.github.com/repos/laravel/roster/zipball/82bbd0e2de614906811aebdf16b4305956816fa6", + "reference": "82bbd0e2de614906811aebdf16b4305956816fa6", "shasum": "" }, "require": { @@ -8677,20 +9188,20 @@ "issues": "https://github.com/laravel/roster/issues", "source": "https://github.com/laravel/roster" }, - "time": "2025-08-13T15:00:25+00:00" + "time": "2025-10-20T09:56:46+00:00" }, { "name": "laravel/sail", - "version": "v1.44.0", + "version": "v1.52.0", "source": { "type": "git", "url": "https://github.com/laravel/sail.git", - "reference": "a09097bd2a8a38e23ac472fa6a6cf5b0d1c1d3fe" + "reference": "64ac7d8abb2dbcf2b76e61289451bae79066b0b3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/sail/zipball/a09097bd2a8a38e23ac472fa6a6cf5b0d1c1d3fe", - "reference": "a09097bd2a8a38e23ac472fa6a6cf5b0d1c1d3fe", + "url": "https://api.github.com/repos/laravel/sail/zipball/64ac7d8abb2dbcf2b76e61289451bae79066b0b3", + "reference": "64ac7d8abb2dbcf2b76e61289451bae79066b0b3", "shasum": "" }, "require": { @@ -8703,7 +9214,7 @@ }, "require-dev": { "orchestra/testbench": "^7.0|^8.0|^9.0|^10.0", - "phpstan/phpstan": "^1.10" + "phpstan/phpstan": "^2.0" }, "bin": [ "bin/sail" @@ -8740,7 +9251,7 @@ "issues": "https://github.com/laravel/sail/issues", "source": "https://github.com/laravel/sail" }, - "time": "2025-07-04T16:17:06+00:00" + "time": "2026-01-01T02:46:03+00:00" }, { "name": "mockery/mockery", @@ -8887,16 +9398,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": { @@ -8918,7 +9429,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", @@ -8982,42 +9493,45 @@ "type": "patreon" } ], - "time": "2025-06-25T02:12:12+00:00" + "time": "2025-11-20T02:55:25+00:00" }, { "name": "pestphp/pest", - "version": "v3.8.2", + "version": "v4.3.1", "source": { "type": "git", "url": "https://github.com/pestphp/pest.git", - "reference": "c6244a8712968dbac88eb998e7ff3b5caa556b0d" + "reference": "bc57a84e77afd4544ff9643a6858f68d05aeab96" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/pestphp/pest/zipball/c6244a8712968dbac88eb998e7ff3b5caa556b0d", - "reference": "c6244a8712968dbac88eb998e7ff3b5caa556b0d", + "url": "https://api.github.com/repos/pestphp/pest/zipball/bc57a84e77afd4544ff9643a6858f68d05aeab96", + "reference": "bc57a84e77afd4544ff9643a6858f68d05aeab96", "shasum": "" }, "require": { - "brianium/paratest": "^7.8.3", - "nunomaduro/collision": "^8.8.0", - "nunomaduro/termwind": "^2.3.0", - "pestphp/pest-plugin": "^3.0.0", - "pestphp/pest-plugin-arch": "^3.1.0", - "pestphp/pest-plugin-mutate": "^3.0.5", - "php": "^8.2.0", - "phpunit/phpunit": "^11.5.15" + "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.2.1", + "php": "^8.3.0", + "phpunit/phpunit": "^12.5.4", + "symfony/process": "^7.4.3|^8.0.0" }, "conflict": { - "filp/whoops": "<2.16.0", - "phpunit/phpunit": ">11.5.15", - "sebastian/exporter": "<6.0.0", + "filp/whoops": "<2.18.3", + "phpunit/phpunit": ">12.5.4", + "sebastian/exporter": "<7.0.0", "webmozart/assert": "<1.11.0" }, "require-dev": { - "pestphp/pest-dev-tools": "^3.4.0", - "pestphp/pest-plugin-type-coverage": "^3.5.0", - "symfony/process": "^7.2.5" + "pestphp/pest-dev-tools": "^4.0.0", + "pestphp/pest-plugin-browser": "^4.1.1", + "pestphp/pest-plugin-type-coverage": "^4.0.3", + "psy/psysh": "^0.12.18" }, "bin": [ "bin/pest" @@ -9043,6 +9557,7 @@ "Pest\\Plugins\\Snapshot", "Pest\\Plugins\\Verbose", "Pest\\Plugins\\Version", + "Pest\\Plugins\\Shard", "Pest\\Plugins\\Parallel" ] }, @@ -9082,7 +9597,7 @@ ], "support": { "issues": "https://github.com/pestphp/pest/issues", - "source": "https://github.com/pestphp/pest/tree/v3.8.2" + "source": "https://github.com/pestphp/pest/tree/v4.3.1" }, "funding": [ { @@ -9094,34 +9609,34 @@ "type": "github" } ], - "time": "2025-04-17T10:53:02+00:00" + "time": "2026-01-04T16:29:59+00:00" }, { "name": "pestphp/pest-plugin", - "version": "v3.0.0", + "version": "v4.0.0", "source": { "type": "git", "url": "https://github.com/pestphp/pest-plugin.git", - "reference": "e79b26c65bc11c41093b10150c1341cc5cdbea83" + "reference": "9d4b93d7f73d3f9c3189bb22c220fef271cdf568" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/pestphp/pest-plugin/zipball/e79b26c65bc11c41093b10150c1341cc5cdbea83", - "reference": "e79b26c65bc11c41093b10150c1341cc5cdbea83", + "url": "https://api.github.com/repos/pestphp/pest-plugin/zipball/9d4b93d7f73d3f9c3189bb22c220fef271cdf568", + "reference": "9d4b93d7f73d3f9c3189bb22c220fef271cdf568", "shasum": "" }, "require": { "composer-plugin-api": "^2.0.0", "composer-runtime-api": "^2.2.2", - "php": "^8.2" + "php": "^8.3" }, "conflict": { - "pestphp/pest": "<3.0.0" + "pestphp/pest": "<4.0.0" }, "require-dev": { - "composer/composer": "^2.7.9", - "pestphp/pest": "^3.0.0", - "pestphp/pest-dev-tools": "^3.0.0" + "composer/composer": "^2.8.10", + "pestphp/pest": "^4.0.0", + "pestphp/pest-dev-tools": "^4.0.0" }, "type": "composer-plugin", "extra": { @@ -9148,7 +9663,7 @@ "unit" ], "support": { - "source": "https://github.com/pestphp/pest-plugin/tree/v3.0.0" + "source": "https://github.com/pestphp/pest-plugin/tree/v4.0.0" }, "funding": [ { @@ -9164,30 +9679,30 @@ "type": "patreon" } ], - "time": "2024-09-08T23:21:41+00:00" + "time": "2025-08-20T12:35:58+00:00" }, { "name": "pestphp/pest-plugin-arch", - "version": "v3.1.1", + "version": "v4.0.0", "source": { "type": "git", "url": "https://github.com/pestphp/pest-plugin-arch.git", - "reference": "db7bd9cb1612b223e16618d85475c6f63b9c8daa" + "reference": "25bb17e37920ccc35cbbcda3b00d596aadf3e58d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/pestphp/pest-plugin-arch/zipball/db7bd9cb1612b223e16618d85475c6f63b9c8daa", - "reference": "db7bd9cb1612b223e16618d85475c6f63b9c8daa", + "url": "https://api.github.com/repos/pestphp/pest-plugin-arch/zipball/25bb17e37920ccc35cbbcda3b00d596aadf3e58d", + "reference": "25bb17e37920ccc35cbbcda3b00d596aadf3e58d", "shasum": "" }, "require": { - "pestphp/pest-plugin": "^3.0.0", - "php": "^8.2", - "ta-tikoma/phpunit-architecture-test": "^0.8.4" + "pestphp/pest-plugin": "^4.0.0", + "php": "^8.3", + "ta-tikoma/phpunit-architecture-test": "^0.8.5" }, "require-dev": { - "pestphp/pest": "^3.8.1", - "pestphp/pest-dev-tools": "^3.4.0" + "pestphp/pest": "^4.0.0", + "pestphp/pest-dev-tools": "^4.0.0" }, "type": "library", "extra": { @@ -9222,7 +9737,7 @@ "unit" ], "support": { - "source": "https://github.com/pestphp/pest-plugin-arch/tree/v3.1.1" + "source": "https://github.com/pestphp/pest-plugin-arch/tree/v4.0.0" }, "funding": [ { @@ -9234,30 +9749,30 @@ "type": "github" } ], - "time": "2025-04-16T22:59:48+00:00" + "time": "2025-08-20T13:10:51+00:00" }, { "name": "pestphp/pest-plugin-drift", - "version": "v3.0.0", + "version": "v4.0.0", "source": { "type": "git", "url": "https://github.com/pestphp/pest-plugin-drift.git", - "reference": "cd506d2b931eb1443b878229b472c59d6f2d8ee8" + "reference": "f4972f2dc6e6e6f1b47db0a8472ca70ca42b622e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/pestphp/pest-plugin-drift/zipball/cd506d2b931eb1443b878229b472c59d6f2d8ee8", - "reference": "cd506d2b931eb1443b878229b472c59d6f2d8ee8", + "url": "https://api.github.com/repos/pestphp/pest-plugin-drift/zipball/f4972f2dc6e6e6f1b47db0a8472ca70ca42b622e", + "reference": "f4972f2dc6e6e6f1b47db0a8472ca70ca42b622e", "shasum": "" }, "require": { - "nikic/php-parser": "^5.1.0", - "pestphp/pest": "^3.0.0", - "php": "^8.2.0", - "symfony/finder": "^7.1.4" + "nikic/php-parser": "^5.6.1", + "pestphp/pest": "^4.0.0", + "php": "^8.3.0", + "symfony/finder": "^7.3.2" }, "require-dev": { - "pestphp/pest-dev-tools": "^3.0.0" + "pestphp/pest-dev-tools": "^4.0.0" }, "type": "library", "extra": { @@ -9287,7 +9802,7 @@ "unit" ], "support": { - "source": "https://github.com/pestphp/pest-plugin-drift/tree/v3.0.0" + "source": "https://github.com/pestphp/pest-plugin-drift/tree/v4.0.0" }, "funding": [ { @@ -9303,31 +9818,31 @@ "type": "github" } ], - "time": "2024-09-08T23:45:48+00:00" + "time": "2025-08-20T12:54:20+00:00" }, { "name": "pestphp/pest-plugin-laravel", - "version": "v3.2.0", + "version": "v4.0.0", "source": { "type": "git", "url": "https://github.com/pestphp/pest-plugin-laravel.git", - "reference": "6801be82fd92b96e82dd72e563e5674b1ce365fc" + "reference": "e12a07046b826a40b1c8632fd7b80d6b8d7b628e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/pestphp/pest-plugin-laravel/zipball/6801be82fd92b96e82dd72e563e5674b1ce365fc", - "reference": "6801be82fd92b96e82dd72e563e5674b1ce365fc", + "url": "https://api.github.com/repos/pestphp/pest-plugin-laravel/zipball/e12a07046b826a40b1c8632fd7b80d6b8d7b628e", + "reference": "e12a07046b826a40b1c8632fd7b80d6b8d7b628e", "shasum": "" }, "require": { - "laravel/framework": "^11.39.1|^12.9.2", - "pestphp/pest": "^3.8.2", - "php": "^8.2.0" + "laravel/framework": "^11.45.2|^12.25.0", + "pestphp/pest": "^4.0.0", + "php": "^8.3.0" }, "require-dev": { - "laravel/dusk": "^8.2.13|dev-develop", - "orchestra/testbench": "^9.9.0|^10.2.1", - "pestphp/pest-dev-tools": "^3.4.0" + "laravel/dusk": "^8.3.3", + "orchestra/testbench": "^9.13.0|^10.5.0", + "pestphp/pest-dev-tools": "^4.0.0" }, "type": "library", "extra": { @@ -9365,7 +9880,7 @@ "unit" ], "support": { - "source": "https://github.com/pestphp/pest-plugin-laravel/tree/v3.2.0" + "source": "https://github.com/pestphp/pest-plugin-laravel/tree/v4.0.0" }, "funding": [ { @@ -9377,32 +9892,32 @@ "type": "github" } ], - "time": "2025-04-21T07:40:53+00:00" + "time": "2025-08-20T12:46:37+00:00" }, { "name": "pestphp/pest-plugin-mutate", - "version": "v3.0.5", + "version": "v4.0.1", "source": { "type": "git", "url": "https://github.com/pestphp/pest-plugin-mutate.git", - "reference": "e10dbdc98c9e2f3890095b4fe2144f63a5717e08" + "reference": "d9b32b60b2385e1688a68cc227594738ec26d96c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/pestphp/pest-plugin-mutate/zipball/e10dbdc98c9e2f3890095b4fe2144f63a5717e08", - "reference": "e10dbdc98c9e2f3890095b4fe2144f63a5717e08", + "url": "https://api.github.com/repos/pestphp/pest-plugin-mutate/zipball/d9b32b60b2385e1688a68cc227594738ec26d96c", + "reference": "d9b32b60b2385e1688a68cc227594738ec26d96c", "shasum": "" }, "require": { - "nikic/php-parser": "^5.2.0", - "pestphp/pest-plugin": "^3.0.0", - "php": "^8.2", + "nikic/php-parser": "^5.6.1", + "pestphp/pest-plugin": "^4.0.0", + "php": "^8.3", "psr/simple-cache": "^3.0.0" }, "require-dev": { - "pestphp/pest": "^3.0.8", - "pestphp/pest-dev-tools": "^3.0.0", - "pestphp/pest-plugin-type-coverage": "^3.0.0" + "pestphp/pest": "^4.0.0", + "pestphp/pest-dev-tools": "^4.0.0", + "pestphp/pest-plugin-type-coverage": "^4.0.0" }, "type": "library", "autoload": { @@ -9415,6 +9930,10 @@ "MIT" ], "authors": [ + { + "name": "Nuno Maduro", + "email": "enunomaduro@gmail.com" + }, { "name": "Sandro Gehri", "email": "sandrogehri@gmail.com" @@ -9433,7 +9952,7 @@ "unit" ], "support": { - "source": "https://github.com/pestphp/pest-plugin-mutate/tree/v3.0.5" + "source": "https://github.com/pestphp/pest-plugin-mutate/tree/v4.0.1" }, "funding": [ { @@ -9449,7 +9968,63 @@ "type": "github" } ], - "time": "2024-09-22T07:54:40+00:00" + "time": "2025-08-21T20:19:25+00:00" + }, + { + "name": "pestphp/pest-plugin-profanity", + "version": "v4.2.1", + "source": { + "type": "git", + "url": "https://github.com/pestphp/pest-plugin-profanity.git", + "reference": "343cfa6f3564b7e35df0ebb77b7fa97039f72b27" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/pestphp/pest-plugin-profanity/zipball/343cfa6f3564b7e35df0ebb77b7fa97039f72b27", + "reference": "343cfa6f3564b7e35df0ebb77b7fa97039f72b27", + "shasum": "" + }, + "require": { + "pestphp/pest-plugin": "^4.0.0", + "php": "^8.3" + }, + "require-dev": { + "faissaloux/pest-plugin-inside": "^1.9", + "pestphp/pest": "^4.0.0", + "pestphp/pest-dev-tools": "^4.0.0" + }, + "type": "library", + "extra": { + "pest": { + "plugins": [ + "Pest\\Profanity\\Plugin" + ] + } + }, + "autoload": { + "psr-4": { + "Pest\\Profanity\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "The Pest Profanity Plugin", + "keywords": [ + "framework", + "pest", + "php", + "plugin", + "profanity", + "test", + "testing", + "unit" + ], + "support": { + "source": "https://github.com/pestphp/pest-plugin-profanity/tree/v4.2.1" + }, + "time": "2025-12-08T00:13:17+00:00" }, { "name": "phar-io/manifest", @@ -9624,16 +10199,16 @@ }, { "name": "phpdocumentor/reflection-docblock", - "version": "5.6.2", + "version": "5.6.6", "source": { "type": "git", "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", - "reference": "92dde6a5919e34835c506ac8c523ef095a95ed62" + "reference": "5cee1d3dfc2d2aa6599834520911d246f656bcb8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/92dde6a5919e34835c506ac8c523ef095a95ed62", - "reference": "92dde6a5919e34835c506ac8c523ef095a95ed62", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/5cee1d3dfc2d2aa6599834520911d246f656bcb8", + "reference": "5cee1d3dfc2d2aa6599834520911d246f656bcb8", "shasum": "" }, "require": { @@ -9643,7 +10218,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", @@ -9682,22 +10257,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.2" + "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/5.6.6" }, - "time": "2025-04-13T19:20:35+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": { @@ -9740,22 +10315,22 @@ "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", - "version": "2.2.0", + "version": "2.3.1", "source": { "type": "git", "url": "https://github.com/phpstan/phpdoc-parser.git", - "reference": "b9e61a61e39e02dd90944e9115241c7f7e76bfd8" + "reference": "16dbf9937da8d4528ceb2145c9c7c0bd29e26374" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/b9e61a61e39e02dd90944e9115241c7f7e76bfd8", - "reference": "b9e61a61e39e02dd90944e9115241c7f7e76bfd8", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/16dbf9937da8d4528ceb2145c9c7c0bd29e26374", + "reference": "16dbf9937da8d4528ceb2145c9c7c0bd29e26374", "shasum": "" }, "require": { @@ -9787,22 +10362,17 @@ "description": "PHPDoc parser with support for nullable, intersection and generic types", "support": { "issues": "https://github.com/phpstan/phpdoc-parser/issues", - "source": "https://github.com/phpstan/phpdoc-parser/tree/2.2.0" + "source": "https://github.com/phpstan/phpdoc-parser/tree/2.3.1" }, - "time": "2025-07-13T07:04:09+00:00" + "time": "2026-01-12T11:33:04+00:00" }, { "name": "phpstan/phpstan", - "version": "2.1.22", - "source": { - "type": "git", - "url": "https://github.com/phpstan/phpstan.git", - "reference": "41600c8379eb5aee63e9413fe9e97273e25d57e4" - }, + "version": "2.1.33", "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/41600c8379eb5aee63e9413fe9e97273e25d57e4", - "reference": "41600c8379eb5aee63e9413fe9e97273e25d57e4", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/9e800e6bee7d5bd02784d4c6069b48032d16224f", + "reference": "9e800e6bee7d5bd02784d4c6069b48032d16224f", "shasum": "" }, "require": { @@ -9847,39 +10417,38 @@ "type": "github" } ], - "time": "2025-08-04T19:17:37+00:00" + "time": "2025-12-05T10:24:31+00:00" }, { "name": "phpunit/php-code-coverage", - "version": "11.0.10", + "version": "12.5.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "1a800a7446add2d79cc6b3c01c45381810367d76" + "reference": "4a9739b51cbcb355f6e95659612f92e282a7077b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/1a800a7446add2d79cc6b3c01c45381810367d76", - "reference": "1a800a7446add2d79cc6b3c01c45381810367d76", + "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.4.0", - "php": ">=8.2", - "phpunit/php-file-iterator": "^5.1.0", - "phpunit/php-text-template": "^4.0.1", - "sebastian/code-unit-reverse-lookup": "^4.0.1", - "sebastian/complexity": "^4.0.1", - "sebastian/environment": "^7.2.0", - "sebastian/lines-of-code": "^3.0.1", - "sebastian/version": "^5.0.2", - "theseer/tokenizer": "^1.2.3" + "nikic/php-parser": "^5.7.0", + "php": ">=8.3", + "phpunit/php-file-iterator": "^6.0", + "phpunit/php-text-template": "^5.0", + "sebastian/complexity": "^5.0", + "sebastian/environment": "^8.0.3", + "sebastian/lines-of-code": "^4.0", + "sebastian/version": "^6.0", + "theseer/tokenizer": "^2.0.1" }, "require-dev": { - "phpunit/phpunit": "^11.5.2" + "phpunit/phpunit": "^12.5.1" }, "suggest": { "ext-pcov": "PHP extension that provides line coverage", @@ -9888,7 +10457,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "11.0.x-dev" + "dev-main": "12.5.x-dev" } }, "autoload": { @@ -9917,7 +10486,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/show" + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/12.5.2" }, "funding": [ { @@ -9937,32 +10506,32 @@ "type": "tidelift" } ], - "time": "2025-06-18T08:56:18+00:00" + "time": "2025-12-24T07:03:04+00:00" }, { "name": "phpunit/php-file-iterator", - "version": "5.1.0", + "version": "6.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-file-iterator.git", - "reference": "118cfaaa8bc5aef3287bf315b6060b1174754af6" + "reference": "961bc913d42fe24a257bfff826a5068079ac7782" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/118cfaaa8bc5aef3287bf315b6060b1174754af6", - "reference": "118cfaaa8bc5aef3287bf315b6060b1174754af6", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/961bc913d42fe24a257bfff826a5068079ac7782", + "reference": "961bc913d42fe24a257bfff826a5068079ac7782", "shasum": "" }, "require": { - "php": ">=8.2" + "php": ">=8.3" }, "require-dev": { - "phpunit/phpunit": "^11.0" + "phpunit/phpunit": "^12.0" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "5.0-dev" + "dev-main": "6.0-dev" } }, "autoload": { @@ -9990,7 +10559,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", "security": "https://github.com/sebastianbergmann/php-file-iterator/security/policy", - "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/5.1.0" + "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/6.0.0" }, "funding": [ { @@ -9998,28 +10567,28 @@ "type": "github" } ], - "time": "2024-08-27T05:02:59+00:00" + "time": "2025-02-07T04:58:37+00:00" }, { "name": "phpunit/php-invoker", - "version": "5.0.1", + "version": "6.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-invoker.git", - "reference": "c1ca3814734c07492b3d4c5f794f4b0995333da2" + "reference": "12b54e689b07a25a9b41e57736dfab6ec9ae5406" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/c1ca3814734c07492b3d4c5f794f4b0995333da2", - "reference": "c1ca3814734c07492b3d4c5f794f4b0995333da2", + "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/12b54e689b07a25a9b41e57736dfab6ec9ae5406", + "reference": "12b54e689b07a25a9b41e57736dfab6ec9ae5406", "shasum": "" }, "require": { - "php": ">=8.2" + "php": ">=8.3" }, "require-dev": { "ext-pcntl": "*", - "phpunit/phpunit": "^11.0" + "phpunit/phpunit": "^12.0" }, "suggest": { "ext-pcntl": "*" @@ -10027,7 +10596,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "5.0-dev" + "dev-main": "6.0-dev" } }, "autoload": { @@ -10054,7 +10623,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/php-invoker/issues", "security": "https://github.com/sebastianbergmann/php-invoker/security/policy", - "source": "https://github.com/sebastianbergmann/php-invoker/tree/5.0.1" + "source": "https://github.com/sebastianbergmann/php-invoker/tree/6.0.0" }, "funding": [ { @@ -10062,32 +10631,32 @@ "type": "github" } ], - "time": "2024-07-03T05:07:44+00:00" + "time": "2025-02-07T04:58:58+00:00" }, { "name": "phpunit/php-text-template", - "version": "4.0.1", + "version": "5.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-text-template.git", - "reference": "3e0404dc6b300e6bf56415467ebcb3fe4f33e964" + "reference": "e1367a453f0eda562eedb4f659e13aa900d66c53" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/3e0404dc6b300e6bf56415467ebcb3fe4f33e964", - "reference": "3e0404dc6b300e6bf56415467ebcb3fe4f33e964", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/e1367a453f0eda562eedb4f659e13aa900d66c53", + "reference": "e1367a453f0eda562eedb4f659e13aa900d66c53", "shasum": "" }, "require": { - "php": ">=8.2" + "php": ">=8.3" }, "require-dev": { - "phpunit/phpunit": "^11.0" + "phpunit/phpunit": "^12.0" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "4.0-dev" + "dev-main": "5.0-dev" } }, "autoload": { @@ -10114,7 +10683,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/php-text-template/issues", "security": "https://github.com/sebastianbergmann/php-text-template/security/policy", - "source": "https://github.com/sebastianbergmann/php-text-template/tree/4.0.1" + "source": "https://github.com/sebastianbergmann/php-text-template/tree/5.0.0" }, "funding": [ { @@ -10122,32 +10691,32 @@ "type": "github" } ], - "time": "2024-07-03T05:08:43+00:00" + "time": "2025-02-07T04:59:16+00:00" }, { "name": "phpunit/php-timer", - "version": "7.0.1", + "version": "8.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-timer.git", - "reference": "3b415def83fbcb41f991d9ebf16ae4ad8b7837b3" + "reference": "f258ce36aa457f3aa3339f9ed4c81fc66dc8c2cc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/3b415def83fbcb41f991d9ebf16ae4ad8b7837b3", - "reference": "3b415def83fbcb41f991d9ebf16ae4ad8b7837b3", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/f258ce36aa457f3aa3339f9ed4c81fc66dc8c2cc", + "reference": "f258ce36aa457f3aa3339f9ed4c81fc66dc8c2cc", "shasum": "" }, "require": { - "php": ">=8.2" + "php": ">=8.3" }, "require-dev": { - "phpunit/phpunit": "^11.0" + "phpunit/phpunit": "^12.0" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "7.0-dev" + "dev-main": "8.0-dev" } }, "autoload": { @@ -10174,7 +10743,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/php-timer/issues", "security": "https://github.com/sebastianbergmann/php-timer/security/policy", - "source": "https://github.com/sebastianbergmann/php-timer/tree/7.0.1" + "source": "https://github.com/sebastianbergmann/php-timer/tree/8.0.0" }, "funding": [ { @@ -10182,20 +10751,20 @@ "type": "github" } ], - "time": "2024-07-03T05:09:35+00:00" + "time": "2025-02-07T04:59:38+00:00" }, { "name": "phpunit/phpunit", - "version": "11.5.15", + "version": "12.5.4", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "4b6a4ee654e5e0c5e1f17e2f83c0f4c91dee1f9c" + "reference": "4ba0e923f9d3fc655de22f9547c01d15a41fc93a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/4b6a4ee654e5e0c5e1f17e2f83c0f4c91dee1f9c", - "reference": "4b6a4ee654e5e0c5e1f17e2f83c0f4c91dee1f9c", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/4ba0e923f9d3fc655de22f9547c01d15a41fc93a", + "reference": "4ba0e923f9d3fc655de22f9547c01d15a41fc93a", "shasum": "" }, "require": { @@ -10205,37 +10774,33 @@ "ext-mbstring": "*", "ext-xml": "*", "ext-xmlwriter": "*", - "myclabs/deep-copy": "^1.13.0", + "myclabs/deep-copy": "^1.13.4", "phar-io/manifest": "^2.0.4", "phar-io/version": "^3.2.1", - "php": ">=8.2", - "phpunit/php-code-coverage": "^11.0.9", - "phpunit/php-file-iterator": "^5.1.0", - "phpunit/php-invoker": "^5.0.1", - "phpunit/php-text-template": "^4.0.1", - "phpunit/php-timer": "^7.0.1", - "sebastian/cli-parser": "^3.0.2", - "sebastian/code-unit": "^3.0.3", - "sebastian/comparator": "^6.3.1", - "sebastian/diff": "^6.0.2", - "sebastian/environment": "^7.2.0", - "sebastian/exporter": "^6.3.0", - "sebastian/global-state": "^7.0.2", - "sebastian/object-enumerator": "^6.0.1", - "sebastian/type": "^5.1.2", - "sebastian/version": "^5.0.2", + "php": ">=8.3", + "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", + "phpunit/php-timer": "^8.0.0", + "sebastian/cli-parser": "^4.2.0", + "sebastian/comparator": "^7.1.3", + "sebastian/diff": "^7.0.0", + "sebastian/environment": "^8.0.3", + "sebastian/exporter": "^7.0.2", + "sebastian/global-state": "^8.0.2", + "sebastian/object-enumerator": "^7.0.0", + "sebastian/type": "^6.0.3", + "sebastian/version": "^6.0.0", "staabm/side-effects-detector": "^1.0.5" }, - "suggest": { - "ext-soap": "To be able to generate mocks based on WSDL files" - }, "bin": [ "phpunit" ], "type": "library", "extra": { "branch-alias": { - "dev-main": "11.5-dev" + "dev-main": "12.5-dev" } }, "autoload": { @@ -10267,7 +10832,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/11.5.15" + "source": "https://github.com/sebastianbergmann/phpunit/tree/12.5.4" }, "funding": [ { @@ -10278,37 +10843,105 @@ "url": "https://github.com/sebastianbergmann", "type": "github" }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, { "url": "https://tidelift.com/funding/github/packagist/phpunit/phpunit", "type": "tidelift" } ], - "time": "2025-03-23T16:02:11+00:00" + "time": "2025-12-15T06:05:34+00:00" }, { - "name": "sebastian/cli-parser", - "version": "3.0.2", + "name": "rector/rector", + "version": "2.3.1", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/cli-parser.git", - "reference": "15c5dd40dc4f38794d383bb95465193f5e0ae180" + "url": "https://github.com/rectorphp/rector.git", + "reference": "9afc1bb43571b25629f353c61a9315b5ef31383a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/15c5dd40dc4f38794d383bb95465193f5e0ae180", - "reference": "15c5dd40dc4f38794d383bb95465193f5e0ae180", + "url": "https://api.github.com/repos/rectorphp/rector/zipball/9afc1bb43571b25629f353c61a9315b5ef31383a", + "reference": "9afc1bb43571b25629f353c61a9315b5ef31383a", "shasum": "" }, "require": { - "php": ">=8.2" + "php": "^7.4|^8.0", + "phpstan/phpstan": "^2.1.33" + }, + "conflict": { + "rector/rector-doctrine": "*", + "rector/rector-downgrade-php": "*", + "rector/rector-phpunit": "*", + "rector/rector-symfony": "*" + }, + "suggest": { + "ext-dom": "To manipulate phpunit.xml via the custom-rule command" + }, + "bin": [ + "bin/rector" + ], + "type": "library", + "autoload": { + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Instant Upgrade and Automated Refactoring of any PHP code", + "homepage": "https://getrector.com/", + "keywords": [ + "automation", + "dev", + "migration", + "refactoring" + ], + "support": { + "issues": "https://github.com/rectorphp/rector/issues", + "source": "https://github.com/rectorphp/rector/tree/2.3.1" + }, + "funding": [ + { + "url": "https://github.com/tomasvotruba", + "type": "github" + } + ], + "time": "2026-01-13T15:13:58+00:00" + }, + { + "name": "sebastian/cli-parser", + "version": "4.2.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/cli-parser.git", + "reference": "90f41072d220e5c40df6e8635f5dafba2d9d4d04" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/90f41072d220e5c40df6e8635f5dafba2d9d4d04", + "reference": "90f41072d220e5c40df6e8635f5dafba2d9d4d04", + "shasum": "" + }, + "require": { + "php": ">=8.3" }, "require-dev": { - "phpunit/phpunit": "^11.0" + "phpunit/phpunit": "^12.0" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "3.0-dev" + "dev-main": "4.2-dev" } }, "autoload": { @@ -10332,152 +10965,51 @@ "support": { "issues": "https://github.com/sebastianbergmann/cli-parser/issues", "security": "https://github.com/sebastianbergmann/cli-parser/security/policy", - "source": "https://github.com/sebastianbergmann/cli-parser/tree/3.0.2" + "source": "https://github.com/sebastianbergmann/cli-parser/tree/4.2.0" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" - } - ], - "time": "2024-07-03T04:41:36+00:00" - }, - { - "name": "sebastian/code-unit", - "version": "3.0.3", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/code-unit.git", - "reference": "54391c61e4af8078e5b276ab082b6d3c54c9ad64" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/54391c61e4af8078e5b276ab082b6d3c54c9ad64", - "reference": "54391c61e4af8078e5b276ab082b6d3c54c9ad64", - "shasum": "" - }, - "require": { - "php": ">=8.2" - }, - "require-dev": { - "phpunit/phpunit": "^11.5" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-main": "3.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ + }, { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" - } - ], - "description": "Collection of value objects that represent the PHP code units", - "homepage": "https://github.com/sebastianbergmann/code-unit", - "support": { - "issues": "https://github.com/sebastianbergmann/code-unit/issues", - "security": "https://github.com/sebastianbergmann/code-unit/security/policy", - "source": "https://github.com/sebastianbergmann/code-unit/tree/3.0.3" - }, - "funding": [ + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2025-03-19T07:56:08+00:00" - }, - { - "name": "sebastian/code-unit-reverse-lookup", - "version": "4.0.1", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", - "reference": "183a9b2632194febd219bb9246eee421dad8d45e" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/183a9b2632194febd219bb9246eee421dad8d45e", - "reference": "183a9b2632194febd219bb9246eee421dad8d45e", - "shasum": "" - }, - "require": { - "php": ">=8.2" - }, - "require-dev": { - "phpunit/phpunit": "^11.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-main": "4.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" + "url": "https://tidelift.com/funding/github/packagist/sebastian/cli-parser", + "type": "tidelift" } ], - "description": "Looks up which function or method a line of code belongs to", - "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", - "support": { - "issues": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/issues", - "security": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/security/policy", - "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/4.0.1" - }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2024-07-03T04:45:54+00:00" + "time": "2025-09-14T09:36:45+00:00" }, { "name": "sebastian/comparator", - "version": "6.3.2", + "version": "7.1.3", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/comparator.git", - "reference": "85c77556683e6eee4323e4c5468641ca0237e2e8" + "reference": "dc904b4bb3ab070865fa4068cd84f3da8b945148" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/85c77556683e6eee4323e4c5468641ca0237e2e8", - "reference": "85c77556683e6eee4323e4c5468641ca0237e2e8", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/dc904b4bb3ab070865fa4068cd84f3da8b945148", + "reference": "dc904b4bb3ab070865fa4068cd84f3da8b945148", "shasum": "" }, "require": { "ext-dom": "*", "ext-mbstring": "*", - "php": ">=8.2", - "sebastian/diff": "^6.0", - "sebastian/exporter": "^6.0" + "php": ">=8.3", + "sebastian/diff": "^7.0", + "sebastian/exporter": "^7.0" }, "require-dev": { - "phpunit/phpunit": "^11.4" + "phpunit/phpunit": "^12.2" }, "suggest": { "ext-bcmath": "For comparing BcMath\\Number objects" @@ -10485,7 +11017,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "6.3-dev" + "dev-main": "7.1-dev" } }, "autoload": { @@ -10525,7 +11057,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/comparator/issues", "security": "https://github.com/sebastianbergmann/comparator/security/policy", - "source": "https://github.com/sebastianbergmann/comparator/tree/6.3.2" + "source": "https://github.com/sebastianbergmann/comparator/tree/7.1.3" }, "funding": [ { @@ -10545,33 +11077,33 @@ "type": "tidelift" } ], - "time": "2025-08-10T08:07:46+00:00" + "time": "2025-08-20T11:27:00+00:00" }, { "name": "sebastian/complexity", - "version": "4.0.1", + "version": "5.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/complexity.git", - "reference": "ee41d384ab1906c68852636b6de493846e13e5a0" + "reference": "bad4316aba5303d0221f43f8cee37eb58d384bbb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/ee41d384ab1906c68852636b6de493846e13e5a0", - "reference": "ee41d384ab1906c68852636b6de493846e13e5a0", + "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/bad4316aba5303d0221f43f8cee37eb58d384bbb", + "reference": "bad4316aba5303d0221f43f8cee37eb58d384bbb", "shasum": "" }, "require": { "nikic/php-parser": "^5.0", - "php": ">=8.2" + "php": ">=8.3" }, "require-dev": { - "phpunit/phpunit": "^11.0" + "phpunit/phpunit": "^12.0" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "4.0-dev" + "dev-main": "5.0-dev" } }, "autoload": { @@ -10595,7 +11127,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/complexity/issues", "security": "https://github.com/sebastianbergmann/complexity/security/policy", - "source": "https://github.com/sebastianbergmann/complexity/tree/4.0.1" + "source": "https://github.com/sebastianbergmann/complexity/tree/5.0.0" }, "funding": [ { @@ -10603,33 +11135,33 @@ "type": "github" } ], - "time": "2024-07-03T04:49:50+00:00" + "time": "2025-02-07T04:55:25+00:00" }, { "name": "sebastian/diff", - "version": "6.0.2", + "version": "7.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/diff.git", - "reference": "b4ccd857127db5d41a5b676f24b51371d76d8544" + "reference": "7ab1ea946c012266ca32390913653d844ecd085f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/b4ccd857127db5d41a5b676f24b51371d76d8544", - "reference": "b4ccd857127db5d41a5b676f24b51371d76d8544", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/7ab1ea946c012266ca32390913653d844ecd085f", + "reference": "7ab1ea946c012266ca32390913653d844ecd085f", "shasum": "" }, "require": { - "php": ">=8.2" + "php": ">=8.3" }, "require-dev": { - "phpunit/phpunit": "^11.0", - "symfony/process": "^4.2 || ^5" + "phpunit/phpunit": "^12.0", + "symfony/process": "^7.2" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "6.0-dev" + "dev-main": "7.0-dev" } }, "autoload": { @@ -10662,7 +11194,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/diff/issues", "security": "https://github.com/sebastianbergmann/diff/security/policy", - "source": "https://github.com/sebastianbergmann/diff/tree/6.0.2" + "source": "https://github.com/sebastianbergmann/diff/tree/7.0.0" }, "funding": [ { @@ -10670,27 +11202,27 @@ "type": "github" } ], - "time": "2024-07-03T04:53:05+00:00" + "time": "2025-02-07T04:55:46+00:00" }, { "name": "sebastian/environment", - "version": "7.2.1", + "version": "8.0.3", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/environment.git", - "reference": "a5c75038693ad2e8d4b6c15ba2403532647830c4" + "reference": "24a711b5c916efc6d6e62aa65aa2ec98fef77f68" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/a5c75038693ad2e8d4b6c15ba2403532647830c4", - "reference": "a5c75038693ad2e8d4b6c15ba2403532647830c4", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/24a711b5c916efc6d6e62aa65aa2ec98fef77f68", + "reference": "24a711b5c916efc6d6e62aa65aa2ec98fef77f68", "shasum": "" }, "require": { - "php": ">=8.2" + "php": ">=8.3" }, "require-dev": { - "phpunit/phpunit": "^11.3" + "phpunit/phpunit": "^12.0" }, "suggest": { "ext-posix": "*" @@ -10698,7 +11230,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "7.2-dev" + "dev-main": "8.0-dev" } }, "autoload": { @@ -10726,7 +11258,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/environment/issues", "security": "https://github.com/sebastianbergmann/environment/security/policy", - "source": "https://github.com/sebastianbergmann/environment/tree/7.2.1" + "source": "https://github.com/sebastianbergmann/environment/tree/8.0.3" }, "funding": [ { @@ -10746,34 +11278,34 @@ "type": "tidelift" } ], - "time": "2025-05-21T11:55:47+00:00" + "time": "2025-08-12T14:11:56+00:00" }, { "name": "sebastian/exporter", - "version": "6.3.0", + "version": "7.0.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/exporter.git", - "reference": "3473f61172093b2da7de1fb5782e1f24cc036dc3" + "reference": "016951ae10980765e4e7aee491eb288c64e505b7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/3473f61172093b2da7de1fb5782e1f24cc036dc3", - "reference": "3473f61172093b2da7de1fb5782e1f24cc036dc3", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/016951ae10980765e4e7aee491eb288c64e505b7", + "reference": "016951ae10980765e4e7aee491eb288c64e505b7", "shasum": "" }, "require": { "ext-mbstring": "*", - "php": ">=8.2", - "sebastian/recursion-context": "^6.0" + "php": ">=8.3", + "sebastian/recursion-context": "^7.0" }, "require-dev": { - "phpunit/phpunit": "^11.3" + "phpunit/phpunit": "^12.0" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "6.1-dev" + "dev-main": "7.0-dev" } }, "autoload": { @@ -10816,43 +11348,55 @@ "support": { "issues": "https://github.com/sebastianbergmann/exporter/issues", "security": "https://github.com/sebastianbergmann/exporter/security/policy", - "source": "https://github.com/sebastianbergmann/exporter/tree/6.3.0" + "source": "https://github.com/sebastianbergmann/exporter/tree/7.0.2" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/exporter", + "type": "tidelift" } ], - "time": "2024-12-05T09:17:50+00:00" + "time": "2025-09-24T06:16:11+00:00" }, { "name": "sebastian/global-state", - "version": "7.0.2", + "version": "8.0.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/global-state.git", - "reference": "3be331570a721f9a4b5917f4209773de17f747d7" + "reference": "ef1377171613d09edd25b7816f05be8313f9115d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/3be331570a721f9a4b5917f4209773de17f747d7", - "reference": "3be331570a721f9a4b5917f4209773de17f747d7", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/ef1377171613d09edd25b7816f05be8313f9115d", + "reference": "ef1377171613d09edd25b7816f05be8313f9115d", "shasum": "" }, "require": { - "php": ">=8.2", - "sebastian/object-reflector": "^4.0", - "sebastian/recursion-context": "^6.0" + "php": ">=8.3", + "sebastian/object-reflector": "^5.0", + "sebastian/recursion-context": "^7.0" }, "require-dev": { "ext-dom": "*", - "phpunit/phpunit": "^11.0" + "phpunit/phpunit": "^12.0" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "7.0-dev" + "dev-main": "8.0-dev" } }, "autoload": { @@ -10878,41 +11422,53 @@ "support": { "issues": "https://github.com/sebastianbergmann/global-state/issues", "security": "https://github.com/sebastianbergmann/global-state/security/policy", - "source": "https://github.com/sebastianbergmann/global-state/tree/7.0.2" + "source": "https://github.com/sebastianbergmann/global-state/tree/8.0.2" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/global-state", + "type": "tidelift" } ], - "time": "2024-07-03T04:57:36+00:00" + "time": "2025-08-29T11:29:25+00:00" }, { "name": "sebastian/lines-of-code", - "version": "3.0.1", + "version": "4.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/lines-of-code.git", - "reference": "d36ad0d782e5756913e42ad87cb2890f4ffe467a" + "reference": "97ffee3bcfb5805568d6af7f0f893678fc076d2f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/d36ad0d782e5756913e42ad87cb2890f4ffe467a", - "reference": "d36ad0d782e5756913e42ad87cb2890f4ffe467a", + "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/97ffee3bcfb5805568d6af7f0f893678fc076d2f", + "reference": "97ffee3bcfb5805568d6af7f0f893678fc076d2f", "shasum": "" }, "require": { "nikic/php-parser": "^5.0", - "php": ">=8.2" + "php": ">=8.3" }, "require-dev": { - "phpunit/phpunit": "^11.0" + "phpunit/phpunit": "^12.0" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "3.0-dev" + "dev-main": "4.0-dev" } }, "autoload": { @@ -10936,7 +11492,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/lines-of-code/issues", "security": "https://github.com/sebastianbergmann/lines-of-code/security/policy", - "source": "https://github.com/sebastianbergmann/lines-of-code/tree/3.0.1" + "source": "https://github.com/sebastianbergmann/lines-of-code/tree/4.0.0" }, "funding": [ { @@ -10944,34 +11500,34 @@ "type": "github" } ], - "time": "2024-07-03T04:58:38+00:00" + "time": "2025-02-07T04:57:28+00:00" }, { "name": "sebastian/object-enumerator", - "version": "6.0.1", + "version": "7.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/object-enumerator.git", - "reference": "f5b498e631a74204185071eb41f33f38d64608aa" + "reference": "1effe8e9b8e068e9ae228e542d5d11b5d16db894" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/f5b498e631a74204185071eb41f33f38d64608aa", - "reference": "f5b498e631a74204185071eb41f33f38d64608aa", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/1effe8e9b8e068e9ae228e542d5d11b5d16db894", + "reference": "1effe8e9b8e068e9ae228e542d5d11b5d16db894", "shasum": "" }, "require": { - "php": ">=8.2", - "sebastian/object-reflector": "^4.0", - "sebastian/recursion-context": "^6.0" + "php": ">=8.3", + "sebastian/object-reflector": "^5.0", + "sebastian/recursion-context": "^7.0" }, "require-dev": { - "phpunit/phpunit": "^11.0" + "phpunit/phpunit": "^12.0" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "6.0-dev" + "dev-main": "7.0-dev" } }, "autoload": { @@ -10994,7 +11550,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/object-enumerator/issues", "security": "https://github.com/sebastianbergmann/object-enumerator/security/policy", - "source": "https://github.com/sebastianbergmann/object-enumerator/tree/6.0.1" + "source": "https://github.com/sebastianbergmann/object-enumerator/tree/7.0.0" }, "funding": [ { @@ -11002,32 +11558,32 @@ "type": "github" } ], - "time": "2024-07-03T05:00:13+00:00" + "time": "2025-02-07T04:57:48+00:00" }, { "name": "sebastian/object-reflector", - "version": "4.0.1", + "version": "5.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/object-reflector.git", - "reference": "6e1a43b411b2ad34146dee7524cb13a068bb35f9" + "reference": "4bfa827c969c98be1e527abd576533293c634f6a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/6e1a43b411b2ad34146dee7524cb13a068bb35f9", - "reference": "6e1a43b411b2ad34146dee7524cb13a068bb35f9", + "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/4bfa827c969c98be1e527abd576533293c634f6a", + "reference": "4bfa827c969c98be1e527abd576533293c634f6a", "shasum": "" }, "require": { - "php": ">=8.2" + "php": ">=8.3" }, "require-dev": { - "phpunit/phpunit": "^11.0" + "phpunit/phpunit": "^12.0" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "4.0-dev" + "dev-main": "5.0-dev" } }, "autoload": { @@ -11050,7 +11606,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/object-reflector/issues", "security": "https://github.com/sebastianbergmann/object-reflector/security/policy", - "source": "https://github.com/sebastianbergmann/object-reflector/tree/4.0.1" + "source": "https://github.com/sebastianbergmann/object-reflector/tree/5.0.0" }, "funding": [ { @@ -11058,32 +11614,32 @@ "type": "github" } ], - "time": "2024-07-03T05:01:32+00:00" + "time": "2025-02-07T04:58:17+00:00" }, { "name": "sebastian/recursion-context", - "version": "6.0.3", + "version": "7.0.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/recursion-context.git", - "reference": "f6458abbf32a6c8174f8f26261475dc133b3d9dc" + "reference": "0b01998a7d5b1f122911a66bebcb8d46f0c82d8c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/f6458abbf32a6c8174f8f26261475dc133b3d9dc", - "reference": "f6458abbf32a6c8174f8f26261475dc133b3d9dc", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/0b01998a7d5b1f122911a66bebcb8d46f0c82d8c", + "reference": "0b01998a7d5b1f122911a66bebcb8d46f0c82d8c", "shasum": "" }, "require": { - "php": ">=8.2" + "php": ">=8.3" }, "require-dev": { - "phpunit/phpunit": "^11.3" + "phpunit/phpunit": "^12.0" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "6.0-dev" + "dev-main": "7.0-dev" } }, "autoload": { @@ -11114,7 +11670,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/recursion-context/issues", "security": "https://github.com/sebastianbergmann/recursion-context/security/policy", - "source": "https://github.com/sebastianbergmann/recursion-context/tree/6.0.3" + "source": "https://github.com/sebastianbergmann/recursion-context/tree/7.0.1" }, "funding": [ { @@ -11134,32 +11690,32 @@ "type": "tidelift" } ], - "time": "2025-08-13T04:42:22+00:00" + "time": "2025-08-13T04:44:59+00:00" }, { "name": "sebastian/type", - "version": "5.1.3", + "version": "6.0.3", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/type.git", - "reference": "f77d2d4e78738c98d9a68d2596fe5e8fa380f449" + "reference": "e549163b9760b8f71f191651d22acf32d56d6d4d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/f77d2d4e78738c98d9a68d2596fe5e8fa380f449", - "reference": "f77d2d4e78738c98d9a68d2596fe5e8fa380f449", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/e549163b9760b8f71f191651d22acf32d56d6d4d", + "reference": "e549163b9760b8f71f191651d22acf32d56d6d4d", "shasum": "" }, "require": { - "php": ">=8.2" + "php": ">=8.3" }, "require-dev": { - "phpunit/phpunit": "^11.3" + "phpunit/phpunit": "^12.0" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "5.1-dev" + "dev-main": "6.0-dev" } }, "autoload": { @@ -11183,7 +11739,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/type/issues", "security": "https://github.com/sebastianbergmann/type/security/policy", - "source": "https://github.com/sebastianbergmann/type/tree/5.1.3" + "source": "https://github.com/sebastianbergmann/type/tree/6.0.3" }, "funding": [ { @@ -11203,29 +11759,29 @@ "type": "tidelift" } ], - "time": "2025-08-09T06:55:48+00:00" + "time": "2025-08-09T06:57:12+00:00" }, { "name": "sebastian/version", - "version": "5.0.2", + "version": "6.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/version.git", - "reference": "c687e3387b99f5b03b6caa64c74b63e2936ff874" + "reference": "3e6ccf7657d4f0a59200564b08cead899313b53c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/c687e3387b99f5b03b6caa64c74b63e2936ff874", - "reference": "c687e3387b99f5b03b6caa64c74b63e2936ff874", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/3e6ccf7657d4f0a59200564b08cead899313b53c", + "reference": "3e6ccf7657d4f0a59200564b08cead899313b53c", "shasum": "" }, "require": { - "php": ">=8.2" + "php": ">=8.3" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "5.0-dev" + "dev-main": "6.0-dev" } }, "autoload": { @@ -11249,7 +11805,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/version/issues", "security": "https://github.com/sebastianbergmann/version/security/policy", - "source": "https://github.com/sebastianbergmann/version/tree/5.0.2" + "source": "https://github.com/sebastianbergmann/version/tree/6.0.0" }, "funding": [ { @@ -11257,73 +11813,7 @@ "type": "github" } ], - "time": "2024-10-09T05:16:32+00:00" - }, - { - "name": "spatie/pest-expectations", - "version": "1.13.2", - "source": { - "type": "git", - "url": "https://github.com/spatie/pest-expectations.git", - "reference": "d78d74cef4b563e669e4e07ae5f88cbeb4373600" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/spatie/pest-expectations/zipball/d78d74cef4b563e669e4e07ae5f88cbeb4373600", - "reference": "d78d74cef4b563e669e4e07ae5f88cbeb4373600", - "shasum": "" - }, - "require": { - "illuminate/database": "^10.7|^11.0|^12.0", - "php": "^8.2" - }, - "require-dev": { - "ext-sockets": "*", - "illuminate/contracts": "^10.0|^11.0|^12.0", - "laravel/pint": "^1.2", - "orchestra/testbench": "^8.3|^9.0|^10.0", - "pestphp/pest": "^3.0", - "spatie/laravel-json-api-paginate": "^1.14", - "spatie/ray": "^1.28" - }, - "type": "library", - "autoload": { - "files": [ - "src/PestExpectations.php", - "src/Helpers.php" - ], - "psr-4": { - "Spatie\\PestExpectations\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Freek Van der Herten", - "email": "freek@spatie.be", - "role": "Developer" - } - ], - "description": "A collection of handy custom Pest customisations", - "homepage": "https://github.com/spatie/pest-expectations", - "keywords": [ - "pest-expectations", - "spatie" - ], - "support": { - "issues": "https://github.com/spatie/pest-expectations/issues", - "source": "https://github.com/spatie/pest-expectations/tree/1.13.2" - }, - "funding": [ - { - "url": "https://github.com/spatie", - "type": "github" - } - ], - "time": "2025-08-12T17:04:55+00:00" + "time": "2025-02-07T05:00:38+00:00" }, { "name": "staabm/side-effects-detector", @@ -11377,82 +11867,6 @@ ], "time": "2024-10-20T05:08:20+00:00" }, - { - "name": "symfony/yaml", - "version": "v7.3.2", - "source": { - "type": "git", - "url": "https://github.com/symfony/yaml.git", - "reference": "b8d7d868da9eb0919e99c8830431ea087d6aae30" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/b8d7d868da9eb0919e99c8830431ea087d6aae30", - "reference": "b8d7d868da9eb0919e99c8830431ea087d6aae30", - "shasum": "" - }, - "require": { - "php": ">=8.2", - "symfony/deprecation-contracts": "^2.5|^3.0", - "symfony/polyfill-ctype": "^1.8" - }, - "conflict": { - "symfony/console": "<6.4" - }, - "require-dev": { - "symfony/console": "^6.4|^7.0" - }, - "bin": [ - "Resources/bin/yaml-lint" - ], - "type": "library", - "autoload": { - "psr-4": { - "Symfony\\Component\\Yaml\\": "" - }, - "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": "Loads and dumps YAML files", - "homepage": "https://symfony.com", - "support": { - "source": "https://github.com/symfony/yaml/tree/v7.3.2" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://github.com/nicolas-grekas", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2025-07-10T08:47:49+00:00" - }, { "name": "ta-tikoma/phpunit-architecture-test", "version": "0.8.5", @@ -11514,23 +11928,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": { @@ -11552,7 +11966,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": [ { @@ -11560,7 +11974,69 @@ "type": "github" } ], - "time": "2024-03-03T12:36:25+00:00" + "time": "2025-12-08T11:19:18+00:00" + }, + { + "name": "webmozart/assert", + "version": "2.1.2", + "source": { + "type": "git", + "url": "https://github.com/webmozarts/assert.git", + "reference": "ce6a2f100c404b2d32a1dd1270f9b59ad4f57649" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/webmozarts/assert/zipball/ce6a2f100c404b2d32a1dd1270f9b59ad4f57649", + "reference": "ce6a2f100c404b2d32a1dd1270f9b59ad4f57649", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "ext-date": "*", + "ext-filter": "*", + "php": "^8.2" + }, + "suggest": { + "ext-intl": "", + "ext-simplexml": "", + "ext-spl": "" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-feature/2-0": "2.0-dev" + } + }, + "autoload": { + "psr-4": { + "Webmozart\\Assert\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "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.", + "keywords": [ + "assert", + "check", + "validate" + ], + "support": { + "issues": "https://github.com/webmozarts/assert/issues", + "source": "https://github.com/webmozarts/assert/tree/2.1.2" + }, + "time": "2026-01-13T14:02:24+00:00" } ], "aliases": [], @@ -11570,8 +12046,10 @@ "prefer-lowest": false, "platform": { "php": "^8.2", - "ext-imagick": "*" + "ext-imagick": "*", + "ext-simplexml": "*", + "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 98eaee9..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' => [ @@ -152,4 +152,5 @@ return [ 'version' => env('APP_VERSION', null), + 'catalog_url' => env('CATALOG_URL', 'https://raw.githubusercontent.com/bnussbau/trmnl-recipe-catalog/refs/heads/main/catalog.yaml'), ]; diff --git a/config/services.php b/config/services.php index 7fe0344..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' => [ @@ -58,7 +60,7 @@ return [ 'endpoint' => env('OIDC_ENDPOINT'), 'client_id' => env('OIDC_CLIENT_ID'), 'client_secret' => env('OIDC_CLIENT_SECRET'), - 'redirect' => env('APP_URL', 'http://localhost:8000') . '/auth/oidc/callback', + 'redirect' => env('APP_URL', 'http://localhost:8000').'/auth/oidc/callback', 'scopes' => explode(',', env('OIDC_SCOPES', 'openid,profile,email')), ], 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_06_13_102932_add_configuration_to_plugins_table.php b/database/migrations/2025_06_13_102932_add_configuration_to_plugins_table.php new file mode 100644 index 0000000..2ed9123 --- /dev/null +++ b/database/migrations/2025_06_13_102932_add_configuration_to_plugins_table.php @@ -0,0 +1,30 @@ +json('configuration_template')->nullable(); + $table->json('configuration')->nullable(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('plugins', function (Blueprint $table) { + $table->dropColumn('configuration_template'); + $table->dropColumn('configuration'); + }); + } +}; 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_08_22_231823_add_trmnlp_to_plugins_table.php b/database/migrations/2025_08_22_231823_add_trmnlp_to_plugins_table.php new file mode 100644 index 0000000..4c90d29 --- /dev/null +++ b/database/migrations/2025_08_22_231823_add_trmnlp_to_plugins_table.php @@ -0,0 +1,28 @@ +string('trmnlp_id')->nullable()->after('uuid'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('plugins', function (Blueprint $table) { + $table->dropColumn('trmnlp_id'); + }); + } +}; diff --git a/database/migrations/2025_10_30_144500_add_no_bleed_and_dark_mode_to_plugins_table.php b/database/migrations/2025_10_30_144500_add_no_bleed_and_dark_mode_to_plugins_table.php new file mode 100644 index 0000000..f7329c8 --- /dev/null +++ b/database/migrations/2025_10_30_144500_add_no_bleed_and_dark_mode_to_plugins_table.php @@ -0,0 +1,32 @@ +boolean('no_bleed')->default(false)->after('configuration_template'); + } + if (! Schema::hasColumn('plugins', 'dark_mode')) { + $table->boolean('dark_mode')->default(false)->after('no_bleed'); + } + }); + } + + public function down(): void + { + Schema::table('plugins', function (Blueprint $table): void { + if (Schema::hasColumn('plugins', 'dark_mode')) { + $table->dropColumn('dark_mode'); + } + if (Schema::hasColumn('plugins', 'no_bleed')) { + $table->dropColumn('no_bleed'); + } + }); + } +}; 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..3b9b1b7 --- /dev/null +++ b/database/migrations/2026_01_11_121809_make_trmnlp_id_unique_in_plugins_table.php @@ -0,0 +1,58 @@ +selectRaw('user_id, trmnlp_id, COUNT(*) as duplicate_count') + ->whereNotNull('trmnlp_id') + ->groupBy('user_id', 'trmnlp_id') + ->havingRaw('COUNT(*) > ?', [1]) + ->get(); + + // For each duplicate combination, keep the first one (by id) and set others to null + foreach ($duplicates as $duplicate) { + $plugins = Plugin::query() + ->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; + } + + $plugin->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..890eed9 100644 --- a/database/seeders/ExampleRecipesSeeder.php +++ b/database/seeders/ExampleRecipesSeeder.php @@ -13,8 +13,8 @@ class ExampleRecipesSeeder extends Seeder public function run($user_id = 1): void { Plugin::updateOrCreate( + ['uuid' => '9e46c6cf-358c-4bfe-8998-436b3a207fec'], [ - 'uuid' => '9e46c6cf-358c-4bfe-8998-436b3a207fec', 'name' => 'ÖBB Departures', 'user_id' => $user_id, 'data_payload' => null, @@ -32,8 +32,8 @@ class ExampleRecipesSeeder extends Seeder ); Plugin::updateOrCreate( + ['uuid' => '3b046eda-34e9-4232-b935-c33b989a284b'], [ - 'uuid' => '3b046eda-34e9-4232-b935-c33b989a284b', 'name' => 'Weather', 'user_id' => $user_id, 'data_payload' => null, @@ -51,8 +51,8 @@ class ExampleRecipesSeeder extends Seeder ); Plugin::updateOrCreate( + ['uuid' => '21464b16-5f5a-4099-a967-f5c915e3da54'], [ - 'uuid' => '21464b16-5f5a-4099-a967-f5c915e3da54', 'name' => 'Zen Quotes', 'user_id' => $user_id, 'data_payload' => null, @@ -70,8 +70,8 @@ class ExampleRecipesSeeder extends Seeder ); Plugin::updateOrCreate( + ['uuid' => '8d472959-400f-46ee-afb2-4a9f1cfd521f'], [ - 'uuid' => '8d472959-400f-46ee-afb2-4a9f1cfd521f', 'name' => 'This Day in History', 'user_id' => $user_id, 'data_payload' => null, @@ -89,8 +89,8 @@ class ExampleRecipesSeeder extends Seeder ); Plugin::updateOrCreate( + ['uuid' => '4349fdad-a273-450b-aa00-3d32f2de788d'], [ - 'uuid' => '4349fdad-a273-450b-aa00-3d32f2de788d', 'name' => 'Home Assistant', 'user_id' => $user_id, 'data_payload' => null, @@ -108,8 +108,8 @@ class ExampleRecipesSeeder extends Seeder ); Plugin::updateOrCreate( + ['uuid' => 'be5f7e1f-3ad8-4d66-93b2-36f7d6dcbd80'], [ - 'uuid' => 'be5f7e1f-3ad8-4d66-93b2-36f7d6dcbd80', 'name' => 'Sunrise/Sunset', 'user_id' => $user_id, 'data_payload' => null, @@ -127,8 +127,8 @@ class ExampleRecipesSeeder extends Seeder ); Plugin::updateOrCreate( + ['uuid' => '82d3ee14-d578-4969-bda5-2bbf825435fe'], [ - 'uuid' => '82d3ee14-d578-4969-bda5-2bbf825435fe', 'name' => 'Pollen Forecast', 'user_id' => $user_id, 'data_payload' => null, @@ -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/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md index 07ec847..40bcbd3 100644 --- a/docs/DEVELOPMENT.md +++ b/docs/DEVELOPMENT.md @@ -16,10 +16,9 @@ git clone git@github.com:usetrmnl/byos_laravel.git ```bash cp .env.example .env -php artisan key:generate ``` -#### Install dependencies and Build frontend +#### Install dependencies and build frontend ```bash composer install @@ -27,6 +26,12 @@ npm i npm run build ``` +#### Generate application key + +```bash +php artisan key:generate +``` + #### Run migrations ```bash diff --git a/package-lock.json b/package-lock.json index 36cffb8..e722432 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,18 +1,31 @@ { - "name": "byos_laravel", + "name": "laravel", "lockfileVersion": 3, "requires": true, "packages": { "": { "dependencies": { - "@tailwindcss/vite": "^4.0.7", + "@codemirror/commands": "^6.9.0", + "@codemirror/lang-css": "^6.3.1", + "@codemirror/lang-html": "^6.4.11", + "@codemirror/lang-javascript": "^6.2.4", + "@codemirror/lang-json": "^6.0.2", + "@codemirror/lang-liquid": "^6.3.0", + "@codemirror/language": "^6.11.3", + "@codemirror/search": "^6.5.11", + "@codemirror/state": "^6.5.2", + "@codemirror/theme-one-dark": "^6.1.3", + "@codemirror/view": "^6.38.5", + "@fsegurai/codemirror-theme-github-light": "^6.2.2", + "@tailwindcss/vite": "^4.1.11", "autoprefixer": "^10.4.20", "axios": "^1.8.2", + "codemirror": "^6.0.2", "concurrently": "^9.0.1", - "laravel-vite-plugin": "^1.0", - "puppeteer": "^24.3.0", + "laravel-vite-plugin": "^2.0", + "puppeteer": "24.30.0", "tailwindcss": "^4.0.7", - "vite": "^6.3" + "vite": "^7.0.4" }, "optionalDependencies": { "@rollup/rollup-linux-x64-gnu": "4.9.5", @@ -20,19 +33,6 @@ "lightningcss-linux-x64-gnu": "^1.29.1" } }, - "node_modules/@ampproject/remapping": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", - "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", - "license": "Apache-2.0", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/@babel/code-frame": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", @@ -48,18 +48,182 @@ } }, "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.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", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.17.0", + "@lezer/common": "^1.0.0" + } + }, + "node_modules/@codemirror/commands": { + "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", + "@codemirror/state": "^6.4.0", + "@codemirror/view": "^6.27.0", + "@lezer/common": "^1.1.0" + } + }, + "node_modules/@codemirror/lang-css": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/@codemirror/lang-css/-/lang-css-6.3.1.tgz", + "integrity": "sha512-kr5fwBGiGtmz6l0LSJIbno9QrifNMUusivHbnA1H6Dmqy4HZFte3UAICix1VuKo0lMPKQr2rqB+0BkKi/S3Ejg==", + "license": "MIT", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@lezer/common": "^1.0.2", + "@lezer/css": "^1.1.7" + } + }, + "node_modules/@codemirror/lang-html": { + "version": "6.4.11", + "resolved": "https://registry.npmjs.org/@codemirror/lang-html/-/lang-html-6.4.11.tgz", + "integrity": "sha512-9NsXp7Nwp891pQchI7gPdTwBuSuT3K65NGTHWHNJ55HjYcHLllr0rbIZNdOzas9ztc1EUVBlHou85FFZS4BNnw==", + "license": "MIT", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/lang-css": "^6.0.0", + "@codemirror/lang-javascript": "^6.0.0", + "@codemirror/language": "^6.4.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.17.0", + "@lezer/common": "^1.0.0", + "@lezer/css": "^1.1.0", + "@lezer/html": "^1.3.12" + } + }, + "node_modules/@codemirror/lang-javascript": { + "version": "6.2.4", + "resolved": "https://registry.npmjs.org/@codemirror/lang-javascript/-/lang-javascript-6.2.4.tgz", + "integrity": "sha512-0WVmhp1QOqZ4Rt6GlVGwKJN3KW7Xh4H2q8ZZNGZaP6lRdxXJzmjm4FqvmOojVj6khWJHIb9sp7U/72W7xQgqAA==", + "license": "MIT", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/language": "^6.6.0", + "@codemirror/lint": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.17.0", + "@lezer/common": "^1.0.0", + "@lezer/javascript": "^1.0.0" + } + }, + "node_modules/@codemirror/lang-json": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@codemirror/lang-json/-/lang-json-6.0.2.tgz", + "integrity": "sha512-x2OtO+AvwEHrEwR0FyyPtfDUiloG3rnVTSZV1W8UteaLL8/MajQd8DpvUb2YVzC+/T18aSDv0H9mu+xw0EStoQ==", + "license": "MIT", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@lezer/json": "^1.0.0" + } + }, + "node_modules/@codemirror/lang-liquid": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/@codemirror/lang-liquid/-/lang-liquid-6.3.0.tgz", + "integrity": "sha512-fY1YsUExcieXRTsCiwX/bQ9+PbCTA/Fumv7C7mTUZHoFkibfESnaXwpr2aKH6zZVwysEunsHHkaIpM/pl3xETQ==", + "license": "MIT", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/lang-html": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "@lezer/common": "^1.0.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.3.1" + } + }, + "node_modules/@codemirror/language": { + "version": "6.11.3", + "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.11.3.tgz", + "integrity": "sha512-9HBM2XnwDj7fnu0551HkGdrUrrqmYq/WC5iv6nbY2WdicXdGbhR/gfbZOH73Aqj4351alY1+aoG9rCNfiwS1RA==", + "license": "MIT", + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.23.0", + "@lezer/common": "^1.1.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0", + "style-mod": "^4.0.0" + } + }, + "node_modules/@codemirror/lint": { + "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", + "@codemirror/view": "^6.35.0", + "crelt": "^1.0.5" + } + }, + "node_modules/@codemirror/search": { + "version": "6.5.11", + "resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.5.11.tgz", + "integrity": "sha512-KmWepDE6jUdL6n8cAAqIpRmLPBZ5ZKnicE8oGU/s3QrAVID+0VhLFrzUucVKHG5035/BSykhExDL/Xm7dHthiA==", + "license": "MIT", + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "crelt": "^1.0.5" + } + }, + "node_modules/@codemirror/state": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.2.tgz", + "integrity": "sha512-FVqsPqtPWKVVL3dPSxy8wEF/ymIEuVzF1PK3VbUgrxXpJUSHQWWZz4JMToquRxnkw+36LTamCZG2iua2Ptq0fA==", + "license": "MIT", + "dependencies": { + "@marijn/find-cluster-break": "^1.0.0" + } + }, + "node_modules/@codemirror/theme-one-dark": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/@codemirror/theme-one-dark/-/theme-one-dark-6.1.3.tgz", + "integrity": "sha512-NzBdIvEJmx6fjeremiGp3t/okrLPYT0d9orIc7AFun8oZcRk58aejkqhv6spnz4MLAevrKNPMQYXEWMg4s+sKA==", + "license": "MIT", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "@lezer/highlight": "^1.0.0" + } + }, + "node_modules/@codemirror/view": { + "version": "6.38.8", + "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.38.8.tgz", + "integrity": "sha512-XcE9fcnkHCbWkjeKyi0lllwXmBLtyYb5dt89dJyx23I9+LSh5vZDIuk7OLG4VM1lgrXZQcY6cxyZyk5WVPRv/A==", + "license": "MIT", + "dependencies": { + "@codemirror/state": "^6.5.0", + "crelt": "^1.0.6", + "style-mod": "^4.1.0", + "w3c-keyname": "^2.2.4" + } + }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.8.tgz", - "integrity": "sha512-urAvrUedIqEiFR3FYSLTWQgLu5tb+m0qZw0NBEasUeo6wuqatkMDaRT+1uABiGXEu5vqgPd7FGE1BhsAIy9QVA==", + "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" ], @@ -73,9 +237,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.8.tgz", - "integrity": "sha512-RONsAvGCz5oWyePVnLdZY/HHwA++nxYWIX1atInlaW6SEkwq6XkP3+cb825EUcRs5Vss/lGh/2YxAb5xqc07Uw==", + "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" ], @@ -89,9 +253,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.8.tgz", - "integrity": "sha512-OD3p7LYzWpLhZEyATcTSJ67qB5D+20vbtr6vHlHWSQYhKtzUYrETuWThmzFpZtFsBIxRvhO07+UgVA9m0i/O1w==", + "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" ], @@ -105,9 +269,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.8.tgz", - "integrity": "sha512-yJAVPklM5+4+9dTeKwHOaA+LQkmrKFX96BM0A/2zQrbS6ENCmxc4OVoBs5dPkCCak2roAD+jKCdnmOqKszPkjA==", + "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" ], @@ -121,9 +285,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.8.tgz", - "integrity": "sha512-Jw0mxgIaYX6R8ODrdkLLPwBqHTtYHJSmzzd+QeytSugzQ0Vg4c5rDky5VgkoowbZQahCbsv1rT1KW72MPIkevw==", + "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" ], @@ -137,9 +301,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.8.tgz", - "integrity": "sha512-Vh2gLxxHnuoQ+GjPNvDSDRpoBCUzY4Pu0kBqMBDlK4fuWbKgGtmDIeEC081xi26PPjn+1tct+Bh8FjyLlw1Zlg==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", "cpu": [ "x64" ], @@ -153,9 +317,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.8.tgz", - "integrity": "sha512-YPJ7hDQ9DnNe5vxOm6jaie9QsTwcKedPvizTVlqWG9GBSq+BuyWEDazlGaDTC5NGU4QJd666V0yqCBL2oWKPfA==", + "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" ], @@ -169,9 +333,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.8.tgz", - "integrity": "sha512-MmaEXxQRdXNFsRN/KcIimLnSJrk2r5H8v+WVafRWz5xdSVmWLoITZQXcgehI2ZE6gioE6HirAEToM/RvFBeuhw==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", "cpu": [ "x64" ], @@ -185,9 +349,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.8.tgz", - "integrity": "sha512-FuzEP9BixzZohl1kLf76KEVOsxtIBFwCaLupVuk4eFVnOZfU+Wsn+x5Ryam7nILV2pkq2TqQM9EZPsOBuMC+kg==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", "cpu": [ "arm" ], @@ -201,9 +365,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.8.tgz", - "integrity": "sha512-WIgg00ARWv/uYLU7lsuDK00d/hHSfES5BzdWAdAig1ioV5kaFNrtK8EqGcUBJhYqotlUByUKz5Qo6u8tt7iD/w==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", "cpu": [ "arm64" ], @@ -217,9 +381,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.8.tgz", - "integrity": "sha512-A1D9YzRX1i+1AJZuFFUMP1E9fMaYY+GnSQil9Tlw05utlE86EKTUA7RjwHDkEitmLYiFsRd9HwKBPEftNdBfjg==", + "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" ], @@ -233,9 +397,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.8.tgz", - "integrity": "sha512-O7k1J/dwHkY1RMVvglFHl1HzutGEFFZ3kNiDMSOyUrB7WcoHGf96Sh+64nTRT26l3GMbCW01Ekh/ThKM5iI7hQ==", + "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" ], @@ -249,9 +413,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.8.tgz", - "integrity": "sha512-uv+dqfRazte3BzfMp8PAQXmdGHQt2oC/y2ovwpTteqrMx2lwaksiFZ/bdkXJC19ttTvNXBuWH53zy/aTj1FgGw==", + "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" ], @@ -265,9 +429,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.8.tgz", - "integrity": "sha512-GyG0KcMi1GBavP5JgAkkstMGyMholMDybAf8wF5A70CALlDM2p/f7YFE7H92eDeH/VBtFJA5MT4nRPDGg4JuzQ==", + "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" ], @@ -281,9 +445,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.8.tgz", - "integrity": "sha512-rAqDYFv3yzMrq7GIcen3XP7TUEG/4LK86LUPMIz6RT8A6pRIDn0sDcvjudVZBiiTcZCY9y2SgYX2lgK3AF+1eg==", + "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" ], @@ -297,9 +461,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.8.tgz", - "integrity": "sha512-Xutvh6VjlbcHpsIIbwY8GVRbwoviWT19tFhgdA7DlenLGC/mbc3lBoVb7jxj9Z+eyGqvcnSyIltYUrkKzWqSvg==", + "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" ], @@ -313,9 +477,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.8.tgz", - "integrity": "sha512-ASFQhgY4ElXh3nDcOMTkQero4b1lgubskNlhIfJrsH5OKZXDpUAKBlNS0Kx81jwOBp+HCeZqmoJuihTv57/jvQ==", + "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" ], @@ -329,9 +493,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.8.tgz", - "integrity": "sha512-d1KfruIeohqAi6SA+gENMuObDbEjn22olAR7egqnkCD9DGBG0wsEARotkLgXDu6c4ncgWTZJtN5vcgxzWRMzcw==", + "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" ], @@ -345,9 +509,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.8.tgz", - "integrity": "sha512-nVDCkrvx2ua+XQNyfrujIG38+YGyuy2Ru9kKVNyh5jAys6n+l44tTtToqHjino2My8VAY6Lw9H7RI73XFi66Cg==", + "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" ], @@ -361,9 +525,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.8.tgz", - "integrity": "sha512-j8HgrDuSJFAujkivSMSfPQSAa5Fxbvk4rgNAS5i3K+r8s1X0p1uOO2Hl2xNsGFppOeHOLAVgYwDVlmxhq5h+SQ==", + "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" ], @@ -377,9 +541,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.8.tgz", - "integrity": "sha512-1h8MUAwa0VhNCDp6Af0HToI2TJFAn1uqT9Al6DJVzdIBAd21m/G0Yfc77KDM3uF3T/YaOgQq3qTJHPbTOInaIQ==", + "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" ], @@ -393,9 +557,9 @@ } }, "node_modules/@esbuild/openharmony-arm64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.8.tgz", - "integrity": "sha512-r2nVa5SIK9tSWd0kJd9HCffnDHKchTGikb//9c7HX+r+wHYCpQrSgxhlY6KWV1nFo1l4KFbsMlHk+L6fekLsUg==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", "cpu": [ "arm64" ], @@ -409,9 +573,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.8.tgz", - "integrity": "sha512-zUlaP2S12YhQ2UzUfcCuMDHQFJyKABkAjvO5YSndMiIkMimPmxA+BYSBikWgsRpvyxuRnow4nS5NPnf9fpv41w==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", "cpu": [ "x64" ], @@ -425,9 +589,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.8.tgz", - "integrity": "sha512-YEGFFWESlPva8hGL+zvj2z/SaK+pH0SwOM0Nc/d+rVnW7GSTFlLBGzZkuSU9kFIGIo8q9X3ucpZhu8PDN5A2sQ==", + "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" ], @@ -441,9 +605,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.8.tgz", - "integrity": "sha512-hiGgGC6KZ5LZz58OL/+qVVoZiuZlUYlYHNAmczOm7bs2oE1XriPFi5ZHHrS8ACpV5EjySrnoCKmcbQMN+ojnHg==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", "cpu": [ "ia32" ], @@ -457,9 +621,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.8.tgz", - "integrity": "sha512-cn3Yr7+OaaZq1c+2pe+8yxC8E144SReCQjN6/2ynubzYjvyqZjTXfQJpAcQpsdJq3My7XADANiYGHoFC69pLQw==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", "cpu": [ "x64" ], @@ -472,28 +636,38 @@ "node": ">=18" } }, - "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/@fsegurai/codemirror-theme-github-light": { + "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", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "@lezer/highlight": "^1.0.0" } }, "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.12", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.12.tgz", - "integrity": "sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==", + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", "license": "MIT", "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, "node_modules/@jridgewell/resolve-uri": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", @@ -504,33 +678,107 @@ } }, "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.4.tgz", - "integrity": "sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==", + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.29", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.29.tgz", - "integrity": "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==", + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@lezer/common": { + "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": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@lezer/css/-/css-1.3.0.tgz", + "integrity": "sha512-pBL7hup88KbI7hXnZV3PQsn43DHy6TWyzuyk2AO9UyoXcDltvIdqWKE1dLL/45JVZ+YZkHe1WVHqO6wugZZWcw==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.3.0" + } + }, + "node_modules/@lezer/highlight": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.3.tgz", + "integrity": "sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.3.0" + } + }, + "node_modules/@lezer/html": { + "version": "1.3.12", + "resolved": "https://registry.npmjs.org/@lezer/html/-/html-1.3.12.tgz", + "integrity": "sha512-RJ7eRWdaJe3bsiiLLHjCFT1JMk8m1YP9kaUbvu2rMLEoOnke9mcTVDyfOslsln0LtujdWespjJ39w6zo+RsQYw==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "node_modules/@lezer/javascript": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@lezer/javascript/-/javascript-1.5.4.tgz", + "integrity": "sha512-vvYx3MhWqeZtGPwDStM2dwgljd5smolYD2lR2UyFcHfxbBQebqx8yjmFmxtJ/E6nN6u1D9srOiVWm3Rb4tmcUA==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.1.3", + "@lezer/lr": "^1.3.0" + } + }, + "node_modules/@lezer/json": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@lezer/json/-/json-1.0.3.tgz", + "integrity": "sha512-BP9KzdF9Y35PDpv04r0VeSTKDeox5vVr3efE7eBbx3r4s3oNLfunchejZhjArmeieBH+nVOpgIiBJpEAv8ilqQ==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "node_modules/@lezer/lr": { + "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" + } + }, + "node_modules/@marijn/find-cluster-break": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.2.tgz", + "integrity": "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==", + "license": "MIT" + }, "node_modules/@puppeteer/browsers": { - "version": "2.10.6", - "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.10.6.tgz", - "integrity": "sha512-pHUn6ZRt39bP3698HFQlu2ZHCkS/lPcpv7fVQcGBSzNNygw171UXAKrCUhy+TEMw4lEttOKDgNpb04hwUAJeiQ==", + "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": { @@ -541,9 +789,9 @@ } }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.45.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.45.1.tgz", - "integrity": "sha512-NEySIFvMY0ZQO+utJkgoMiCAjMrGvnbDLHvcmlA33UXJpYBCvlBEbMMtV837uCkS+plG2umfhn0T5mMAxGrlRA==", + "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" ], @@ -554,9 +802,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.45.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.45.1.tgz", - "integrity": "sha512-ujQ+sMXJkg4LRJaYreaVx7Z/VMgBBd89wGS4qMrdtfUFZ+TSY5Rs9asgjitLwzeIbhwdEhyj29zhst3L1lKsRQ==", + "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" ], @@ -567,9 +815,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.45.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.45.1.tgz", - "integrity": "sha512-FSncqHvqTm3lC6Y13xncsdOYfxGSLnP+73k815EfNmpewPs+EyM49haPS105Rh4aF5mJKywk9X0ogzLXZzN9lA==", + "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" ], @@ -580,9 +828,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.45.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.45.1.tgz", - "integrity": "sha512-2/vVn/husP5XI7Fsf/RlhDaQJ7x9zjvC81anIVbr4b/f0xtSmXQTFcGIQ/B1cXIYM6h2nAhJkdMHTnD7OtQ9Og==", + "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" ], @@ -593,9 +841,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.45.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.45.1.tgz", - "integrity": "sha512-4g1kaDxQItZsrkVTdYQ0bxu4ZIQ32cotoQbmsAnW1jAE4XCMbcBPDirX5fyUzdhVCKgPcrwWuucI8yrVRBw2+g==", + "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" ], @@ -606,9 +854,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.45.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.45.1.tgz", - "integrity": "sha512-L/6JsfiL74i3uK1Ti2ZFSNsp5NMiM4/kbbGEcOCps99aZx3g8SJMO1/9Y0n/qKlWZfn6sScf98lEOUe2mBvW9A==", + "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" ], @@ -619,9 +867,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.45.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.45.1.tgz", - "integrity": "sha512-RkdOTu2jK7brlu+ZwjMIZfdV2sSYHK2qR08FUWcIoqJC2eywHbXr0L8T/pONFwkGukQqERDheaGTeedG+rra6Q==", + "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" ], @@ -632,9 +880,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.45.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.45.1.tgz", - "integrity": "sha512-3kJ8pgfBt6CIIr1o+HQA7OZ9mp/zDk3ctekGl9qn/pRBgrRgfwiffaUmqioUGN9hv0OHv2gxmvdKOkARCtRb8Q==", + "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" ], @@ -645,9 +893,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.45.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.45.1.tgz", - "integrity": "sha512-k3dOKCfIVixWjG7OXTCOmDfJj3vbdhN0QYEqB+OuGArOChek22hn7Uy5A/gTDNAcCy5v2YcXRJ/Qcnm4/ma1xw==", + "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" ], @@ -658,9 +906,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.45.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.45.1.tgz", - "integrity": "sha512-PmI1vxQetnM58ZmDFl9/Uk2lpBBby6B6rF4muJc65uZbxCs0EA7hhKCk2PKlmZKuyVSHAyIw3+/SiuMLxKxWog==", + "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" ], @@ -670,10 +918,10 @@ "linux" ] }, - "node_modules/@rollup/rollup-linux-loongarch64-gnu": { - "version": "4.45.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.45.1.tgz", - "integrity": "sha512-9UmI0VzGmNJ28ibHW2GpE2nF0PBQqsyiS4kcJ5vK+wuwGnV5RlqdczVocDSUfGX/Na7/XINRVoUgJyFIgipoRg==", + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "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" ], @@ -683,10 +931,10 @@ "linux" ] }, - "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.45.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.45.1.tgz", - "integrity": "sha512-7nR2KY8oEOUTD3pBAxIBBbZr0U7U+R9HDTPNy+5nVVHDXI4ikYniH1oxQz9VoB5PbBU1CZuDGHkLJkd3zLMWsg==", + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "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" ], @@ -697,9 +945,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.45.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.45.1.tgz", - "integrity": "sha512-nlcl3jgUultKROfZijKjRQLUu9Ma0PeNv/VFHkZiKbXTBQXhpytS8CIj5/NfBeECZtY2FJQubm6ltIxm/ftxpw==", + "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" ], @@ -710,9 +958,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.45.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.45.1.tgz", - "integrity": "sha512-HJV65KLS51rW0VY6rvZkiieiBnurSzpzore1bMKAhunQiECPuxsROvyeaot/tcK3A3aGnI+qTHqisrpSgQrpgA==", + "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" ], @@ -723,9 +971,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.45.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.45.1.tgz", - "integrity": "sha512-NITBOCv3Qqc6hhwFt7jLV78VEO/il4YcBzoMGGNxznLgRQf43VQDae0aAzKiBeEPIxnDrACiMgbqjuihx08OOw==", + "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" ], @@ -749,9 +997,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.45.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.45.1.tgz", - "integrity": "sha512-a6WIAp89p3kpNoYStITT9RbTbTnqarU7D8N8F2CV+4Cl9fwCOZraLVuVFvlpsW0SbIiYtEnhCZBPLoNdRkjQFw==", + "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" ], @@ -761,10 +1009,23 @@ "linux" ] }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "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" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.45.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.45.1.tgz", - "integrity": "sha512-T5Bi/NS3fQiJeYdGvRpTAP5P02kqSOpqiopwhj0uaXB6nzs5JVi2XMJb18JUSKhCOX8+UE1UKQufyD6Or48dJg==", + "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" ], @@ -775,9 +1036,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.45.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.45.1.tgz", - "integrity": "sha512-lxV2Pako3ujjuUe9jiU3/s7KSrDfH6IgTSQOnDWr9aJ92YsFd7EurmClK0ly/t8dzMkDtd04g60WX6yl0sGfdw==", + "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" ], @@ -787,10 +1048,23 @@ "win32" ] }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "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" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.45.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.45.1.tgz", - "integrity": "sha512-M/fKi4sasCdM8i0aWJjCSFm2qEnYRR8AMLG2kxp6wD13+tMGA4Z1tVAuHkNRjud5SW2EM3naLuK35w9twvf6aA==", + "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" ], @@ -801,52 +1075,47 @@ ] }, "node_modules/@tailwindcss/node": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.11.tgz", - "integrity": "sha512-yzhzuGRmv5QyU9qLNg4GTlYI6STedBWRE7NjxP45CsFYYq9taI0zJXZBMqIC/c8fViNLhmrbpSFS57EoxUmD6Q==", + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.17.tgz", + "integrity": "sha512-csIkHIgLb3JisEFQ0vxr2Y57GUNYh447C8xzwj89U/8fdW8LhProdxvnVH6U8M2Y73QKiTIH+LWbK3V2BBZsAg==", "license": "MIT", "dependencies": { - "@ampproject/remapping": "^2.3.0", - "enhanced-resolve": "^5.18.1", - "jiti": "^2.4.2", - "lightningcss": "1.30.1", - "magic-string": "^0.30.17", + "@jridgewell/remapping": "^2.3.4", + "enhanced-resolve": "^5.18.3", + "jiti": "^2.6.1", + "lightningcss": "1.30.2", + "magic-string": "^0.30.21", "source-map-js": "^1.2.1", - "tailwindcss": "4.1.11" + "tailwindcss": "4.1.17" } }, "node_modules/@tailwindcss/oxide": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.11.tgz", - "integrity": "sha512-Q69XzrtAhuyfHo+5/HMgr1lAiPP/G40OMFAnws7xcFEYqcypZmdW8eGXaOUIeOl1dzPJBPENXgbjsOyhg2nkrg==", - "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.4.3" - }, "engines": { "node": ">= 10" }, "optionalDependencies": { - "@tailwindcss/oxide-android-arm64": "4.1.11", - "@tailwindcss/oxide-darwin-arm64": "4.1.11", - "@tailwindcss/oxide-darwin-x64": "4.1.11", - "@tailwindcss/oxide-freebsd-x64": "4.1.11", - "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.11", - "@tailwindcss/oxide-linux-arm64-gnu": "4.1.11", - "@tailwindcss/oxide-linux-arm64-musl": "4.1.11", - "@tailwindcss/oxide-linux-x64-gnu": "4.1.11", - "@tailwindcss/oxide-linux-x64-musl": "4.1.11", - "@tailwindcss/oxide-wasm32-wasi": "4.1.11", - "@tailwindcss/oxide-win32-arm64-msvc": "4.1.11", - "@tailwindcss/oxide-win32-x64-msvc": "4.1.11" + "@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.11", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.11.tgz", - "integrity": "sha512-3IfFuATVRUMZZprEIx9OGDjG3Ou3jG4xQzNTvjDoKmU9JdmoCohQJ83MYd0GPnQIu89YoJqvMM0G3uqLRFtetg==", + "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" ], @@ -860,9 +1129,9 @@ } }, "node_modules/@tailwindcss/oxide-darwin-arm64": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.11.tgz", - "integrity": "sha512-ESgStEOEsyg8J5YcMb1xl8WFOXfeBmrhAwGsFxxB2CxY9evy63+AtpbDLAyRkJnxLy2WsD1qF13E97uQyP1lfQ==", + "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" ], @@ -876,9 +1145,9 @@ } }, "node_modules/@tailwindcss/oxide-darwin-x64": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.11.tgz", - "integrity": "sha512-EgnK8kRchgmgzG6jE10UQNaH9Mwi2n+yw1jWmof9Vyg2lpKNX2ioe7CJdf9M5f8V9uaQxInenZkOxnTVL3fhAw==", + "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" ], @@ -892,9 +1161,9 @@ } }, "node_modules/@tailwindcss/oxide-freebsd-x64": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.11.tgz", - "integrity": "sha512-xdqKtbpHs7pQhIKmqVpxStnY1skuNh4CtbcyOHeX1YBE0hArj2romsFGb6yUmzkq/6M24nkxDqU8GYrKrz+UcA==", + "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" ], @@ -908,9 +1177,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.11.tgz", - "integrity": "sha512-ryHQK2eyDYYMwB5wZL46uoxz2zzDZsFBwfjssgB7pzytAeCCa6glsiJGjhTEddq/4OsIjsLNMAiMlHNYnkEEeg==", + "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" ], @@ -924,9 +1193,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.11.tgz", - "integrity": "sha512-mYwqheq4BXF83j/w75ewkPJmPZIqqP1nhoghS9D57CLjsh3Nfq0m4ftTotRYtGnZd3eCztgbSPJ9QhfC91gDZQ==", + "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" ], @@ -940,9 +1209,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-arm64-musl": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.11.tgz", - "integrity": "sha512-m/NVRFNGlEHJrNVk3O6I9ggVuNjXHIPoD6bqay/pubtYC9QIdAMpS+cswZQPBLvVvEF6GtSNONbDkZrjWZXYNQ==", + "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" ], @@ -956,9 +1225,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-x64-gnu": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.11.tgz", - "integrity": "sha512-YW6sblI7xukSD2TdbbaeQVDysIm/UPJtObHJHKxDEcW2exAtY47j52f8jZXkqE1krdnkhCMGqP3dbniu1Te2Fg==", + "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" ], @@ -972,9 +1241,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-x64-musl": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.11.tgz", - "integrity": "sha512-e3C/RRhGunWYNC3aSF7exsQkdXzQ/M+aYuZHKnw4U7KQwTJotnWsGOIVih0s2qQzmEzOFIJ3+xt7iq67K/p56Q==", + "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" ], @@ -988,9 +1257,9 @@ } }, "node_modules/@tailwindcss/oxide-wasm32-wasi": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.11.tgz", - "integrity": "sha512-Xo1+/GU0JEN/C/dvcammKHzeM6NqKovG+6921MR6oadee5XPBaKOumrJCXvopJ/Qb5TH7LX/UAywbqrP4lax0g==", + "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", @@ -1005,21 +1274,21 @@ "license": "MIT", "optional": true, "dependencies": { - "@emnapi/core": "^1.4.3", - "@emnapi/runtime": "^1.4.3", - "@emnapi/wasi-threads": "^1.0.2", - "@napi-rs/wasm-runtime": "^0.2.11", - "@tybys/wasm-util": "^0.9.0", - "tslib": "^2.8.0" + "@emnapi/core": "^1.6.0", + "@emnapi/runtime": "^1.6.0", + "@emnapi/wasi-threads": "^1.1.0", + "@napi-rs/wasm-runtime": "^1.0.7", + "@tybys/wasm-util": "^0.10.1", + "tslib": "^2.4.0" }, "engines": { "node": ">=14.0.0" } }, "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.11.tgz", - "integrity": "sha512-UgKYx5PwEKrac3GPNPf6HVMNhUIGuUh4wlDFR2jYYdkX6pL/rn73zTq/4pzUm8fOjAn5L8zDeHp9iXmUGOXZ+w==", + "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" ], @@ -1033,9 +1302,9 @@ } }, "node_modules/@tailwindcss/oxide-win32-x64-msvc": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.11.tgz", - "integrity": "sha512-YfHoggn1j0LK7wR82TOucWc5LDCguHnoS879idHekmmiR7g9HUtMw9MI0NHatS28u/Xlkfi9w5RJWgz2Dl+5Qg==", + "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" ], @@ -1049,14 +1318,14 @@ } }, "node_modules/@tailwindcss/vite": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.1.11.tgz", - "integrity": "sha512-RHYhrR3hku0MJFRV+fN2gNbDNEh3dwKvY8XJvTxCSXeMOsCRSr+uKvDWQcbizrHgjML6ZmTE5OwMrl5wKcujCw==", + "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.11", - "@tailwindcss/oxide": "4.1.11", - "tailwindcss": "4.1.11" + "@tailwindcss/node": "4.1.17", + "@tailwindcss/oxide": "4.1.17", + "tailwindcss": "4.1.17" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7" @@ -1075,13 +1344,13 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "24.1.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.1.0.tgz", - "integrity": "sha512-ut5FthK5moxFKH2T1CUOC6ctR67rQRvvHdFLCD2Ql6KXmMuCrjsSsRI9UsLCm9M18BMwClv4pn327UvB7eeO1w==", + "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.8.0" + "undici-types": "~7.16.0" } }, "node_modules/@types/yauzl": { @@ -1152,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", @@ -1171,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" @@ -1189,9 +1458,9 @@ } }, "node_modules/axios": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.11.0.tgz", - "integrity": "sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==", + "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", @@ -1200,28 +1469,45 @@ } }, "node_modules/b4a": { - "version": "1.6.7", - "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.7.tgz", - "integrity": "sha512-OnAYlL5b7LEkALw87fUVafQw5rVR9RjwGd4KUwNQ6DrrNmaVaUCgLipfVlzrPQ4tWOR9P0IXGNOx50jYCCdSJg==", - "license": "Apache-2.0" + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.7.3.tgz", + "integrity": "sha512-5Q2mfq2WfGuFp3uS//0s6baOJLMoVduPYVeNmDYxu5OUA1/cBfvr2RIS7vi62LdNj/urk1hfmj867I3qt6uZ7Q==", + "license": "Apache-2.0", + "peerDependencies": { + "react-native-b4a": "*" + }, + "peerDependenciesMeta": { + "react-native-b4a": { + "optional": true + } + } }, "node_modules/bare-events": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.6.0.tgz", - "integrity": "sha512-EKZ5BTXYExaNqi3I3f9RtEsaI/xBSGjE0XZCZilPzFAV/goswFHuPd9jEZlPIZ/iNZJwDSao9qRiScySz7MbQg==", + "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", - "optional": true + "peerDependencies": { + "bare-abort-controller": "*" + }, + "peerDependenciesMeta": { + "bare-abort-controller": { + "optional": true + } + } }, "node_modules/bare-fs": { - "version": "4.1.6", - "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.1.6.tgz", - "integrity": "sha512-25RsLF33BqooOEFNdMcEhMpJy8EoR88zSMrnOQOaM3USnOK2VmaJ1uaQEwPA6AQjrv1lXChScosN6CzbwbO9OQ==", + "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": { "bare-events": "^2.5.4", "bare-path": "^3.0.0", - "bare-stream": "^2.6.4" + "bare-stream": "^2.6.4", + "bare-url": "^2.2.2", + "fast-fifo": "^1.3.2" }, "engines": { "bare": ">=1.16.0" @@ -1236,9 +1522,9 @@ } }, "node_modules/bare-os": { - "version": "3.6.1", - "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.6.1.tgz", - "integrity": "sha512-uaIjxokhFidJP+bmmvKSgiMzj2sV5GPHaZVAIktcxcpCyBFFWO+YlikVAdhmUo2vYFvFhOXIAlldqV29L8126g==", + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.6.2.tgz", + "integrity": "sha512-T+V1+1srU2qYNBmJCXZkUY5vQ0B4FSlL3QDROnKQYOqeiQR8UbjNHlPa+TIbM4cuidiN9GaTaOZgSEgsvPbh5A==", "license": "Apache-2.0", "optional": true, "engines": { @@ -1256,9 +1542,9 @@ } }, "node_modules/bare-stream": { - "version": "2.6.5", - "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.6.5.tgz", - "integrity": "sha512-jSmxKJNJmHySi6hC42zlZnq00rga4jjxcgNZjY9N5WlOe/iOoGRtdwGsHzQv2RlH2KOYMwGUXhf2zXd32BA9RA==", + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.7.0.tgz", + "integrity": "sha512-oyXQNicV1y8nc2aKffH+BUHFRXmx6VrPzlnaEvMhram0nPBrKcEdcyBg5r08D0i8VxngHFAiVyn1QKXpSG0B8A==", "license": "Apache-2.0", "optional": true, "dependencies": { @@ -1277,6 +1563,25 @@ } } }, + "node_modules/bare-url": { + "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": { + "bare-path": "^3.0.0" + } + }, + "node_modules/baseline-browser-mapping": { + "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" + } + }, "node_modules/basic-ftp": { "version": "5.0.5", "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.0.5.tgz", @@ -1287,9 +1592,9 @@ } }, "node_modules/browserslist": { - "version": "4.25.1", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.1.tgz", - "integrity": "sha512-KGj0KoOMXLpSNkkEI6Z6mShmQy0bc1I+T7K9N81k4WWMrfz+6fQ6es80B/YLAeRoKvjYE1YSHHOW1qe9xIVzHw==", + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", "funding": [ { "type": "opencollective", @@ -1306,10 +1611,11 @@ ], "license": "MIT", "dependencies": { - "caniuse-lite": "^1.0.30001726", - "electron-to-chromium": "^1.5.173", - "node-releases": "^2.0.19", - "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" @@ -1350,9 +1656,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001727", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001727.tgz", - "integrity": "sha512-pB68nIHmbN6L/4C6MH1DokyR3bYqFwjaSs/sWDHGj4CTcFtQUQMuJftVwWkXq7mNWOybD3KhUv3oWHoGxgP14Q==", + "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", @@ -1397,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": "7.2.0", - "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-7.2.0.tgz", - "integrity": "sha512-gREyhyBstermK+0RbcJLbFhcQctg92AGgDe/h/taMJEOLRdtSswBAO9KmvltFSQWgM2LrwWu5SIuEUbdm3JsyQ==", + "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", @@ -1433,6 +1730,21 @@ "node": ">=12" } }, + "node_modules/codemirror": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-6.0.2.tgz", + "integrity": "sha512-VhydHotNW5w1UGK0Qj96BwSk/Zqbp9WbnyK2W/eVMv4QyF41INRGpjUhFJY7/uDNuudSc33a/PKr4iDqRduvHw==", + "license": "MIT", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/commands": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/lint": "^6.0.0", + "@codemirror/search": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -1464,18 +1776,17 @@ } }, "node_modules/concurrently": { - "version": "9.2.0", - "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.2.0.tgz", - "integrity": "sha512-IsB/fiXTupmagMW4MNp2lx2cdSN2FfZq78vF90LBB+zZHArbIQZjQtzXCiXnvTxCZSvXanTqFLWBjw2UkLx1SQ==", + "version": "9.2.1", + "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.2.1.tgz", + "integrity": "sha512-fsfrO0MxV64Znoy8/l1vVIjjHa29SZyyqPgQBwhiDcaW8wJc2W3XWVOGx4M3oJBnv/zdUZIIp1gDeS98GzP8Ng==", "license": "MIT", "dependencies": { - "chalk": "^4.1.2", - "lodash": "^4.17.21", - "rxjs": "^7.8.1", - "shell-quote": "^1.8.1", - "supports-color": "^8.1.1", - "tree-kill": "^1.2.2", - "yargs": "^17.7.2" + "chalk": "4.1.2", + "rxjs": "7.8.2", + "shell-quote": "1.8.3", + "supports-color": "8.1.1", + "tree-kill": "1.2.2", + "yargs": "17.7.2" }, "bin": { "conc": "dist/bin/concurrently.js", @@ -1514,6 +1825,12 @@ } } }, + "node_modules/crelt": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz", + "integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==", + "license": "MIT" + }, "node_modules/data-uri-to-buffer": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz", @@ -1524,9 +1841,9 @@ } }, "node_modules/debug": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", - "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -1564,18 +1881,18 @@ } }, "node_modules/detect-libc": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz", - "integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", "license": "Apache-2.0", "engines": { "node": ">=8" } }, "node_modules/devtools-protocol": { - "version": "0.0.1464554", - "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1464554.tgz", - "integrity": "sha512-CAoP3lYfwAGQTaAXYvA6JZR0fjGUb7qec1qf4mToyoH2TZgUFeIqYcjh6f9jNuhHfuZiEdH+PONHYrLhRQX6aw==", + "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": { @@ -1593,9 +1910,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.190", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.190.tgz", - "integrity": "sha512-k4McmnB2091YIsdCgkS0fMVMPOJgxl93ltFzaryXqwip1AaxeDqKCGLxkXODDA5Ab/D+tV5EL5+aTx76RvLRxw==", + "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": { @@ -1614,9 +1931,9 @@ } }, "node_modules/enhanced-resolve": { - "version": "5.18.2", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.2.tgz", - "integrity": "sha512-6Jw4sE1maoRJo3q8MsSIn2onJFbLTOjY9hlx4DZXmOKvLRd1Ok2kXmAGXaafL2+ijsJZ1ClYbl/pmqr9+k4iUQ==", + "version": "5.18.3", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz", + "integrity": "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==", "license": "MIT", "dependencies": { "graceful-fs": "^4.2.4", @@ -1636,9 +1953,9 @@ } }, "node_modules/error-ex": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", - "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", "license": "MIT", "dependencies": { "is-arrayish": "^0.2.1" @@ -1690,9 +2007,9 @@ } }, "node_modules/esbuild": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.8.tgz", - "integrity": "sha512-vVC0USHGtMi8+R4Kz8rt6JhEWLxsv9Rnu/lGYbPR8u47B+DCBksq9JarW0zOO7bs37hyOK1l2/oqtbciutL5+Q==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", "hasInstallScript": true, "license": "MIT", "bin": { @@ -1702,32 +2019,32 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.8", - "@esbuild/android-arm": "0.25.8", - "@esbuild/android-arm64": "0.25.8", - "@esbuild/android-x64": "0.25.8", - "@esbuild/darwin-arm64": "0.25.8", - "@esbuild/darwin-x64": "0.25.8", - "@esbuild/freebsd-arm64": "0.25.8", - "@esbuild/freebsd-x64": "0.25.8", - "@esbuild/linux-arm": "0.25.8", - "@esbuild/linux-arm64": "0.25.8", - "@esbuild/linux-ia32": "0.25.8", - "@esbuild/linux-loong64": "0.25.8", - "@esbuild/linux-mips64el": "0.25.8", - "@esbuild/linux-ppc64": "0.25.8", - "@esbuild/linux-riscv64": "0.25.8", - "@esbuild/linux-s390x": "0.25.8", - "@esbuild/linux-x64": "0.25.8", - "@esbuild/netbsd-arm64": "0.25.8", - "@esbuild/netbsd-x64": "0.25.8", - "@esbuild/openbsd-arm64": "0.25.8", - "@esbuild/openbsd-x64": "0.25.8", - "@esbuild/openharmony-arm64": "0.25.8", - "@esbuild/sunos-x64": "0.25.8", - "@esbuild/win32-arm64": "0.25.8", - "@esbuild/win32-ia32": "0.25.8", - "@esbuild/win32-x64": "0.25.8" + "@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": { @@ -1791,6 +2108,15 @@ "node": ">=0.10.0" } }, + "node_modules/events-universal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz", + "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==", + "license": "Apache-2.0", + "dependencies": { + "bare-events": "^2.7.0" + } + }, "node_modules/extract-zip": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", @@ -1827,10 +2153,13 @@ } }, "node_modules/fdir": { - "version": "6.4.6", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz", - "integrity": "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==", + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, "peerDependencies": { "picomatch": "^3 || ^4" }, @@ -1841,9 +2170,9 @@ } }, "node_modules/follow-redirects": { - "version": "1.15.9", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", - "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", "funding": [ { "type": "individual", @@ -1861,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", @@ -1877,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" } }, @@ -2096,14 +2425,10 @@ } }, "node_modules/ip-address": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz", - "integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==", + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz", + "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==", "license": "MIT", - "dependencies": { - "jsbn": "1.1.0", - "sprintf-js": "^1.1.3" - }, "engines": { "node": ">= 12" } @@ -2124,9 +2449,9 @@ } }, "node_modules/jiti": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.5.1.tgz", - "integrity": "sha512-twQoecYPiVA5K/h6SxtORw/Bs3ar+mLUtoPSc7iMXzQzK8d7eJ/R09wmTwAjiamETn1cXYPGfNnu7DMoHgu12w==", + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", + "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", "license": "MIT", "bin": { "jiti": "lib/jiti-cli.mjs" @@ -2139,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" @@ -2150,12 +2475,6 @@ "js-yaml": "bin/js-yaml.js" } }, - "node_modules/jsbn": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz", - "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==", - "license": "MIT" - }, "node_modules/json-parse-even-better-errors": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", @@ -2163,9 +2482,9 @@ "license": "MIT" }, "node_modules/laravel-vite-plugin": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/laravel-vite-plugin/-/laravel-vite-plugin-1.3.0.tgz", - "integrity": "sha512-P5qyG56YbYxM8OuYmK2OkhcKe0AksNVJUjq9LUZ5tOekU9fBn9LujYyctI4t9XoLjuMvHJXXpCoPntY1oKltuA==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/laravel-vite-plugin/-/laravel-vite-plugin-2.0.1.tgz", + "integrity": "sha512-zQuvzWfUKQu9oNVi1o0RZAJCwhGsdhx4NEOyrVQwJHaWDseGP9tl7XUPLY2T8Cj6+IrZ6lmyxlR1KC8unf3RLA==", "license": "MIT", "dependencies": { "picocolors": "^1.0.0", @@ -2175,16 +2494,16 @@ "clean-orphaned-assets": "bin/clean.js" }, "engines": { - "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + "node": "^20.19.0 || >=22.12.0" }, "peerDependencies": { - "vite": "^5.0.0 || ^6.0.0" + "vite": "^7.0.0" } }, "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" @@ -2197,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" ], @@ -2230,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" ], @@ -2250,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" ], @@ -2270,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" ], @@ -2290,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" ], @@ -2310,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" ], @@ -2330,9 +2670,9 @@ } }, "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==", + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.2.tgz", + "integrity": "sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==", "cpu": [ "x64" ], @@ -2350,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" ], @@ -2370,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" ], @@ -2390,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" ], @@ -2415,12 +2755,6 @@ "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", "license": "MIT" }, - "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "license": "MIT" - }, "node_modules/lru-cache": { "version": "7.18.3", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", @@ -2431,12 +2765,12 @@ } }, "node_modules/magic-string": { - "version": "0.30.17", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", - "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==", + "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.0" + "@jridgewell/sourcemap-codec": "^1.5.5" } }, "node_modules/math-intrinsics": { @@ -2469,48 +2803,12 @@ "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.0.2", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.0.2.tgz", - "integrity": "sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA==", - "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", "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", "license": "MIT" }, - "node_modules/mkdirp": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz", - "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==", - "license": "MIT", - "bin": { - "mkdirp": "dist/cjs/src/bin.js" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -2545,9 +2843,9 @@ } }, "node_modules/node-releases": { - "version": "2.0.19", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", - "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", + "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": { @@ -2733,17 +3031,17 @@ } }, "node_modules/puppeteer": { - "version": "24.15.0", - "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-24.15.0.tgz", - "integrity": "sha512-HPSOTw+DFsU/5s2TUUWEum9WjFbyjmvFDuGHtj2X4YUz2AzOzvKMkT3+A3FR+E+ZefiX/h3kyLyXzWJWx/eMLQ==", + "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.6", - "chromium-bidi": "7.2.0", + "@puppeteer/browsers": "2.10.13", + "chromium-bidi": "11.0.0", "cosmiconfig": "^9.0.0", - "devtools-protocol": "0.0.1464554", - "puppeteer-core": "24.15.0", + "devtools-protocol": "0.0.1521046", + "puppeteer-core": "24.30.0", "typed-query-selector": "^2.12.0" }, "bin": { @@ -2754,16 +3052,17 @@ } }, "node_modules/puppeteer-core": { - "version": "24.15.0", - "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-24.15.0.tgz", - "integrity": "sha512-2iy0iBeWbNyhgiCGd/wvGrDSo73emNFjSxYOcyAqYiagkYt5q4cPfVXaVDKBsukgc2fIIfLAalBZlaxldxdDYg==", + "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.6", - "chromium-bidi": "7.2.0", - "debug": "^4.4.1", - "devtools-protocol": "0.0.1464554", + "@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": { @@ -2789,9 +3088,9 @@ } }, "node_modules/rollup": { - "version": "4.45.1", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.45.1.tgz", - "integrity": "sha512-4iya7Jb76fVpQyLoiVpzUrsjQ12r3dM7fIVz+4NwoYvZOShknRmiv+iu9CClZml5ZLGb0XMcYLutK6w9tgxHDw==", + "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" @@ -2804,33 +3103,35 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.45.1", - "@rollup/rollup-android-arm64": "4.45.1", - "@rollup/rollup-darwin-arm64": "4.45.1", - "@rollup/rollup-darwin-x64": "4.45.1", - "@rollup/rollup-freebsd-arm64": "4.45.1", - "@rollup/rollup-freebsd-x64": "4.45.1", - "@rollup/rollup-linux-arm-gnueabihf": "4.45.1", - "@rollup/rollup-linux-arm-musleabihf": "4.45.1", - "@rollup/rollup-linux-arm64-gnu": "4.45.1", - "@rollup/rollup-linux-arm64-musl": "4.45.1", - "@rollup/rollup-linux-loongarch64-gnu": "4.45.1", - "@rollup/rollup-linux-powerpc64le-gnu": "4.45.1", - "@rollup/rollup-linux-riscv64-gnu": "4.45.1", - "@rollup/rollup-linux-riscv64-musl": "4.45.1", - "@rollup/rollup-linux-s390x-gnu": "4.45.1", - "@rollup/rollup-linux-x64-gnu": "4.45.1", - "@rollup/rollup-linux-x64-musl": "4.45.1", - "@rollup/rollup-win32-arm64-msvc": "4.45.1", - "@rollup/rollup-win32-ia32-msvc": "4.45.1", - "@rollup/rollup-win32-x64-msvc": "4.45.1", + "@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.45.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.45.1.tgz", - "integrity": "sha512-+E/lYl6qu1zqgPEnTrs4WysQtvc/Sh4fC2nByfFExqgYrqkKWp1tWIbe+ELhixnenSpBbLXNi6vbEEJ8M7fiHw==", + "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" ], @@ -2850,9 +3151,9 @@ } }, "node_modules/semver": { - "version": "7.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", - "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -2884,12 +3185,12 @@ } }, "node_modules/socks": { - "version": "2.8.6", - "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.6.tgz", - "integrity": "sha512-pe4Y2yzru68lXCb38aAqRf5gvN8YdjP1lok5o0J7BOHljkyCGKVz7H3vpVIXKD27rj2giOJ7DwVyk/GWrPHDWA==", + "version": "2.8.7", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz", + "integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==", "license": "MIT", "dependencies": { - "ip-address": "^9.0.5", + "ip-address": "^10.0.1", "smart-buffer": "^4.2.0" }, "engines": { @@ -2930,23 +3231,15 @@ "node": ">=0.10.0" } }, - "node_modules/sprintf-js": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", - "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", - "license": "BSD-3-Clause" - }, "node_modules/streamx": { - "version": "2.22.1", - "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.22.1.tgz", - "integrity": "sha512-znKXEBxfatz2GBNK02kRnCXjV+AA4kjZIUxeWSr3UGirZMJfTE9uiwKHobnbgxWyL/JWro8tTq+vOqAK1/qbSA==", + "version": "2.23.0", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.23.0.tgz", + "integrity": "sha512-kn+e44esVfn2Fa/O0CPFcex27fjIL6MkVae0Mm6q+E6f0hWv578YCERbv+4m02cjxvDsPKLnmxral/rR6lBMAg==", "license": "MIT", "dependencies": { + "events-universal": "^1.0.0", "fast-fifo": "^1.3.2", "text-decoder": "^1.1.0" - }, - "optionalDependencies": { - "bare-events": "^2.2.0" } }, "node_modules/string-width": { @@ -2975,6 +3268,12 @@ "node": ">=8" } }, + "node_modules/style-mod": { + "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": { "version": "8.1.1", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", @@ -2991,41 +3290,28 @@ } }, "node_modules/tailwindcss": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.11.tgz", - "integrity": "sha512-2E9TBm6MDD/xKYe+dvJZAmg3yxIEDNRc0jwlNyDg/4Fil2QcSLjFKGVff0lAf1jjeaArlG/M75Ey/EYr/OJtBA==", + "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": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.2.tgz", - "integrity": "sha512-Re10+NauLTMCudc7T5WLFLAwDhQ0JWdrMK+9B2M8zR5hRExKmsRDCBA7/aV/pNJFltmBFO5BAMlQFi/vq3nKOg==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", + "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", "license": "MIT", "engines": { "node": ">=6" - } - }, - "node_modules/tar": { - "version": "7.4.3", - "resolved": "https://registry.npmjs.org/tar/-/tar-7.4.3.tgz", - "integrity": "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==", - "license": "ISC", - "dependencies": { - "@isaacs/fs-minipass": "^4.0.0", - "chownr": "^3.0.0", - "minipass": "^7.1.2", - "minizlib": "^3.0.1", - "mkdirp": "^3.0.1", - "yallist": "^5.0.0" }, - "engines": { - "node": ">=18" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" } }, "node_modules/tar-fs": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.0.tgz", - "integrity": "sha512-5Mty5y/sOF1YWj1J6GiBodjlDc05CUR8PKXrsnFAiSG0xA+GHeWLovaZPYUDXkH/1iKRf2+M5+OrRgzC7O9b7w==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.1.tgz", + "integrity": "sha512-LZA0oaPOc2fVo82Txf3gw+AkEd38szODlptMYejQUhndHMLQ9M059uXR+AfS7DNo0NpINvSqDsvyaCrBVkptWg==", "license": "MIT", "dependencies": { "pump": "^3.0.0", @@ -3057,13 +3343,13 @@ } }, "node_modules/tinyglobby": { - "version": "0.2.14", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz", - "integrity": "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==", + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", "license": "MIT", "dependencies": { - "fdir": "^6.4.4", - "picomatch": "^4.0.2" + "fdir": "^6.5.0", + "picomatch": "^4.0.3" }, "engines": { "node": ">=12.0.0" @@ -3094,16 +3380,16 @@ "license": "MIT" }, "node_modules/undici-types": { - "version": "7.8.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.8.0.tgz", - "integrity": "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==", + "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", @@ -3131,23 +3417,23 @@ } }, "node_modules/vite": { - "version": "6.3.5", - "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz", - "integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==", + "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", "dependencies": { "esbuild": "^0.25.0", - "fdir": "^6.4.4", - "picomatch": "^4.0.2", - "postcss": "^8.5.3", - "rollup": "^4.34.9", - "tinyglobby": "^0.2.13" + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" }, "bin": { "vite": "bin/vite.js" }, "engines": { - "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + "node": "^20.19.0 || >=22.12.0" }, "funding": { "url": "https://github.com/vitejs/vite?sponsor=1" @@ -3156,14 +3442,14 @@ "fsevents": "~2.3.3" }, "peerDependencies": { - "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", - "less": "*", + "less": "^4.0.0", "lightningcss": "^1.21.0", - "sass": "*", - "sass-embedded": "*", - "stylus": "*", - "sugarss": "*", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" @@ -3226,6 +3512,18 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/w3c-keyname": { + "version": "2.2.8", + "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz", + "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", @@ -3279,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 5073158..7262ad1 100644 --- a/package.json +++ b/package.json @@ -6,14 +6,27 @@ "dev": "vite" }, "dependencies": { - "@tailwindcss/vite": "^4.0.7", + "@codemirror/commands": "^6.9.0", + "@codemirror/lang-css": "^6.3.1", + "@codemirror/lang-html": "^6.4.11", + "@codemirror/lang-javascript": "^6.2.4", + "@codemirror/lang-json": "^6.0.2", + "@codemirror/lang-liquid": "^6.3.0", + "@codemirror/language": "^6.11.3", + "@codemirror/search": "^6.5.11", + "@codemirror/state": "^6.5.2", + "@codemirror/theme-one-dark": "^6.1.3", + "@codemirror/view": "^6.38.5", + "@fsegurai/codemirror-theme-github-light": "^6.2.2", + "@tailwindcss/vite": "^4.1.11", "autoprefixer": "^10.4.20", "axios": "^1.8.2", + "codemirror": "^6.0.2", "concurrently": "^9.0.1", - "laravel-vite-plugin": "^1.0", - "puppeteer": "^24.3.0", + "laravel-vite-plugin": "^2.0", + "puppeteer": "24.30.0", "tailwindcss": "^4.0.7", - "vite": "^6.3" + "vite": "^7.0.4" }, "optionalDependencies": { "@rollup/rollup-linux-x64-gnu": "4.9.5", 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/rector.php b/rector.php new file mode 100644 index 0000000..dde2f14 --- /dev/null +++ b/rector.php @@ -0,0 +1,26 @@ +paths([ + __DIR__.'/app', + __DIR__.'/tests', + ]); + + $rectorConfig->sets([ + LevelSetList::UP_TO_PHP_82, + SetList::CODE_QUALITY, + SetList::DEAD_CODE, + SetList::EARLY_RETURN, + SetList::TYPE_DECLARATION, + ]); + + $rectorConfig->skip([ + // Skip any specific rules if needed + ]); +}; 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/app.js b/resources/js/app.js index e69de29..db3ebf3 100644 --- a/resources/js/app.js +++ b/resources/js/app.js @@ -0,0 +1,3 @@ +import { codeEditorFormComponent } from './codemirror-alpine.js'; + +window.codeEditorFormComponent = codeEditorFormComponent; diff --git a/resources/js/codemirror-alpine.js b/resources/js/codemirror-alpine.js new file mode 100644 index 0000000..9ce12f1 --- /dev/null +++ b/resources/js/codemirror-alpine.js @@ -0,0 +1,198 @@ +import { createCodeMirror, getSystemTheme, watchThemeChange } from './codemirror-core.js'; +import { EditorView } from '@codemirror/view'; + +/** + * Alpine.js component for CodeMirror that integrates with textarea and Livewire + * Inspired by Filament's approach with proper state entanglement + * @param {Object} config - Configuration object + * @returns {Object} Alpine.js component object + */ +export function codeEditorFormComponent(config) { + return { + editor: null, + textarea: null, + isLoading: false, + unwatchTheme: null, + + // Configuration + isDisabled: config.isDisabled || false, + language: config.language || 'html', + state: config.state || '', + textareaId: config.textareaId || null, + + /** + * Initialize the component + */ + async init() { + this.isLoading = true; + + try { + // Wait for textarea if provided + if (this.textareaId) { + await this.waitForTextarea(); + } + + await this.$nextTick(); + this.createEditor(); + this.setupEventListeners(); + } finally { + this.isLoading = false; + } + }, + + /** + * Wait for textarea to be available in the DOM + */ + async waitForTextarea() { + let attempts = 0; + const maxAttempts = 50; // 5 seconds max wait + + while (attempts < maxAttempts) { + this.textarea = document.getElementById(this.textareaId); + if (this.textarea) { + return; + } + + // Wait 100ms before trying again + await new Promise(resolve => setTimeout(resolve, 100)); + attempts++; + } + + console.error(`Textarea with ID "${this.textareaId}" not found after ${maxAttempts} attempts`); + }, + + /** + * Update both Livewire state and textarea with new value + */ + updateState(value) { + this.state = value; + if (this.textarea) { + this.textarea.value = value; + this.textarea.dispatchEvent(new Event('input', { bubbles: true })); + } + }, + + /** + * Create the CodeMirror editor instance + */ + createEditor() { + // Clean up any existing editor first + if (this.editor) { + this.editor.destroy(); + } + + const effectiveTheme = this.getEffectiveTheme(); + const initialValue = this.textarea ? this.textarea.value : this.state; + + this.editor = createCodeMirror(this.$refs.editor, { + value: initialValue || '', + language: this.language, + theme: effectiveTheme, + readOnly: this.isDisabled, + onChange: (value) => this.updateState(value), + onUpdate: (value) => this.updateState(value), + onBlur: () => { + if (this.editor) { + this.updateState(this.editor.state.doc.toString()); + } + } + }); + }, + + /** + * Get effective theme + */ + getEffectiveTheme() { + return getSystemTheme(); + }, + + /** + * Update editor content with new value + */ + updateEditorContent(value) { + if (this.editor && value !== this.editor.state.doc.toString()) { + this.editor.dispatch({ + changes: { + from: 0, + to: this.editor.state.doc.length, + insert: value + } + }); + } + }, + + /** + * Setup event listeners for theme changes and state synchronization + */ + setupEventListeners() { + // Watch for state changes from Livewire + this.$watch('state', (newValue) => { + this.updateEditorContent(newValue); + }); + + // Watch for disabled state changes + this.$watch('isDisabled', (newValue) => { + if (this.editor) { + this.editor.dispatch({ + effects: EditorView.editable.reconfigure(!newValue) + }); + } + }); + + // Watch for textarea changes (from Livewire updates) + if (this.textarea) { + this.textarea.addEventListener('input', (event) => { + this.updateEditorContent(event.target.value); + this.state = event.target.value; + }); + + // Listen for Livewire updates that might change the textarea value + const observer = new MutationObserver((mutations) => { + mutations.forEach((mutation) => { + if (mutation.type === 'attributes' && mutation.attributeName === 'value') { + this.updateEditorContent(this.textarea.value); + this.state = this.textarea.value; + } + }); + }); + + observer.observe(this.textarea, { + attributes: true, + attributeFilter: ['value'] + }); + } + + // Listen for theme changes + this.unwatchTheme = watchThemeChange(() => { + this.recreateEditor(); + }); + }, + + /** + * Recreate the editor (useful for theme changes) + */ + async recreateEditor() { + if (this.editor) { + this.editor.destroy(); + this.editor = null; + await this.$nextTick(); + this.createEditor(); + } + }, + + + /** + * Clean up resources when component is destroyed + */ + destroy() { + if (this.editor) { + this.editor.destroy(); + this.editor = null; + } + if (this.unwatchTheme) { + this.unwatchTheme(); + } + } + }; +} + diff --git a/resources/js/codemirror-core.js b/resources/js/codemirror-core.js new file mode 100644 index 0000000..f23389f --- /dev/null +++ b/resources/js/codemirror-core.js @@ -0,0 +1,265 @@ +import { EditorView, lineNumbers, keymap } from '@codemirror/view'; +import { ViewPlugin } from '@codemirror/view'; +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'; +import { css } from '@codemirror/lang-css'; +import { liquid } from '@codemirror/lang-liquid'; +import { oneDark } from '@codemirror/theme-one-dark'; +import { githubLight } from '@fsegurai/codemirror-theme-github-light'; + +// Language support mapping +const LANGUAGE_MAP = { + 'javascript': javascript, + 'js': javascript, + 'json': json, + 'css': css, + 'liquid': liquid, + 'html': html, +}; + +// Theme support mapping +const THEME_MAP = { + 'light': githubLight, + 'dark': oneDark, +}; + +/** + * Get language support based on language parameter + * @param {string} language - Language name or comma-separated list + * @returns {Array|Extension} Language extension(s) + */ +function getLanguageSupport(language) { + // Handle comma-separated languages + if (language.includes(',')) { + const languages = language.split(',').map(lang => lang.trim().toLowerCase()); + const languageExtensions = []; + + languages.forEach(lang => { + const languageFn = LANGUAGE_MAP[lang]; + if (languageFn) { + languageExtensions.push(languageFn()); + } + }); + + return languageExtensions; + } + + // Handle single language + const languageFn = LANGUAGE_MAP[language.toLowerCase()] || LANGUAGE_MAP.html; + return languageFn(); +} + +/** + * Get theme support + * @param {string} theme - Theme name + * @returns {Array} Theme extensions + */ +function getThemeSupport(theme) { + const themeFn = THEME_MAP[theme] || THEME_MAP.light; + return [themeFn]; +} + +/** + * Create a resize plugin that handles container resizing + * @returns {ViewPlugin} Resize plugin + */ +function createResizePlugin() { + return ViewPlugin.fromClass(class { + constructor(view) { + this.view = view; + this.resizeObserver = null; + this.setupResizeObserver(); + } + + setupResizeObserver() { + const container = this.view.dom.parentElement; + if (container) { + this.resizeObserver = new ResizeObserver(() => { + // Use requestAnimationFrame to ensure proper timing + requestAnimationFrame(() => { + this.view.requestMeasure(); + }); + }); + this.resizeObserver.observe(container); + } + } + + destroy() { + if (this.resizeObserver) { + this.resizeObserver.disconnect(); + } + } + }); +} + +/** + * Get Flux-like theme styling based on theme + * @param {string} theme - Theme name ('light', 'dark', or 'auto') + * @returns {Object} Theme-specific styling + */ +function getFluxThemeStyling(theme) { + const isDark = theme === 'dark' || (theme === 'auto' && getSystemTheme() === 'dark'); + + if (isDark) { + return { + backgroundColor: 'oklab(0.999994 0.0000455678 0.0000200868 / 0.1)', + gutterBackgroundColor: 'oklch(26.9% 0 0)', + borderColor: '#374151', + focusBorderColor: 'rgb(224 91 68)', + }; + } else { + return { + backgroundColor: '#fff', // zinc-50 + gutterBackgroundColor: '#fafafa', // zinc-50 + borderColor: '#e5e7eb', // gray-200 + focusBorderColor: 'rgb(224 91 68)', // red-500 + }; + } +} + +/** + * Create CodeMirror editor instance + * @param {HTMLElement} element - DOM element to mount editor + * @param {Object} options - Editor options + * @returns {EditorView} CodeMirror editor instance + */ +export function createCodeMirror(element, options = {}) { + const { + value = '', + language = 'html', + theme = 'light', + readOnly = false, + onChange = () => {}, + onUpdate = () => {}, + onBlur = () => {} + } = options; + + // Get language and theme support + const languageSupport = getLanguageSupport(language); + const themeSupport = getThemeSupport(theme); + const fluxStyling = getFluxThemeStyling(theme); + + // Create editor + const editor = new EditorView({ + doc: value, + extensions: [ + lineNumbers(), + foldGutter(), + history(), + EditorView.lineWrapping, + createResizePlugin(), + ...(Array.isArray(languageSupport) ? languageSupport : [languageSupport]), + ...themeSupport, + keymap.of([ + indentWithTab, + ...foldKeymap, + ...historyKeymap, + ...searchKeymap, + { + key: 'Mod-a', + run: selectAll, + }, + ]), + EditorView.theme({ + '&': { + fontSize: '14px', + border: `1px solid ${fluxStyling.borderColor}`, + borderRadius: '0.375rem', + height: '100%', + maxHeight: '100%', + overflow: 'hidden', + backgroundColor: fluxStyling.backgroundColor + ' !important', + resize: 'vertical', + minHeight: '200px', + }, + '.cm-gutters': { + borderTopLeftRadius: '0.375rem', + backgroundColor: fluxStyling.gutterBackgroundColor + ' !important', + }, + '.cm-gutter': { + backgroundColor: fluxStyling.gutterBackgroundColor + ' !important', + }, + '&.cm-focused': { + outline: 'none', + borderColor: fluxStyling.focusBorderColor, + }, + '.cm-content': { + padding: '12px', + }, + '.cm-scroller': { + fontFamily: 'ui-monospace, SFMono-Regular, "SF Mono", Monaco, Consolas, "Liberation Mono", "Courier New", monospace', + height: '100%', + overflow: 'auto', + }, + '.cm-editor': { + height: '100%', + }, + '.cm-editor .cm-scroller': { + height: '100%', + overflow: 'auto', + }, + '.cm-foldGutter': { + width: '12px', + }, + '.cm-foldGutter .cm-gutterElement': { + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + cursor: 'pointer', + fontSize: '12px', + color: '#6b7280', + }, + '.cm-foldGutter .cm-gutterElement:hover': { + color: '#374151', + }, + '.cm-foldGutter .cm-gutterElement.cm-folded': { + color: '#3b82f6', + } + }), + EditorView.updateListener.of((update) => { + if (update.docChanged) { + const newValue = update.state.doc.toString(); + onChange(newValue); + onUpdate(newValue); + } + }), + EditorView.domEventHandlers({ + blur: onBlur + }), + EditorView.editable.of(!readOnly), + ], + parent: element + }); + + return editor; +} + +/** + * Auto-detect system theme preference + * @returns {string} 'dark' or 'light' + */ +export function getSystemTheme() { + if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) { + return 'dark'; + } + return 'light'; +} + +/** + * Watch for system theme changes + * @param {Function} callback - Callback function when theme changes + * @returns {Function} Unwatch function + */ +export function watchThemeChange(callback) { + if (window.matchMedia) { + const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); + mediaQuery.addEventListener('change', callback); + return () => mediaQuery.removeEventListener('change', callback); + } + return () => {}; +} 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/default-screens/setup.blade.php b/resources/views/default-screens/setup.blade.php new file mode 100644 index 0000000..3b0ff05 --- /dev/null +++ b/resources/views/default-screens/setup.blade.php @@ -0,0 +1,22 @@ +@props([ + 'noBleed' => false, + 'darkMode' => false, + 'deviceVariant' => 'og', + 'deviceOrientation' => null, + 'colorDepth' => '1bit', + 'scaleLevel' => null, +]) + + + + + + Welcome to BYOS Laravel! + Your device is connected. + + + + + diff --git a/resources/views/default-screens/sleep.blade.php b/resources/views/default-screens/sleep.blade.php new file mode 100644 index 0000000..89d6baa --- /dev/null +++ b/resources/views/default-screens/sleep.blade.php @@ -0,0 +1,28 @@ +@props([ + 'noBleed' => false, + 'darkMode' => true, + 'deviceVariant' => 'og', + 'deviceOrientation' => null, + 'colorDepth' => '1bit', + 'scaleLevel' => null, +]) + + + + + +
+ + + +
+ Sleep Mode +
+
+ +
+
diff --git a/resources/views/flux/icon/github.blade.php b/resources/views/flux/icon/github.blade.php new file mode 100644 index 0000000..1463734 --- /dev/null +++ b/resources/views/flux/icon/github.blade.php @@ -0,0 +1,42 @@ +{{-- Credit: Lucide (https://lucide.dev) --}} + +@props([ + 'variant' => 'outline', +]) + +@php +if ($variant === 'solid') { + throw new \Exception('The "solid" variant is not supported in Lucide.'); +} + +$classes = Flux::classes('shrink-0') + ->add(match($variant) { + 'outline' => '[:where(&)]:size-6', + 'solid' => '[:where(&)]:size-6', + 'mini' => '[:where(&)]:size-5', + 'micro' => '[:where(&)]:size-4', + }); + +$strokeWidth = match ($variant) { + 'outline' => 2, + 'mini' => 2.25, + 'micro' => 2.5, +}; +@endphp + +class($classes) }} + data-flux-icon + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 24 24" + fill="none" + stroke="currentColor" + stroke-width="{{ $strokeWidth }}" + stroke-linecap="round" + stroke-linejoin="round" + aria-hidden="true" + data-slot="icon" +> + + + diff --git a/resources/views/livewire/catalog/index.blade.php b/resources/views/livewire/catalog/index.blade.php new file mode 100644 index 0000000..fdf7f34 --- /dev/null +++ b/resources/views/livewire/catalog/index.blade.php @@ -0,0 +1,268 @@ +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::timeout(10)->get($catalogUrl); + $catalogContent = $response->body(); + $catalog = Yaml::parse($catalogContent); + + $currentVersion = config('app.version'); + + return collect($catalog) + ->filter(function ($plugin) use ($currentVersion) { + // Check if Laravel compatibility is true + if (! Arr::get($plugin, 'byos.byos_laravel.compatibility', false)) { + return false; + } + + // Check minimum version if specified + $minVersion = Arr::get($plugin, 'byos.byos_laravel.min_version'); + if ($minVersion && $currentVersion && version_compare($currentVersion, $minVersion, '<')) { + return false; + } + + return true; + }) + ->map(function ($plugin, $key) { + return [ + 'id' => $key, + 'name' => Arr::get($plugin, 'name', 'Unknown Plugin'), + 'description' => Arr::get($plugin, 'author_bio.description', ''), + 'author' => Arr::get($plugin, 'author.name', 'Unknown Author'), + 'github' => Arr::get($plugin, 'author.github'), + 'license' => Arr::get($plugin, 'license'), + 'zip_url' => Arr::get($plugin, 'trmnlp.zip_url'), + 'zip_entry_path' => Arr::get($plugin, 'trmnlp.zip_entry_path'), + 'repo_url' => Arr::get($plugin, 'trmnlp.repo'), + 'logo_url' => Arr::get($plugin, 'logo_url'), + 'screenshot_url' => Arr::get($plugin, 'screenshot_url'), + 'learn_more_url' => Arr::get($plugin, 'author_bio.learn_more_url'), + ]; + }) + ->sortBy('name') + ->toArray(); + } catch (Exception $e) { + Log::error('Failed to load catalog from URL: '.$e->getMessage()); + + return []; + } + }); + } + + public function installPlugin(string $pluginId, PluginImportService $pluginImportService): void + { + abort_unless(auth()->user() !== null, 403); + + $plugin = collect($this->catalogPlugins)->firstWhere('id', $pluginId); + + 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, + 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()); + } 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 +
+ @else +
+ @error('installation') + + @enderror + + @foreach($catalogPlugins as $plugin) +
+
+ @if($plugin['logo_url']) + {{ $plugin['name'] }} + @else +
+ +
+ @endif + +
+
+
+ {{ $plugin['name'] }} + @if ($plugin['github']) + by {{ $plugin['github'] }} + @endif +
+
+ @if($plugin['license']) + {{ $plugin['license'] }} + @endif + @if($plugin['repo_url']) + + + + @endif +
+
+ + @if($plugin['description']) + {{ $plugin['description'] }} + @endif + +
+ + Install + + + @if($plugin['screenshot_url']) + + + Preview + + + @endif + + + + @if($plugin['learn_more_url']) + + Learn More + + @endif +
+
+
+
+ @endforeach +
+ @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/codemirror.blade.php b/resources/views/livewire/codemirror.blade.php new file mode 100644 index 0000000..fad3e53 --- /dev/null +++ b/resources/views/livewire/codemirror.blade.php @@ -0,0 +1,64 @@ +language = $language; + $this->theme = $theme; + $this->readonly = $readonly; + $this->placeholder = $placeholder; + $this->height = $height; + $this->id = $id; + } + + + public function toJSON() + { + return json_encode([ + 'model' => $this->model, + 'language' => $this->language, + 'theme' => $this->theme, + 'readonly' => $this->readonly, + 'placeholder' => $this->placeholder, + 'height' => $this->height, + 'id' => $this->id, + ]); + } +} ?> + + +
+ +
+
+ + + + + Loading editor... +
+
+ + +
+
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 44e424c..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 @@ -383,7 +387,6 @@ new class extends Component { Edit TRMNL
- + + @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 241ff57..d902183 100644 --- a/resources/views/livewire/plugins/index.blade.php +++ b/resources/views/livewire/plugins/index.blade.php @@ -1,8 +1,13 @@ ['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 = [ @@ -30,13 +40,55 @@ new class extends Component { 'polling_body' => 'nullable|string', ]; - private function refreshPlugins(): void + public function refreshPlugins(): void { - $userPlugins = auth()->user()?->plugins?->map(function ($plugin) { - return $plugin->toArray(); - })->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); + $this->plugins = $allPlugins; + } - $this->plugins = array_merge($this->native_plugins, $userPlugins ?? []); + protected function sortPlugins(array $plugins): array + { + $pluginsToSort = array_values($plugins); + + switch ($this->sortBy) { + case 'name_asc': + usort($pluginsToSort, function($a, $b) { + return strcasecmp($a['name'] ?? '', $b['name'] ?? ''); + }); + break; + + case 'name_desc': + usort($pluginsToSort, function($a, $b) { + return strcasecmp($b['name'] ?? '', $a['name'] ?? ''); + }); + break; + + case 'date_desc': + usort($pluginsToSort, function($a, $b) { + $aDate = $a['created_at'] ?? '1970-01-01'; + $bDate = $b['created_at'] ?? '1970-01-01'; + return strcmp($bDate, $aDate); + }); + break; + + case 'date_asc': + usort($pluginsToSort, function($a, $b) { + $aDate = $a['created_at'] ?? '1970-01-01'; + $bDate = $b['created_at'] ?? '1970-01-01'; + return strcmp($aDate, $bDate); + }); + break; + } + + return $pluginsToSort; } public function mount(): void @@ -44,13 +96,25 @@ new class extends Component { $this->refreshPlugins(); } + public function updatedSortBy(): void + { + $this->refreshPlugins(); + } + + public function getListeners(): array + { + return [ + 'plugin-installed' => 'refreshPlugins', + ]; + } + public function addPlugin(): void { abort_unless(auth()->user() !== null, 403); $this->validate(); \App\Models\Plugin::create([ - 'uuid' => \Illuminate\Support\Str::uuid(), + 'uuid' => Str::uuid(), 'user_id' => auth()->id(), 'name' => $this->name, 'data_stale_minutes' => $this->data_stale_minutes, @@ -69,43 +133,191 @@ new class extends Component { public function seedExamplePlugins(): void { -// \Artisan::call('db:seed', ['--class' => 'ExampleRecipesSeeder']); - \Artisan::call(\App\Console\Commands\ExampleRecipesSeederCommand::class, ['user_id' => auth()->id()]); + Artisan::call(ExampleRecipesSeederCommand::class, ['user_id' => auth()->id()]); + $this->refreshPlugins(); + } + + public function importZip(PluginImportService $pluginImportService): void + { + abort_unless(auth()->user() !== null, 403); + + $this->validate([ + 'zipFile' => 'required|file|mimes:zip|max:10240', // 10MB max + ]); + + try { + $plugin = $pluginImportService->importFromZip($this->zipFile, auth()->user()); + + $this->refreshPlugins(); + $this->reset(['zipFile']); + + Flux::modal('import-zip')->close(); + } catch (\Exception $e) { + $this->addError('zipFile', 'Error installing plugin: ' . $e->getMessage()); + } } }; ?> -
+

Plugins & Recipes

+
+ + + + Add Recipe + - - - Add Recipe - - - - - - Seed Example Recipes - {{-- --}} - {{-- --}} - {{-- Import Recipe ZIP File--}} - {{-- --}} - {{-- --}} - {{-- --}} - {{-- New Native Plugin--}} - {{-- --}} - - - - - + + + + + Import from OSS Catalog + + @if(config('services.trmnl.liquid_enabled')) + + Import from TRMNL Catalog + + @endif + + + Import Recipe Archive + + + Seed Example Recipes + + + +
+ + + + + +
+
+ Import Recipe + Beta + + Upload a ZIP archive containing a TRMNL recipe — either exported from the cloud service or structured using the trmnlp project structure. +
+ +
+ The archive must at least contain settings.yml and full.liquid files. +{{--

The ZIP file should contain the following structure:

--}} +{{--
--}}
+{{--.--}}
+{{--├── src--}}
+{{--│   ├── full.liquid (required)--}}
+{{--│   ├── settings.yml (required)--}}
+{{--│   └── ...--}}
+{{--└── ...--}}
+{{--                    
--}} +
+ +
+ Limitations +
    +
  • Only full view will be imported; shared markup will be prepended
  • +
  • Some Liquid filters may be not supported or behave differently
  • +
  • API responses in formats other than JSON are not yet supported
  • +{{--
      --}} +{{--
    • date: "%N" is unsupported. Use date: "u" instead
    • --}} +{{--
    --}} +
+ Please report issues on GitHub. Include your example zip file. +
+ +
+
+ .zip Archive + + @error('zipFile') + + @enderror +
+ +
+ + Import +
+
+
+
+ + +
+
+ Import from Catalog + Beta + + Browse and install Recipes from the community. Add yours here. +
+ +
+
+ + +
+
+ Import from TRMNL Recipe Catalog + Alpha + + + Limitations +
    +
  • Only full view will be imported; shared markup will be prepended
  • +
  • Requires trmnl-liquid-cli executable.
  • +
  • API responses in formats other than JSON are not yet fully supported.
  • +
  • There are limitations in payload size (Data Payload, Template).
  • +
+ Please report issues, aside from the known limitations, on GitHub. Include the recipe URL. +
+
+ +
+
+
@@ -173,15 +385,26 @@ new class extends Component {
+ @php + $allPlugins = $this->plugins; + @endphp +
- @foreach($plugins as $plugin) + @foreach($allPlugins as $index => $plugin)
+ wire:key="plugin-{{ $plugin['id'] ?? $plugin['name'] ?? $index }}" + x-data="{ pluginName: {{ json_encode(strtolower($plugin['name'] ?? '')) }} }" + x-show="searchTerm.length <= 1 || pluginName.includes(searchTerm.toLowerCase())" + class="styled-container"> -
- +
+ @isset($plugin['icon_url']) + + @else + + @endif

{{$plugin['name']}}

diff --git a/resources/views/livewire/plugins/markup.blade.php b/resources/views/livewire/plugins/markup.blade.php index 4cea323..cb7823e 100644 --- a/resources/views/livewire/plugins/markup.blade.php +++ b/resources/views/livewire/plugins/markup.blade.php @@ -70,11 +70,11 @@ new class extends Component { - + TRMNL BYOS Laravel “This screen was rendered by BYOS Laravel” Benjamin Nussbaum - + @@ -88,11 +88,11 @@ HTML; - + Motivational Quote “I love inside jokes. I hope to be a part of one someday.” Michael Scott - + diff --git a/resources/views/livewire/plugins/recipe.blade.php b/resources/views/livewire/plugins/recipe.blade.php index 32f6e18..0e29e76 100644 --- a/resources/views/livewire/plugins/recipe.blade.php +++ b/resources/views/livewire/plugins/recipe.blade.php @@ -1,9 +1,16 @@ user()->plugins->contains($this->plugin), 403); + $this->blade_code = $this->plugin->render_markup; + // required to render some stuff + $this->configuration_template = $this->plugin->configuration_template ?? []; if ($this->plugin->render_markup_view) { try { @@ -56,8 +71,18 @@ new class extends Component { $this->markup_language = $this->plugin->markup_language ?? 'blade'; } + // Initialize screen settings from the model + $this->no_bleed = (bool) ($this->plugin->no_bleed ?? false); + $this->dark_mode = (bool) ($this->plugin->dark_mode ?? false); + $this->fillformFields(); $this->data_payload_updated_at = $this->plugin->data_payload_updated_at; + + // Set default preview device model + if ($this->preview_device_model_id === null) { + $defaultModel = DeviceModel::where('name', 'og_plus')->first() ?? DeviceModel::first(); + $this->preview_device_model_id = $defaultModel?->id; + } } public function fillFormFields(): void @@ -86,7 +111,7 @@ new class extends Component { 'name' => 'required|string|max:255', 'data_stale_minutes' => 'required|integer|min:1', 'data_strategy' => 'required|string|in:polling,webhook,static', - 'polling_url' => 'required_if:data_strategy,polling|nullable|url', + 'polling_url' => 'required_if:data_strategy,polling|nullable', 'polling_verb' => 'required|string|in:get,post', 'polling_header' => 'nullable|string|max:255', 'polling_body' => 'nullable|string', @@ -94,19 +119,53 @@ new class extends Component { 'markup_code' => 'nullable|string', 'markup_language' => 'nullable|string|in:blade,liquid', 'checked_devices' => 'array', - 'playlist_name' => 'required_if:selected_playlist,new|string|max:255', - 'selected_weekdays' => 'nullable|array', - 'active_from' => 'nullable|date_format:H:i', - 'active_until' => 'nullable|date_format:H:i', - 'selected_playlist' => 'nullable|string', + 'device_playlist_names' => 'array', + 'device_playlists' => 'array', + 'device_weekdays' => 'array', + 'device_active_from' => 'array', + 'device_active_until' => 'array', + 'no_bleed' => 'boolean', + 'dark_mode' => 'boolean', ]; public function editSettings() { abort_unless(auth()->user()->plugins->contains($this->plugin), 403); + + // Custom validation for polling_url with Liquid variable resolution + $this->validatePollingUrl(); + $validated = $this->validate(); $validated['data_payload'] = json_decode(Arr::get($validated,'data_payload'), true); $this->plugin->update($validated); + + foreach ($this->configuration_template as $fieldKey => $field) { + if (($field['field_type'] ?? null) !== 'multi_string') { + continue; + } + + if (!isset($this->multiValues[$fieldKey])) { + continue; + } + + $validated[$fieldKey] = implode(',', array_filter(array_map('trim', $this->multiValues[$fieldKey]))); + } + + } + + protected function validatePollingUrl(): void + { + if ($this->data_strategy === 'polling' && !empty($this->polling_url)) { + try { + $resolvedUrl = $this->plugin->resolveLiquidVariables($this->polling_url); + + if (!filter_var($resolvedUrl, FILTER_VALIDATE_URL)) { + $this->addError('polling_url', 'The polling URL must be a valid URL after resolving configuration variables.'); + } + } catch (\Exception $e) { + $this->addError('polling_url', 'Error resolving Liquid variables: ' . $e->getMessage() . $e->getPrevious()?->getMessage()); + } + } } public function updateData(): void @@ -119,7 +178,7 @@ new class extends Component { $this->data_payload_updated_at = $this->plugin->data_payload_updated_at; } catch (\Exception $e) { - $this->dispatch('data-update-error', message: $e->getMessage()); + $this->dispatch('data-update-error', message: $e->getMessage() . $e->getPrevious()?->getMessage()); } } } @@ -147,29 +206,40 @@ new class extends Component { { $this->validate([ 'checked_devices' => 'required|array|min:1', - 'selected_playlist' => 'required|string', 'mashup_layout' => 'required|string', 'mashup_plugins' => 'required_if:mashup_layout,1Lx1R,1Lx2R,2Lx1R,1Tx1B,2Tx1B,1Tx2B,2x2|array', ]); + // Validate that each checked device has a playlist selected + 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 creating new playlist, validate required fields + 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->selected_playlist === 'new') { + if ($this->device_playlists[$deviceId] === 'new') { // Create new playlist - $this->validate([ - 'playlist_name' => 'required|string|max:255', - ]); - $playlist = \App\Models\Playlist::create([ 'device_id' => $deviceId, - 'name' => $this->playlist_name, - 'weekdays' => !empty($this->selected_weekdays) ? $this->selected_weekdays : null, - 'active_from' => $this->active_from ?: null, - 'active_until' => $this->active_until ?: null, + '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->selected_playlist); + $playlist = \App\Models\Playlist::findOrFail($this->device_playlists[$deviceId]); } // Add plugin to playlist @@ -193,7 +263,16 @@ new class extends Component { } } - $this->reset(['checked_devices', 'playlist_name', 'selected_weekdays', 'active_from', 'active_until', 'selected_playlist', 'mashup_layout', 'mashup_plugins']); + $this->reset([ + 'checked_devices', + 'device_playlists', + 'device_playlist_names', + 'device_weekdays', + 'device_active_from', + 'device_active_until', + 'mashup_layout', + 'mashup_plugins' + ]); Flux::modal('add-to-playlist')->close(); } @@ -202,6 +281,21 @@ new class extends Component { 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 getConfigurationValue($key, $default = null) + { + return $this->configuration[$key] ?? $default; + } + public function renderExample(string $example) { switch ($example) { @@ -270,22 +364,89 @@ HTML; { abort_unless(auth()->user()->plugins->contains($this->plugin), 403); + $this->preview_size = $size; + + // If data strategy is polling and data_payload is null, fetch the data first + if ($this->plugin->data_strategy === 'polling' && $this->plugin->data_payload === null) { + $this->updateData(); + } + try { - $previewMarkup = $this->plugin->render($size); + // Create a device object with og_plus model and the selected bitdepth + $device = $this->createPreviewDevice(); + $previewMarkup = $this->plugin->render($size, true, $device); $this->dispatch('preview-updated', preview: $previewMarkup); + } catch (LiquidException $e) { + $this->dispatch('preview-error', message: $e->toLiquidErrorMessage()); } catch (\Exception $e) { $this->dispatch('preview-error', message: $e->getMessage()); } } + private function createPreviewDevice(): \App\Models\Device + { + $deviceModel = DeviceModel::with(['palette'])->find($this->preview_device_model_id) + ?? DeviceModel::with(['palette'])->first(); + + $device = new Device(); + $device->setRelation('deviceModel', $deviceModel); + + return $device; + } + + public function getDeviceModels() + { + return DeviceModel::whereKind('trmnl')->orderBy('label')->get(); + } + + public function updatedPreviewDeviceModelId(): void + { + $this->renderPreview($this->preview_size); + } + + public function duplicatePlugin(): void + { + abort_unless(auth()->user()->plugins->contains($this->plugin), 403); + + // Use the model's duplicate method + $newPlugin = $this->plugin->duplicate(auth()->id()); + + // Redirect to the new plugin's detail page + $this->redirect(route('plugins.recipe', ['plugin' => $newPlugin])); + } + public function deletePlugin(): void { abort_unless(auth()->user()->plugins->contains($this->plugin), 403); $this->plugin->delete(); $this->redirect(route('plugins.index')); } -} + #[On('config-updated')] + public function refreshPlugin() + { + // This pulls the fresh 'configuration' from the DB + // and re-triggers the @if check in the Blade template + $this->plugin = $this->plugin->fresh(); + } + + // Laravel Livewire computed property: access with $this->parsed_urls + #[Computed] + private function parsedUrls() + { + if (!isset($this->polling_url)) { + return null; + } + + try { + return $this->plugin->resolveLiquidVariables($this->polling_url); + + } catch (\Exception $e) { + return 'PARSE_ERROR: ' . $e->getMessage(); + } + } + +} ?>
@@ -297,36 +458,40 @@ HTML; - Preview + Preview - + - Half-Horizontal + Half-Horizontal - Half-Vertical + Half-Vertical - Quadrant + Quadrant - - Add to Playlist + Add to Playlist + + Recipe Settings + + + Duplicate Plugin Delete Plugin @@ -351,44 +516,60 @@ HTML;
- @if(count($checked_devices) === 1) - -
- - - @foreach($this->getDevicePlaylists($checked_devices[0]) as $playlist) - - @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 - @if($selected_playlist) - @if($selected_playlist === 'new') -
- -
-
- - - - - - - - - -
- -
- -
- -
- -
- @endif + @if(count($checked_devices) > 0 && $this->hasAnyPlaylistSelected())
@@ -429,7 +610,7 @@ HTML;
- Add to Playlist + Add to Playlist
@@ -452,8 +633,15 @@ HTML; -
+
Preview {{ $plugin->name }} + + + @foreach($this->getDeviceModels() as $model) + + @endforeach + +
@@ -461,6 +649,10 @@ HTML;
+ + + +

Settings

@@ -472,6 +664,85 @@ HTML; name="name" autofocus/>
+ @php + $authorField = null; + if (isset($configuration_template['custom_fields'])) { + foreach ($configuration_template['custom_fields'] as $field) { + if ($field['field_type'] === 'author_bio') { + $authorField = $field; + break; + } + } + } + @endphp + + @if($authorField) +
+
+ {{ $authorField['description'] }} +
+ + @if(isset($authorField['github_url']) || isset($authorField['learn_more_url']) || isset($authorField['email_address'])) +
+ @if(isset($authorField['github_url'])) + @php + $githubUrl = $authorField['github_url']; + $githubUsername = null; + + // Extract username from various GitHub URL formats + if (preg_match('/github\.com\/([^\/\?]+)/', $githubUrl, $matches)) { + $githubUsername = $matches[1]; + } + @endphp + @if($githubUsername)@endif + @endif + @if(isset($authorField['learn_more_url'])) + + Learn More + + @endif + + @if(isset($authorField['github_url'])) + + + @endif + + @if(isset($authorField['email_address'])) + + + @endif +
+ @endif +
+ @endif + + @if(isset($configuration_template['custom_fields']) && !empty($configuration_template['custom_fields'])) + @if($plugin->hasMissingRequiredConfigurationFields()) + + @endif +
+ + Configuration Fields + +
+ @endif
@@ -481,18 +752,63 @@ HTML;
@if($data_strategy === 'polling') -
- - - - - + Polling URL + +
+
+ + +
+
+
+ + Enter the URL(s) to poll for data: + + + {!! 'Hint: Supports multiple requests via line break separation. You can also use configuration variables with Liquid syntax. ' !!} + + +
+ +
+ + Preview computed URLs here (readonly): + + {{ $this->parsed_urls }} + + +
+ + + Fetch data now + +
+
+
@@ -533,6 +849,7 @@ HTML;
-
-

Send JSON payload with key merge_variables to the webhook URL. The payload - will be merged with the plugin data.

-
@elseif($data_strategy === 'static') -
-

Enter static JSON data in the Data Payload field.

-
+ Enter static JSON data in the Data Payload field. @endif +
+ Screen Settings +
+ + +
+
+
- Save + Save
- Data Payload - @isset($this->data_payload_updated_at) - {{ $this->data_payload_updated_at?->diffForHumans() ?? 'Never' }} - @endisset +
+ Data Payload + @isset($this->data_payload_updated_at) + {{ $this->data_payload_updated_at?->diffForHumans() ?? 'Never' }} + @endisset +
- + + @php + $textareaId = 'payload-' . uniqid(); + @endphp + + +
@@ -576,15 +937,40 @@ HTML; {{ $plugin->render_markup_view }} to update.
- + + @php + $textareaId = 'code-view-' . uniqid(); + @endphp + +
@else
@@ -606,15 +992,41 @@ HTML; @if(!$plugin->render_markup_view)
- + + @php + $textareaId = 'code-' . uniqid(); + @endphp + {{ $markup_language === 'liquid' ? 'Liquid Code' : 'Blade Code' }} + +
@@ -627,6 +1039,8 @@ HTML;
+ + @script ', 'Safe ', '