diff --git a/.devcontainer/cli/Dockerfile b/.devcontainer/cli/Dockerfile index 908acef..d2c44b1 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,27 @@ RUN apk add --no-cache composer # Add Chromium and Image Magick for puppeteer. RUN apk add --no-cache \ imagemagick-dev \ - chromium + chromium \ + libzip-dev \ + freetype-dev \ + libpng-dev \ + libjpeg-turbo-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 + +RUN docker-php-ext-configure gd --with-freetype --with-jpeg # Install PHP extensions -RUN docker-php-ext-install imagick +RUN docker-php-ext-install imagick zip gd # 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..d8ce6cc 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,24 @@ RUN apk add --no-cache \ nodejs \ npm \ imagemagick-dev \ - chromium + chromium \ + libzip-dev \ + freetype-dev \ + libpng-dev \ + libjpeg-turbo-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 + +RUN docker-php-ext-configure gd --with-freetype --with-jpeg # Install PHP extensions -RUN docker-php-ext-install imagick +RUN docker-php-ext-install imagick zip gd -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/.dockerignore b/.dockerignore index 32d49fe..9de2cf4 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,4 +1,8 @@ +/.devcontainer +/.github /.phpunit.cache +/database/*.sqlite +/bootstrap/cache/* /node_modules /public/build /public/hot @@ -6,6 +10,7 @@ /storage/*.key /storage/pail /vendor +.editorconfig .env .env.backup .env.production @@ -20,5 +25,3 @@ yarn-error.log /.idea /.vscode /.zed -/bootstrap/cache/* -/database/database.sqlite diff --git a/.env.example b/.env.example index de1aadc..7d64dce 100644 --- a/.env.example +++ b/.env.example @@ -68,3 +68,10 @@ VITE_APP_NAME="${APP_NAME}" TRMNL_PROXY_BASE_URL=https://trmnl.app TRMNL_PROXY_REFRESH_MINUTES=15 REGISTRATION_ENABLED=1 + +PUPPETEER_MODE= +SIDECAR_ACCESS_KEY_ID= +SIDECAR_SECRET_ACCESS_KEY= +SIDECAR_REGION= +SIDECAR_ARTIFACT_BUCKET_NAME= +SIDECAR_EXECUTION_ROLE= diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index 6bd92b2..ab2802d 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1 +1 @@ -custom: ["https://usetrmnl.com/?ref=laravel-trmnl"] +custom: ["https://trmnl.com/?ref=laravel-trmnl"] diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml index 179d070..c8327d3 100644 --- a/.github/workflows/docker-build.yml +++ b/.github/workflows/docker-build.yml @@ -6,7 +6,6 @@ on: env: REGISTRY: ghcr.io - IMAGE_NAME: ${{ github.repository }} jobs: build-and-push: @@ -19,6 +18,10 @@ jobs: - name: Checkout repository uses: actions/checkout@v4 + - name: Extract version from tag + id: get_version + run: echo "VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV + - name: Set up QEMU uses: docker/setup-qemu-action@v3 @@ -36,10 +39,11 @@ jobs: id: meta uses: docker/metadata-action@v5 with: - images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + images: | + ${{ env.REGISTRY }}/usetrmnl/byos_laravel + ${{ env.REGISTRY }}/usetrmnl/larapaper tags: | - type=ref,event=tag - latest + type=semver,pattern={{version}} - name: Build and push Docker image uses: docker/build-push-action@v6 @@ -51,3 +55,6 @@ jobs: labels: ${{ steps.meta.outputs.labels }} cache-from: type=gha cache-to: type=gha,mode=max + target: production + build-args: | + APP_VERSION=${{ env.VERSION }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index cbb9253..78e4fbb 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -12,7 +12,7 @@ on: jobs: ci: - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 environment: Testing steps: @@ -23,7 +23,6 @@ jobs: uses: shivammathur/setup-php@v2 with: php-version: 8.4 - tools: composer:v2 coverage: xdebug - name: Setup Node @@ -32,9 +31,6 @@ jobs: node-version: '22' cache: 'npm' - - name: Install Node Dependencies - run: npm i - - name: Install Dependencies run: composer install --no-interaction --prefer-dist --optimize-autoloader @@ -45,7 +41,9 @@ jobs: run: php artisan key:generate - name: Build Assets - run: npm run build + run: | + npm ci --no-audit + npm run build - name: Run Tests run: ./vendor/bin/pest --ci --coverage diff --git a/.gitignore b/.gitignore index 33806df..838d9c1 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,23 @@ yarn-error.log /.vscode /.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 +/.cursor +/.opencode +/build.sh +/.junie +/.agents diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..7bc786e --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,17 @@ +### CONTRIBUTING +Contributions are welcome! If you’d like to improve the project, follow these steps: + +1. Open an Issue + - Before submitting a pull request, create an issue to discuss your idea. + - Clearly describe the feature or bug fix you want to work on. +2. Fork the Repository & Create a Branch +3. Make Your Changes & Add Tests + - Ensure your code follows best practices. + - Add Pest tests to cover your changes. +4. Run Tests + - `php artisan test` +5. Submit a Pull Request (PR) + - Push your branch and create a PR. + - Provide a clear description of your changes. + +Thank you for contributing! diff --git a/Dockerfile b/Dockerfile index 75293d5..4dc531b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,37 +1,57 @@ -FROM bnussbau/php:8.3-fpm-opcache-imagick-puppeteer-alpine3.20 +######################## +# Base Image +######################## +FROM bnussbau/serversideup-php:8.4-fpm-nginx-alpine-imagick-chromium@sha256:ed705a4060d50143ddc538c1288afff217eaf76ad5791f7556a97943854cf745 AS base -# Install composer -COPY --from=composer:latest /usr/bin/composer /usr/bin/composer +LABEL org.opencontainers.image.source=https://github.com/usetrmnl/larapaper +LABEL org.opencontainers.image.description="LaraPaper" +LABEL org.opencontainers.image.licenses=MIT -# Set working directory +ARG APP_VERSION +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.2.0 /usr/local/bin/trmnl-liquid-cli /usr/local/bin/ + +# Set the working directory WORKDIR /var/www/html -# Copy configuration files -COPY docker/nginx.conf /etc/nginx/http.d/default.conf -COPY docker/supervisord.conf /etc/supervisor/conf.d/supervisord.conf -COPY docker/php.ini /usr/local/etc/php/conf.d/custom.ini +# Copy the application files +COPY --chown=www-data:www-data . /var/www/html +COPY --chown=www-data:www-data .env.example .env -# Create required directories -RUN mkdir -p /var/log/supervisor \ - && mkdir -p storage/logs \ - && mkdir -p storage/framework/{cache,sessions,views} \ - && chmod -R 775 storage \ - && mkdir -p bootstrap/cache \ - && chmod -R 775 bootstrap/cache \ - && mkdir -p database \ - && touch database/database.sqlite \ - && chmod -R 777 database +# Install the composer dependencies +RUN composer install --no-dev --no-interaction --prefer-dist --optimize-autoloader -# Copy application files -COPY --chown=www-data:www-data . . -COPY --chown=www-data:www-data ./.env.example ./.env +######################## +# Assets Image +######################## +FROM node:22-alpine AS assets -# Install application dependencies -RUN composer install --no-interaction --prefer-dist --optimize-autoloader -RUN npm ci && npm run build +# Copy the application +COPY --from=base /var/www/html /app -# Expose port 80 -EXPOSE 80 +# Set the working directory +WORKDIR /app -# Start supervisor -CMD ["/usr/bin/supervisord", "-n", "-c", "/etc/supervisor/conf.d/supervisord.conf"] +# Install the node dependencies and build the assets +RUN npm ci --no-audit \ + && npm run build + +######################## +# Production Image +######################## +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 76cb413..c1ebafe 100644 --- a/README.md +++ b/README.md @@ -1,106 +1,143 @@ -## TRMNL BYOS (PHP/Laravel) +## LaraPaper (PHP/Laravel) -[![tests](https://github.com/usetrmnl/byos_laravel/actions/workflows/test.yml/badge.svg)](https://github.com/usetrmnl/byos_laravel/actions/workflows/test.yml) +[![tests](https://github.com/usetrmnl/larapaper/actions/workflows/test.yml/badge.svg)](https://github.com/usetrmnl/larapaper/actions/workflows/test.yml) -Laravel Trmnl Server 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 TRMNL API (native plugin system). -Inspired by [usetrmnl/byos_sinatra](https://github.com/usetrmnl/byos_sinatra). - -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). +LaraPaper is a self-hostable implementation of a TRMNL server (BYOS), built with Laravel. +It allows you to manage TRMNL devices, generate screens using **native plugins** (Screens API, Markup), **recipes** (130+ from the [OSS community catalog](https://bnussbau.github.io/trmnl-recipe-catalog/), 700+ from the [TRMNL catalog](https://trmnl.com/recipes), or your own), or the **API**, and can also act as a **proxy** for the native cloud service (Core). With over 50k downloads and 200+ stars, it’s the most popular community-driven BYOS. ![Screenshot](README_byos-screenshot.png) +![Screenshot](README_byos-screenshot-dark.png) * πŸ‘‰ [more Screenshots](screenshots/SCREENSHOTS.md) ### Key Features -* πŸ“‘ 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 Markup, API, or update via Code. -* πŸ”„ TRMNL API Proxy – Can act as a proxy for the TRMNL Display API (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. +* πŸ“‘ 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 (including Mashups), Recipes, API, Markup, or updates via Code. + * Support for TRMNL [Design Framework](https://trmnl.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://trmnl.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/larapaper/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. -### 🎯 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. +![Devices](README_byos-devices.jpeg) ### Support ❀️ This repo is maintained voluntarily by [@bnussbau](https://github.com/bnussbau). -Support the development of this package by purchasing a TRMNL device through our referral link: https://usetrmnl.com/?ref=laravel-trmnl. At checkout, use the code `laravel-trmnl` to receive a $15 discount on your purchase. +Support the development of this package by purchasing a TRMNL device through the referral link: https://trmnl.com/?ref=laravel-trmnl. At checkout, use the code `laravel-trmnl` to receive a $15 discount on your purchase. -### Requirements +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, ...). +For production use, generate a new APP_KEY (`php artisan key:generate --show`) and set the environment variable `APP_KEY=`. For personal use, you can disable registration (see section Environment Variables). + +#### Docker Compose +Docker Compose file located at: [docker/prod/docker-compose.yml](docker/prod/docker-compose.yml). + +##### Backup Database +```sh +docker ps #find container id of larapaper container +docker cp {{CONTAINER_ID}}:/var/www/html/database/storage/database.sqlite database_backup.sqlite +``` + +##### Updating via Docker Compose +```sh +docker compose pull +docker compose down +docker compose up -d +``` + +#### VPS +If you’re using a VPS (e.g., Hetzner) and prefer an alternative to native Docker, you can install Dokploy and deploy LaraPaper 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 +You can vote for LaraPaper 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. + +#### Requirements * PHP >= 8.2 * ext-imagick * puppeteer [see Browsershot docs](https://spatie.be/docs/browsershot/v4/requirements) -### Installation +### Local Development -#### Clone the repository +see [docs/DEVELOPMENT.md](docs/DEVELOPMENT.md) + + +### Demo Plugins + +Run the ExampleRecipesSeeder to seed the database with example plugins: ```bash -git clone git@github.com:usetrmnl/byos_laravel.git +php artisan db:seed --class=ExampleRecipesSeeder ``` -#### Copy environment file - -```bash -cp .env.example .env -php artisan key:generate -``` - -#### Install dependencies - -```bash -composer install -npm i -``` - -#### Run migrations - -```bash -php artisan migrate --seed -``` - -#### Run the server - -To make your server accessible in the network, you can run the following command: - -```bash -php artisan serve --host=0.0.0.0 --port 4567 -``` - -### Docker -Use the provided Dockerfile, or docker-compose file to run the server in a container. -You can persist the database file by mounting a volume to `/var/www/html/database/database.sqlite`. - -```Dockerfile -# docker-compose.yaml -volumes: - - ./database/database.sqlite:/var/www/html/database/database.sqlite -``` +* Zen Quotes +* This Day in History +* Weather +* Train Departure Monitor +* Home Assistant +* Sunrise/Sunset ### Usage #### Environment Variables -| environment | description | default | -|-------------------------------|------------------------------------------------------------------|-------------------| -| `TRMNL_PROXY_BASE_URL` | Base URL of the native TRMNL service | https://trmnl.app | -| `TRMNL_PROXY_REFRESH_MINUTES` | How often should the server fetch new images from native service | 15 | -| `REGISTRATION_ENABLED` | Allow user registration via Webinterface | 1 | -| `FORCE_HTTPS` | If your server handles SSL termination, enforce HTTPS. | 0 | +| Environment Variable | Description | Default | +|-------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------|-------------------| +| `TRMNL_PROXY_BASE_URL` | Base URL of the native TRMNL service | https://trmnl.app | +| `TRMNL_PROXY_REFRESH_MINUTES` | How often should the server fetch new images from native service | 15 | +| `REGISTRATION_ENABLED` | Allow user registration via Webinterface | 1 | +| `SSL_MODE` | SSL Mode, if not using a Reverse Proxy ([docs](https://serversideup.net/open-source/docker-php/docs/customizing-the-image/configuring-ssl)) | `off` | +| `FORCE_HTTPS` | If your server handles SSL termination, enforce HTTPS. Alternative: `TRUSTED_PROXIES`. | 0 | +| `TRUSTED_PROXIES` | If your server handles SSL termination, allow mixed mode. e.g. `"172.0.0.0/8"` or `*` | null | +| `PHP_OPCACHE_ENABLE` | Enable PHP Opcache | 0 | +| `TRMNL_IMAGE_URL_TIMEOUT` | How long TRMNL waits for a response on the display endpoint. (sec) | 30 | +| `APP_TIMEZONE` | Default timezone, which will be used by the PHP date functions. UTC is recommended. | UTC | + +##### Experimental Environment Variables +| Environment Variable | Description | Default | +|----------------------------------|--------------------------------------------------------------------------------|---------| +| `PUPPETEER_WINDOW_SIZE_STRATEGY` | Set to `v2` to size the browser window to match the device’s screen dimensions | `null` | #### Login If your environment is local, you can access the server at `http://localhost:4567` and login with user / password `admin@example.com` / `admin@example.com`, otherwise register. With environment variable `REGISTRATION_ENABLED` you can control, if registration is allowed. -#### βž• Add Your TRMNL Device +### βž• Add Your TRMNL Device ##### Auto-Join (Local Network) @@ -112,11 +149,12 @@ If your environment is local, you can access the server at `http://localhost:456 ##### Manually 1. Open the Devices page: -πŸ‘‰ http://localhost:4567/devices + πŸ‘‰ http://localhost:4567/devices 2. Click β€œAdd New Device”. 3. Retrieve your TRMNL MAC Address and API Key: - - You can grab the TRMNL Mac Address and API Key from the TRMNL Dashboard - - Alternatively, debug incoming requests to /api/setup to determine them +- You can grab the TRMNL Mac Address and API Key from the TRMNL Dashboard +- Alternatively, debug incoming requests to /api/setup to determine them + ### βš™οΈ Configure Server for Device @@ -132,11 +170,37 @@ If your device firmware is older than 1.4.6, you need to flash a new firmware ve See this YouTube guide: [https://www.youtube.com/watch?v=3xehPW-PCOM](https://www.youtube.com/watch?v=3xehPW-PCOM) +### ☁️ Activate fresh TRMNL Device with Cloud Proxy + +1) Setup the TRMNL as in the official docs with the cloud service (connect one of the plugins to later verify it works) +2) Setup LaraPaper, create a user and login +3) In LaraPaper in the header bar, activate the toggle "Permit Auto-Join" +4) Press and hold the button on the back of your TRMNL for 5 seconds to reactivate the captive portal (or reflash). +5) Go through the setup process again, in the screen where you provide the Wi-Fi credentials there is also option to set the Server URL. Use the local address of LaraPaper. +6) The device should automatically appear in the device list; you can deactivate the "Permit Auto-Join" toggle again. +7) In the devices list, activate the toggle "☁️ Proxy" for your device. (Make sure that the queue worker is active. In the docker image it should be running automatically.) +8) As long as no LaraPaper plugin is scheduled, the device will show your cloud plugins. + +###### Troubleshooting + +Make sure that your device has a Developer license, you should be able to verify by calling the `https://trmnl.app/api/display` endpoint. + +* [https://docs.usetrmnl.com/go/private-api/introduction](https://docs.usetrmnl.com/go/private-api/introduction) +* [https://docs.usetrmnl.com/go/private-api/fetch-screen-content](https://docs.usetrmnl.com/go/private-api/fetch-screen-content) + ### πŸ–₯️ Generate Screens +#### Markup via Web Interface + +1. Navigate to Plugins > Markup in the Web Interface. +2. Enter your markup manually or select from the available templates. +3. Save and apply the changes. + +* Available Blade Components are listed here: [laravel-trmnl-blade | Blade Components](https://github.com/bnussbau/laravel-trmnl-blade/tree/main/resources/views/components) + #### 🎨 Blade View * Edit `resources/views/trmnl.blade.php` - * Available Blade Components are listed here: [laravel-trmnl | Blade Components](https://github.com/bnussbau/laravel-trmnl/tree/main/resources/views/components) + * Available Blade Components are listed here: [laravel-trmnl-blade | Blade Components](https://github.com/bnussbau/laravel-trmnl-blade/tree/main/resources/views/components) * To generate the screen, run ```bash @@ -160,109 +224,15 @@ You can dynamically update screens by sending a POST request. } ``` -Token can be retrieved under Plugins > API in the Web Interface. +### 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. -#### Markup via Web Interface - -1. Navigate to Plugins > Markup in the Web Interface. -2. Enter your markup manually or select from the available templates. -3. Save and apply the changes. - -* Available Blade Components are listed here: [laravel-trmnl | Blade Components](https://github.com/bnussbau/laravel-trmnl/tree/main/resources/views/components) - -#### πŸ› οΈ Generate Screens Programmatically - -You can fetch external data, process it, and generate screens dynamically. -* Fetch data from an external source. -* Either render it in a Blade view or directly insert markup. -* Use Laravel’s scheduler to automate updates. - -#### πŸ“Œ Example: Fetch Train Monitor Data - -This example retrieves data from [trmnl-train-monitor](https://github.com/bnussbau/trmnl-train-monitor) and updates the screen periodically. - -##### Step 1: Create a new Artisan Command - -```bash -php artisan make:command PluginTrainMonitorFetch -``` - -##### Step 2: Edit PluginTrainMonitorFetch.php - -```php -class PluginTrainMonitorFetch extends Command -{ - protected $signature = 'plugin:train:fetch'; - - protected $description = 'Fetches train monitor data and updates the screen'; - - public function handle(): void - { - $markup = Http::get('https://oebb.trmnl.yourserver.at/markup')->json('markup'); - GenerateScreenJob::dispatchSync(1, $markup); - } -} -``` - -##### Step 3: Schedule the Command in console.php - -```php -Schedule::command('plugin:train:fetch') - ->everyFiveMinutes() - ->timezone('Europe/Vienna') - ->between('5:00', '18:00'); -``` - -This will automatically update the screen every 5 minutes between 5:00 AM and 6:00 PM local time. - -### πŸ—οΈ Roadmap - -Here are some features and improvements that are open for contribution: - -##### πŸ”Œ Plugin System - -- ~~Enable configurable plugins via the Web Interface.~~ βœ… -- Ensure compatibility with the trmnl-laravel package. -- Implement auto-discovery for plugins. - -##### ⏳ Scheduling / Playlists - -- Move task scheduling from console.php to a Web Interface. -- ~~Allow users to configure custom schedule intervals.~~ βœ… - -##### πŸ–₯️ β€œNative” Plugins -- Add built-in plugins such as (as an example): - - ☁️ Weather - - πŸ’¬ Quotes - - 🏑 Home Assistant integration -- Provide Web UI controls to enable/disable plugins. - -##### πŸ“¦ Visual Studio Code Devcontainer -* ~~Add a .devcontainer to this repo for easier development with Docker.~~ βœ… - -##### Improve Code Coverage - -- Expand Pest tests to cover more functionality. -- Increase code coverage (currently at 86.9%). ### 🀝 Contribution -Contributions are welcome! If you’d like to improve the project, follow these steps: - -1. Open an Issue - - Before submitting a pull request, create an issue to discuss your idea. - - Clearly describe the feature or bug fix you want to work on. -2. Fork the Repository & Create a Branch -3. Make Your Changes & Add Tests - - Ensure your code follows best practices. - - Add Pest tests to cover your changes. -4. Run Tests - - `php artisan test` -5. Submit a Pull Request (PR) - - Push your branch and create a PR. - - Provide a clear description of your changes. - -Thank you for contributing! +Contributions are welcome! See [CONTRIBUTING.md](CONTRIBUTING.md) for details. ### License -MIT +[MIT](LICENSE.md) diff --git a/README_byos-devices.jpeg b/README_byos-devices.jpeg new file mode 100644 index 0000000..1e49a2c Binary files /dev/null and b/README_byos-devices.jpeg differ diff --git a/README_byos-screenshot-dark.png b/README_byos-screenshot-dark.png new file mode 100644 index 0000000..2b8174e Binary files /dev/null and b/README_byos-screenshot-dark.png differ diff --git a/README_byos-screenshot.png b/README_byos-screenshot.png index 7cd524a..b84d090 100644 Binary files a/README_byos-screenshot.png and b/README_byos-screenshot.png differ diff --git a/app/Actions/Fortify/CreateNewUser.php b/app/Actions/Fortify/CreateNewUser.php new file mode 100644 index 0000000..3c7c00c --- /dev/null +++ b/app/Actions/Fortify/CreateNewUser.php @@ -0,0 +1,33 @@ + $input + */ + public function create(array $input): User + { + Validator::make($input, [ + ...$this->profileRules(), + 'password' => $this->passwordRules(), + ])->validate(); + + return User::create([ + 'name' => $input['name'], + 'email' => $input['email'], + 'password' => $input['password'], + ]); + } +} diff --git a/app/Actions/Fortify/ResetUserPassword.php b/app/Actions/Fortify/ResetUserPassword.php new file mode 100644 index 0000000..8fda5dd --- /dev/null +++ b/app/Actions/Fortify/ResetUserPassword.php @@ -0,0 +1,29 @@ + $input + */ + public function reset(User $user, array $input): void + { + Validator::make($input, [ + 'password' => $this->passwordRules(), + ])->validate(); + + $user->forceFill([ + 'password' => $input['password'], + ])->save(); + } +} diff --git a/app/Concerns/PasswordValidationRules.php b/app/Concerns/PasswordValidationRules.php new file mode 100644 index 0000000..9b45ef0 --- /dev/null +++ b/app/Concerns/PasswordValidationRules.php @@ -0,0 +1,28 @@ +|string> + */ + protected function passwordRules(): array + { + return ['required', 'string', Password::default(), 'confirmed']; + } + + /** + * Get the validation rules used to validate the current password. + * + * @return array|string> + */ + protected function currentPasswordRules(): array + { + return ['required', 'string', 'current_password']; + } +} diff --git a/app/Concerns/ProfileValidationRules.php b/app/Concerns/ProfileValidationRules.php new file mode 100644 index 0000000..46e19ba --- /dev/null +++ b/app/Concerns/ProfileValidationRules.php @@ -0,0 +1,50 @@ +|string>> + */ + protected function profileRules(?int $userId = null): array + { + return [ + 'name' => $this->nameRules(), + 'email' => $this->emailRules($userId), + ]; + } + + /** + * Get the validation rules used to validate user names. + * + * @return array|string> + */ + protected function nameRules(): array + { + return ['required', 'string', 'max:255']; + } + + /** + * Get the validation rules used to validate user emails. + * + * @return array|string> + */ + protected function emailRules(?int $userId = null): array + { + return [ + 'required', + 'string', + 'email', + 'max:255', + $userId === null + ? Rule::unique(User::class) + : Rule::unique(User::class)->ignore($userId), + ]; + } +} diff --git a/app/Console/Commands/ExampleRecipesSeederCommand.php b/app/Console/Commands/ExampleRecipesSeederCommand.php new file mode 100644 index 0000000..9146276 --- /dev/null +++ b/app/Console/Commands/ExampleRecipesSeederCommand.php @@ -0,0 +1,20 @@ +argument('user_id'); + $seeder->run($user_id); + } +} diff --git a/app/Console/Commands/FetchDeviceModelsCommand.php b/app/Console/Commands/FetchDeviceModelsCommand.php new file mode 100644 index 0000000..78dd02a --- /dev/null +++ b/app/Console/Commands/FetchDeviceModelsCommand.php @@ -0,0 +1,46 @@ +info('Dispatching FetchDeviceModelsJob...'); + + try { + FetchDeviceModelsJob::dispatchSync(); + + $this->info('FetchDeviceModelsJob has been dispatched successfully.'); + + return self::SUCCESS; + } catch (Exception $e) { + $this->error('Failed to dispatch FetchDeviceModelsJob: '.$e->getMessage()); + + return self::FAILURE; + } + } +} diff --git a/app/Console/Commands/FirmwareCheckCommand.php b/app/Console/Commands/FirmwareCheckCommand.php new file mode 100644 index 0000000..91922ba --- /dev/null +++ b/app/Console/Commands/FirmwareCheckCommand.php @@ -0,0 +1,38 @@ + FirmwarePollJob::dispatchSync(download: $this->option('download')), + message: 'Checking for latest firmware...' + ); + + $latestFirmware = Firmware::getLatest(); + if ($latestFirmware instanceof Firmware) { + table( + rows: [ + ['Latest Version', $latestFirmware->version_tag], + ['Download URL', $latestFirmware->url], + ['Storage Location', $latestFirmware->storage_location], + ] + ); + } else { + $this->error('No firmware found.'); + } + } +} diff --git a/app/Console/Commands/FirmwareUpdateCommand.php b/app/Console/Commands/FirmwareUpdateCommand.php new file mode 100644 index 0000000..bd43786 --- /dev/null +++ b/app/Console/Commands/FirmwareUpdateCommand.php @@ -0,0 +1,70 @@ + 'Check. Devices will download binary from the original source.', + 'download' => 'Check & Download. Devices will download binary from BYOS.', + 'no' => 'Do not check.', + ], + ); + + if ($checkFirmware !== 'no') { + $this->call('trmnl:firmware:check', [ + '--download' => $checkFirmware === 'download', + ]); + } + + $firmwareVersion = select( + label: 'Update to which version?', + options: Firmware::pluck('version_tag', 'id') + ); + + $devices = multiselect( + label: 'Which devices should be updated?', + options: [ + 'all' => 'ALL Devices', + ...Device::all()->mapWithKeys(fn ($device): array => + // without _ returns index + ["_$device->id" => "$device->name (Current version: $device->last_firmware_version)"])->toArray(), + ], + scroll: 10 + ); + + if ($devices === []) { + $this->error('No devices selected. Aborting.'); + + return; + } + + if (in_array('all', $devices)) { + $devices = Device::pluck('id')->toArray(); + } else { + $devices = array_map(fn ($selected): int => (int) str_replace('_', '', $selected), $devices); + } + + foreach ($devices as $deviceId) { + Device::find($deviceId)->update(['update_firmware_id' => $firmwareVersion]); + + $this->info("Device with id [$deviceId] will update firmware on next request."); + } + } +} diff --git a/app/Console/Commands/GenerateDefaultImagesCommand.php b/app/Console/Commands/GenerateDefaultImagesCommand.php new file mode 100644 index 0000000..42e22ba --- /dev/null +++ b/app/Console/Commands/GenerateDefaultImagesCommand.php @@ -0,0 +1,202 @@ +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->css_name ?? $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, + 'cssVariables' => $deviceModel->css_variables, + ])->render(); + } +} diff --git a/app/Console/Commands/MashupCreateCommand.php b/app/Console/Commands/MashupCreateCommand.php new file mode 100644 index 0000000..7201274 --- /dev/null +++ b/app/Console/Commands/MashupCreateCommand.php @@ -0,0 +1,175 @@ +selectDevice(); + if (! $device instanceof Device) { + return 1; + } + + // Select playlist + $playlist = $this->selectPlaylist($device); + if (! $playlist instanceof Playlist) { + return 1; + } + + // Select mashup layout + $layout = $this->selectLayout(); + if (! $layout) { + return 1; + } + + // Get mashup name + $name = $this->getMashupName(); + if (! $name) { + return 1; + } + + // Select plugins + $plugins = $this->selectPlugins($layout); + if ($plugins->isEmpty()) { + return 1; + } + + $maxOrder = $playlist->items()->max('order') ?? 0; + + // Create playlist item with mashup + PlaylistItem::createMashup( + playlist: $playlist, + layout: $layout, + pluginIds: $plugins->pluck('id')->toArray(), + name: $name, + order: $maxOrder + 1 + ); + + $this->info('Mashup created successfully!'); + + return 0; + } + + protected function selectDevice(): ?Device + { + $devices = Device::all(); + if ($devices->isEmpty()) { + $this->error('No devices found. Please create a device first.'); + + return null; + } + + $deviceId = $this->choice( + 'Select a device', + $devices->mapWithKeys(fn ($device): array => [$device->id => $device->name])->toArray() + ); + + return $devices->firstWhere('id', $deviceId); + } + + protected function selectPlaylist(Device $device): ?Playlist + { + /** @var Collection|Playlist[] $playlists */ + $playlists = $device->playlists; + if ($playlists->isEmpty()) { + $this->error('No playlists found for this device. Please create a playlist first.'); + + return null; + } + + $playlistId = $this->choice( + 'Select a playlist', + $playlists->mapWithKeys(fn (Playlist $playlist): array => [$playlist->id => $playlist->name])->toArray() + ); + + return $playlists->firstWhere('id', $playlistId); + } + + protected function selectLayout(): ?string + { + return $this->choice( + 'Select a layout', + PlaylistItem::getAvailableLayouts() + ); + } + + protected function getMashupName(): ?string + { + $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 + { + $requiredCount = PlaylistItem::getRequiredPluginCountForLayout($layout); + + $plugins = Plugin::all(); + if ($plugins->isEmpty()) { + $this->error('No plugins found. Please create some plugins first.'); + + return collect(); + } + + $selectedPlugins = collect(); + $availablePlugins = $plugins->mapWithKeys(fn ($plugin): array => [$plugin->id => $plugin->name])->toArray(); + + for ($i = 0; $i < $requiredCount; ++$i) { + $position = match ($i) { + 0 => 'first', + 1 => 'second', + 2 => 'third', + 3 => 'fourth', + default => ($i + 1).'th' + }; + + $pluginId = $this->choice( + "Select the $position plugin", + $availablePlugins + ); + + $selectedPlugins->push($plugins->firstWhere('id', $pluginId)); + unset($availablePlugins[$pluginId]); + } + + return $selectedPlugins; + } +} diff --git a/app/Console/Commands/OidcTestCommand.php b/app/Console/Commands/OidcTestCommand.php new file mode 100644 index 0000000..81dff0b --- /dev/null +++ b/app/Console/Commands/OidcTestCommand.php @@ -0,0 +1,104 @@ +info('Testing OIDC Configuration...'); + $this->newLine(); + + // Check if OIDC is enabled + $enabled = config('services.oidc.enabled'); + $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: βœ… '.implode(', ', $effectiveScopes)); + + $this->newLine(); + + // Test driver registration + try { + // Only test driver if we have basic configuration + if ($endpoint && $clientId && $clientSecret) { + $driver = Socialite::driver('oidc'); + $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'); + } else { + $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)'); + } + if (! $clientId) { + $this->line(' - OIDC_CLIENT_ID=your-client-id'); + } + if (! $clientSecret) { + $this->line(' - OIDC_CLIENT_SECRET=your-client-secret'); + } + } + } catch (InvalidArgumentException $e) { + if (str_contains($e->getMessage(), 'Driver [oidc] not supported')) { + $this->error('❌ OIDC Driver registration failed: Driver not supported'); + } else { + $this->error('❌ OIDC Driver 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 baafacb..c0a2cc3 100644 --- a/app/Console/Commands/ScreenGeneratorCommand.php +++ b/app/Console/Commands/ScreenGeneratorCommand.php @@ -4,6 +4,7 @@ namespace App\Console\Commands; use App\Jobs\GenerateScreenJob; use Illuminate\Console\Command; +use Throwable; class ScreenGeneratorCommand extends Command { @@ -24,20 +25,19 @@ class ScreenGeneratorCommand extends Command /** * Execute the console command. */ - public function handle() + public function handle(): int { $deviceId = $this->argument('deviceId'); $view = $this->argument('view'); try { $markup = view($view)->render(); - } catch (\Throwable $e) { + } catch (Throwable $e) { $this->error('Failed to render view: '.$e->getMessage()); return 1; } - - GenerateScreenJob::dispatchSync($deviceId, $markup); + GenerateScreenJob::dispatchSync($deviceId, null, $markup); $this->info('Screen generation job finished.'); diff --git a/app/Enums/ImageFormat.php b/app/Enums/ImageFormat.php new file mode 100644 index 0000000..67e9b79 --- /dev/null +++ b/app/Enums/ImageFormat.php @@ -0,0 +1,23 @@ + 'Auto', + self::PNG_8BIT_GRAYSCALE => 'PNG 8-bit Grayscale Gray 2c', + self::BMP3_1BIT_SRGB => 'BMP3 1-bit sRGB 2c', + self::PNG_8BIT_256C => 'PNG 8-bit Grayscale Gray 256c', + self::PNG_2BIT_4C => 'PNG 2-bit Grayscale 4c', + }; + } +} diff --git a/app/Facades/QrCode.php b/app/Facades/QrCode.php new file mode 100644 index 0000000..00de934 --- /dev/null +++ b/app/Facades/QrCode.php @@ -0,0 +1,24 @@ +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}")) { + 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()); + + return redirect()->route('login')->withErrors(['oidc' => 'Failed to initiate OIDC authentication.']); + } + } + + /** + * Obtain the user information from the OIDC provider. + */ + public function callback(Request $request) + { + 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}")) { + 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()); + + return redirect()->route('login')->withErrors(['oidc' => 'Failed to authenticate with OIDC provider.']); + } + } + + /** + * Find or create a user based on OIDC information. + */ + protected function findOrCreateUser($oidcUser) + { + // 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; + } + } + + // Create new user + return User::create([ + 'oidc_sub' => $oidcUser->getId(), + 'name' => $oidcUser->getName() ?: 'OIDC User', + '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 + ]); + } +} diff --git a/app/Jobs/CheckVersionUpdateJob.php b/app/Jobs/CheckVersionUpdateJob.php new file mode 100644 index 0000000..c2413c0 --- /dev/null +++ b/app/Jobs/CheckVersionUpdateJob.php @@ -0,0 +1,216 @@ +errorResponse(); + } + + $backoffUntil = Cache::get(self::BACKOFF_KEY); + if ($this->isInBackoffPeriod($backoffUntil)) { + return $this->rateLimitResponse($backoffUntil); + } + + $cachedResponse = Cache::get(self::CACHE_KEY); + $response = $this->fetchOrUseCache($cachedResponse, $updateSettings->prereleases, $backoffUntil); + + if (! $response) { + return $this->errorResponse('fetch_failed'); + } + + [$latestVersion, $releaseData] = $this->extractLatestVersion($response, $updateSettings->prereleases); + $isNewer = $latestVersion && version_compare($latestVersion, $currentVersion, '>'); + + return [ + 'latest_version' => $latestVersion, + 'is_newer' => $isNewer, + 'release_data' => $releaseData, + 'error' => null, + ]; + } catch (ConnectionException $e) { + Log::error('Version check failed: '.$e->getMessage()); + + return $this->errorResponse('connection_failed'); + } catch (Exception $e) { + Log::error('Unexpected error in version check: '.$e->getMessage()); + + return $this->errorResponse('unexpected_error'); + } + } + + private function isInBackoffPeriod(?\Illuminate\Support\Carbon $backoffUntil): bool + { + return $backoffUntil !== null && now()->isBefore($backoffUntil); + } + + private function rateLimitResponse(\Illuminate\Support\Carbon $backoffUntil): array + { + return [ + 'latest_version' => null, + 'is_newer' => false, + 'release_data' => null, + 'error' => 'rate_limit', + 'backoff_until' => $backoffUntil->timestamp, + ]; + } + + private function errorResponse(?string $error = null): array + { + return [ + 'latest_version' => null, + 'is_newer' => false, + 'release_data' => null, + 'error' => $error, + ]; + } + + private function fetchOrUseCache(?array $cachedResponse, bool $enablePrereleases, ?\Illuminate\Support\Carbon $backoffUntil): ?array + { + if ($cachedResponse && ! $this->forceRefresh) { + return $cachedResponse; + } + + if ($this->isInBackoffPeriod($backoffUntil)) { + return $cachedResponse; + } + + try { + $httpResponse = $this->fetchReleases($enablePrereleases); + + if ($httpResponse->status() === 429) { + return $this->handleRateLimit($cachedResponse); + } + + if ($httpResponse->successful()) { + $responseData = $httpResponse->json(); + Cache::put(self::CACHE_KEY, $responseData, self::CACHE_TTL); + + return $responseData; + } + + Log::warning('GitHub API request failed', [ + 'status' => $httpResponse->status(), + 'body' => $httpResponse->body(), + ]); + + return $cachedResponse; + } catch (ConnectionException $e) { + Log::debug('Failed to fetch releases: '.$e->getMessage()); + + return $cachedResponse ?? null; + } catch (Exception $e) { + Log::debug('Failed to fetch releases: '.$e->getMessage()); + + return $cachedResponse ?? null; + } + } + + private function fetchReleases(bool $enablePrereleases) + { + $githubRepo = config('app.github_repo'); + $apiBaseUrl = "https://api.github.com/repos/{$githubRepo}"; + $endpoint = $enablePrereleases ? "{$apiBaseUrl}/releases" : "{$apiBaseUrl}/releases/latest"; + + return Http::timeout(10)->connectTimeout(5)->get($endpoint); + } + + private function handleRateLimit(?array $cachedResponse): ?array + { + $backoffUntil = now()->addMinutes(self::BACKOFF_MINUTES); + Cache::put(self::BACKOFF_KEY, $backoffUntil, 600); + Log::warning('GitHub API rate limit exceeded. Backing off for 10 minutes.'); + + return $cachedResponse; + } + + private function extractLatestVersion(array $response, bool $enablePrereleases): array + { + if (! $enablePrereleases || ! isset($response[0])) { + return [ + Arr::get($response, 'tag_name'), + $response, + ]; + } + + [$stableRelease, $prerelease] = $this->findReleases($response); + + if ($prerelease && $stableRelease) { + $prereleaseVersion = Arr::get($prerelease, 'tag_name'); + $stableVersion = Arr::get($stableRelease, 'tag_name'); + + if (version_compare($prereleaseVersion, $stableVersion, '>')) { + return [$prereleaseVersion, $prerelease]; + } + + return [$stableVersion, $stableRelease]; + } + + if ($prerelease) { + return [Arr::get($prerelease, 'tag_name'), $prerelease]; + } + + if ($stableRelease) { + return [Arr::get($stableRelease, 'tag_name'), $stableRelease]; + } + + return [null, null]; + } + + private function findReleases(array $allReleases): array + { + $stableRelease = null; + $prerelease = null; + + foreach ($allReleases as $release) { + $tagName = Arr::get($release, 'tag_name'); + if (! $tagName) { + continue; + } + + $isPrerelease = (bool) Arr::get($release, 'prerelease', false); + + if ($isPrerelease && ! $prerelease) { + $prerelease = $release; + } elseif (! $isPrerelease && ! $stableRelease) { + $stableRelease = $release; + } + + if ($stableRelease && $prerelease) { + break; + } + } + + return [$stableRelease, $prerelease]; + } +} diff --git a/app/Jobs/CleanupDeviceLogsJob.php b/app/Jobs/CleanupDeviceLogsJob.php new file mode 100644 index 0000000..d2f1dd9 --- /dev/null +++ b/app/Jobs/CleanupDeviceLogsJob.php @@ -0,0 +1,30 @@ +logs()->latest('device_timestamp')->take(50)->pluck('id'); + + // Delete all other logs for this device + $device->logs() + ->whereNotIn('id', $keepIds) + ->delete(); + }); + } +} diff --git a/app/Jobs/FetchDeviceModelsJob.php b/app/Jobs/FetchDeviceModelsJob.php new file mode 100644 index 0000000..2cd39d7 --- /dev/null +++ b/app/Jobs/FetchDeviceModelsJob.php @@ -0,0 +1,278 @@ +processPalettes(); + + $response = Http::timeout(30)->get(config('services.trmnl.base_url').self::API_URL); + + if (! $response->successful()) { + Log::error('Failed to fetch device models from API', [ + 'status' => $response->status(), + 'body' => $response->body(), + ]); + + return; + } + + $data = $response->json('data', []); + + if (! is_array($data)) { + Log::error('Invalid response format from device models API', [ + 'response' => $response->json(), + ]); + + return; + } + + $this->processDeviceModels($data); + + Log::info('Successfully fetched and updated device models', [ + 'count' => count($data), + ]); + + } catch (Exception $e) { + Log::error('Exception occurred while fetching device models', [ + 'message' => $e->getMessage(), + 'trace' => $e->getTraceAsString(), + ]); + } + } + + /** + * 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. + */ + private function processDeviceModels(array $deviceModels): void + { + foreach ($deviceModels as $modelData) { + try { + $this->updateOrCreateDeviceModel($modelData); + } catch (Exception $e) { + Log::error('Failed to process device model', [ + 'model_data' => $modelData, + 'error' => $e->getMessage(), + ]); + } + } + } + + /** + * Update or create a device model record. + */ + private function updateOrCreateDeviceModel(array $modelData): void + { + $name = $modelData['name'] ?? null; + + if (! $name) { + Log::warning('Device model data missing name field', [ + 'model_data' => $modelData, + ]); + + return; + } + + $attributes = [ + 'label' => $modelData['label'] ?? '', + 'description' => $modelData['description'] ?? '', + 'width' => $modelData['width'] ?? 0, + 'height' => $modelData['height'] ?? 0, + 'colors' => $modelData['colors'] ?? 0, + 'bit_depth' => $modelData['bit_depth'] ?? 0, + 'scale_factor' => $modelData['scale_factor'] ?? 1, + 'rotation' => $modelData['rotation'] ?? 0, + 'mime_type' => $modelData['mime_type'] ?? '', + '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; + } + + $attributes['css_name'] = $this->parseCssNameFromApi($modelData['css'] ?? null); + $attributes['css_variables'] = $this->parseCssVariablesFromApi($modelData['css'] ?? null); + + DeviceModel::updateOrCreate( + ['name' => $name], + $attributes + ); + } + + /** + * Extract css_name from API css payload (strip "screen--" prefix from classes.device). + */ + private function parseCssNameFromApi(mixed $css): ?string + { + $deviceClass = is_array($css) ? Arr::get($css, 'classes.device') : null; + + return (is_string($deviceClass) ? Str::after($deviceClass, 'screen--') : null) ?: null; + } + + /** + * Extract css_variables from API css payload (convert [[key, value], ...] to associative array). + */ + private function parseCssVariablesFromApi(mixed $css): ?array + { + $pairs = is_array($css) ? Arr::get($css, 'variables', []) : []; + if (! is_array($pairs)) { + return null; + } + + $validPairs = Arr::where($pairs, fn (mixed $pair): bool => is_array($pair) && isset($pair[0], $pair[1])); + $variables = Arr::pluck($validPairs, 1, 0); + + return $variables !== [] ? $variables : null; + } + + /** + * 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 31fb5bf..3c8af13 100644 --- a/app/Jobs/FetchProxyCloudResponses.php +++ b/app/Jobs/FetchProxyCloudResponses.php @@ -3,93 +3,142 @@ namespace App\Jobs; use App\Models\Device; +use Exception; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; +use Illuminate\Http\Client\Response; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\SerializesModels; use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Storage; +use Illuminate\Support\Str; class FetchProxyCloudResponses implements ShouldQueue { use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; - /** - * Execute the job. - */ public function handle(): void { - Device::where('proxy_cloud', true)->each(function ($device) { - try { - $response = Http::withHeaders([ - 'id' => $device->mac_address, - 'access-token' => $device->api_key, - 'width' => 800, - 'height' => 480, - 'rssi' => $device->last_rssi_level, - 'battery_voltage' => $device->last_battery_voltage, - 'refresh-rate' => $device->default_refresh_interval, - 'fw-version' => $device->last_firmware_version, - 'accept-encoding' => 'identity;q=1,chunked;q=0.1,*;q=0', - 'user-agent' => 'ESP32HTTPClient', - ])->get(config('services.trmnl.proxy_base_url').'/api/display'); + Device::where('proxy_cloud', true)->each(function ($device): void { + if ($device->getNextPlaylistItem()) { + Log::info("Skipping device: {$device->mac_address} as it has a pending playlist item."); + return; + } + + try { + $response = $this->fetchDisplayResponse($device); $device->update([ 'proxy_cloud_response' => $response->json(), ]); - $imageUrl = $response->json('image_url'); - $filename = $response->json('filename'); - - \Log::info('Response data: '.$imageUrl); - if (isset($imageUrl)) { - try { - $imageContents = Http::get($imageUrl)->body(); - if (! Storage::disk('public')->exists("images/generated/{$filename}.bmp")) { - Storage::disk('public')->put( - "images/generated/{$filename}.bmp", - $imageContents - ); - } - - $device->update([ - 'current_screen_image' => $filename, - ]); - } catch (\Exception $e) { - Log::error("Failed to download and save image for device: {$device->mac_address}", [ - 'error' => $e->getMessage(), - ]); - } - } + $this->processImage($device, $response); + $this->uploadLogRequest($device); Log::info("Successfully updated proxy cloud response for device: {$device->mac_address}"); - - if ($device->last_log_request) { - Http::withHeaders([ - 'id' => $device->mac_address, - 'access-token' => $device->api_key, - 'width' => 800, - 'height' => 480, - 'rssi' => $device->last_rssi_level, - 'battery_voltage' => $device->last_battery_voltage, - 'refresh-rate' => $device->default_refresh_interval, - 'fw-version' => $device->last_firmware_version, - 'accept-encoding' => 'identity;q=1,chunked;q=0.1,*;q=0', - 'user-agent' => 'ESP32HTTPClient', - ])->post(config('services.trmnl.proxy_base_url').'/api/log', $device->last_log_request); - - $device->update([ - 'last_log_request' => null, - ]); - } - - } catch (\Exception $e) { + } catch (Exception $e) { Log::error("Failed to fetch proxy cloud response for device: {$device->mac_address}", [ 'error' => $e->getMessage(), ]); } }); } + + private function fetchDisplayResponse(Device $device): Response + { + /** @var Response $response */ + $response = Http::withHeaders($this->getDeviceHeaders($device)) + ->get(config('services.trmnl.proxy_base_url').'/api/display'); + + return $response; + } + + private function getDeviceHeaders(Device $device): array + { + return [ + 'id' => $device->mac_address, + 'access-token' => $device->api_key, + 'width' => 800, + 'height' => 480, + 'rssi' => $device->last_rssi_level, + 'battery_voltage' => $device->last_battery_voltage, + 'refresh-rate' => $device->default_refresh_interval, + 'fw-version' => $device->last_firmware_version, + 'accept-encoding' => 'identity;q=1,chunked;q=0.1,*;q=0', + 'user-agent' => 'ESP32HTTPClient', + ]; + } + + private function processImage(Device $device, Response $response): void + { + $imageUrl = $response->json('image_url'); + $filename = $response->json('filename'); + + if ($imageUrl === null) { + return; + } + + $imageExtension = $this->determineImageExtension($imageUrl); + Log::info("Response data: $imageUrl. Image Extension: $imageExtension"); + + try { + $imageContents = Http::get($imageUrl)->body(); + $filePath = "images/generated/{$filename}.{$imageExtension}"; + + if (! Storage::disk('public')->exists($filePath)) { + Storage::disk('public')->put($filePath, $imageContents); + } + + $device->update([ + 'current_screen_image' => $filename, + ]); + } catch (Exception $e) { + Log::error("Failed to download and save image for device: {$device->mac_address}", [ + 'error' => $e->getMessage(), + ]); + } + } + + private function determineImageExtension(?string $imageUrl): string + { + if ($imageUrl === null) { + return 'bmp'; + } + + if (Str::contains($imageUrl, '.png')) { + return 'png'; + } + + $parsedUrl = parse_url($imageUrl); + if ($parsedUrl === false || ! isset($parsedUrl['query'])) { + return 'bmp'; + } + + parse_str($parsedUrl['query'], $queryParams); + $imageType = urldecode($queryParams['response-content-type'] ?? 'image/bmp'); + + return $imageType === 'image/png' ? 'png' : 'bmp'; + } + + private function uploadLogRequest(Device $device): void + { + if (! $device->last_log_request) { + return; + } + + try { + Http::withHeaders($this->getDeviceHeaders($device)) + ->post(config('services.trmnl.proxy_base_url').'/api/log', $device->last_log_request); + + $device->update([ + 'last_log_request' => null, + ]); + } catch (Exception $e) { + Log::error("Failed to upload device log for device: {$device->mac_address}", [ + 'error' => $e->getMessage(), + ]); + } + } } diff --git a/app/Jobs/FirmwareDownloadJob.php b/app/Jobs/FirmwareDownloadJob.php new file mode 100644 index 0000000..dfc851d --- /dev/null +++ b/app/Jobs/FirmwareDownloadJob.php @@ -0,0 +1,52 @@ +exists('firmwares')) { + Storage::disk('public')->makeDirectory('firmwares'); + } + + try { + $filename = "FW{$this->firmware->version_tag}.bin"; + $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 new file mode 100644 index 0000000..7fb45f9 --- /dev/null +++ b/app/Jobs/FirmwarePollJob.php @@ -0,0 +1,55 @@ +json(); + + if (! is_array($response) || ! isset($response['version']) || ! isset($response['url'])) { + Log::error('Invalid firmware response format received'); + + return; + } + + $latestFirmware = Firmware::updateOrCreate( + ['version_tag' => $response['version']], + [ + 'url' => $response['url'], + 'latest' => true, + ] + ); + + Firmware::where('id', '!=', $latestFirmware->id)->update(['latest' => false]); + + if ($this->download && $latestFirmware->url && $latestFirmware->storage_location === null) { + FirmwareDownloadJob::dispatchSync($latestFirmware); + } + + } catch (ConnectionException $e) { + Log::error('Firmware download failed: '.$e->getMessage()); + } catch (Exception $e) { + Log::error('Unexpected error in firmware polling: '.$e->getMessage()); + } + } +} diff --git a/app/Jobs/GenerateScreenJob.php b/app/Jobs/GenerateScreenJob.php index e1d0d25..4af43dd 100644 --- a/app/Jobs/GenerateScreenJob.php +++ b/app/Jobs/GenerateScreenJob.php @@ -3,14 +3,13 @@ namespace App\Jobs; use App\Models\Device; +use App\Models\Plugin; +use App\Services\ImageGenerationService; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\SerializesModels; -use Illuminate\Support\Facades\Storage; -use Ramsey\Uuid\Uuid; -use Spatie\Browsershot\Browsershot; class GenerateScreenJob implements ShouldQueue { @@ -21,6 +20,7 @@ class GenerateScreenJob implements ShouldQueue */ public function __construct( private readonly int $deviceId, + private readonly ?int $pluginId, private readonly string $markup ) {} @@ -29,61 +29,20 @@ class GenerateScreenJob implements ShouldQueue */ public function handle(): void { - $uuid = Uuid::uuid4()->toString(); - $pngPath = Storage::disk('public')->path('/images/generated/'.$uuid.'.png'); - $bmpPath = Storage::disk('public')->path('/images/generated/'.$uuid.'.bmp'); + $newImageUuid = ImageGenerationService::generateImage($this->markup, $this->deviceId); - // Generate PNG - try { - Browsershot::html($this->markup) - ->setOption('args', config('app.puppeteer_docker') ? ['--no-sandbox', '--disable-setuid-sandbox', '--disable-gpu'] : []) - ->windowSize(800, 480) - ->save($pngPath); - } catch (\Exception $e) { - throw new \RuntimeException('Failed to generate PNG: '.$e->getMessage(), 0, $e); - } + Device::find($this->deviceId)->update(['current_screen_image' => $newImageUuid]); - try { - $this->convertToBmpImageMagick($pngPath, $bmpPath); - } catch (\ImagickException $e) { - throw new \RuntimeException('Failed to convert image to BMP: '.$e->getMessage(), 0, $e); - } - Device::find($this->deviceId)->update(['current_screen_image' => $uuid]); - \Log::info("Device $this->deviceId: updated with new image: $uuid"); - - $this->cleanupFolder(); - } - - /** - * @throws \ImagickException - */ - private 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(); - } - - private function cleanupFolder(): void - { - $activeImageUuids = Device::pluck('current_screen_image')->filter()->toArray(); - - $files = Storage::disk('public')->files('/images/generated/'); - foreach ($files as $file) { - if (basename($file) === '.gitignore') { - continue; - } - // Get filename without path and extension - $fileUuid = pathinfo($file, PATHINFO_FILENAME); - // If the UUID is not in use by any device, move it to archive - if (! in_array($fileUuid, $activeImageUuids)) { - Storage::disk('public')->delete($file); + if ($this->pluginId) { + $plugin = Plugin::find($this->pluginId); + $update = ['current_image' => $newImageUuid]; + if ($plugin->plugin_type === 'recipe') { + $device = Device::with(['deviceModel', 'deviceModel.palette'])->find($this->deviceId); + $update['current_image_metadata'] = ImageGenerationService::buildImageMetadataFromDevice($device); } + $plugin->update($update); } + + ImageGenerationService::cleanupFolder(); } } diff --git a/app/Jobs/NotifyDeviceBatteryLowJob.php b/app/Jobs/NotifyDeviceBatteryLowJob.php new file mode 100644 index 0000000..9b1001b --- /dev/null +++ b/app/Jobs/NotifyDeviceBatteryLowJob.php @@ -0,0 +1,54 @@ +battery_percent; + + // If battery is above threshold, reset the notification flag + if ($batteryPercent > $batteryThreshold && $device->battery_notification_sent) { + $device->battery_notification_sent = false; + $device->save(); + + continue; + } + // Skip if battery is not low or notification was already sent + if ($batteryPercent > $batteryThreshold) { + continue; + } + if ($device->battery_notification_sent) { + continue; + } + + /** @var User|null $user */ + $user = $device->user; + + if (! $user) { + continue; // Skip if no user is associated with the device + } + + // Send notification and mark as sent + $user->notify(new BatteryLow($device)); + $device->battery_notification_sent = true; + $device->save(); + } + } +} 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 new file mode 100644 index 0000000..2387ac5 --- /dev/null +++ b/app/Liquid/Filters/Data.php @@ -0,0 +1,136 @@ +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/Localization.php b/app/Liquid/Filters/Localization.php new file mode 100644 index 0000000..c91c75b --- /dev/null +++ b/app/Liquid/Filters/Localization.php @@ -0,0 +1,52 @@ +locale($locale); + } + + return $carbon->translatedFormat($format); + } + + /** + * Translate a common word to another language + * + * @param string $word The word to translate + * @param string $locale The locale to translate to + * @return string The translated word + */ + public function l_word(string $word, string $locale): string + { + $translation = trans('custom_plugins.'.mb_strtolower($word), locale: $locale); + + if ($translation === 'custom_plugins.'.mb_strtolower($word)) { + return $word; + } + + return $translation; + } +} diff --git a/app/Liquid/Filters/Numbers.php b/app/Liquid/Filters/Numbers.php new file mode 100644 index 0000000..0e31de1 --- /dev/null +++ b/app/Liquid/Filters/Numbers.php @@ -0,0 +1,50 @@ +convert($markdown); + } catch (CommonMarkException $e) { + Log::error('Markdown conversion error: '.$e->getMessage()); + } + + return null; + } + + /** + * Strip HTML tags from a string + * + * @param string $html The HTML string to strip + * @return string The string without HTML tags + */ + public function strip_html(string $html): string + { + return strip_tags($html); + } + + /** + * Generate a QR code as SVG from the input text + * + * @param string $text The text to encode in the QR code + * @param int|null $moduleSize Optional module size (defaults to 11, which equals 319px) + * @param string|null $errorCorrection Optional error correction level: 'l', 'm', 'q', 'h' (defaults to 'm') + * @return string The SVG QR code + */ + public function qr_code(string $text, ?int $moduleSize = null, ?string $errorCorrection = null): string + { + // Default module_size is 11 + // Size calculation: (21 modules for QR code + 4 modules margin on each side * 2) * module_size + // = (21 + 8) * module_size = 29 * module_size + $moduleSize ??= 11; + $size = 29 * $moduleSize; + + $qrCode = QrCode::format('svg') + ->size($size); + + // Set error correction level if provided + if ($errorCorrection !== null) { + $qrCode->errorCorrection($errorCorrection); + } + + return $qrCode->generate($text); + } +} diff --git a/app/Liquid/Filters/Uniqueness.php b/app/Liquid/Filters/Uniqueness.php new file mode 100644 index 0000000..35378b3 --- /dev/null +++ b/app/Liquid/Filters/Uniqueness.php @@ -0,0 +1,43 @@ +generateRandomString(); + } + + /** + * Generate a random string + * + * @param int $length The length of the random string + * @return string A random string + */ + private function generateRandomString(int $length = 4): string + { + $characters = 'abcdefghijklmnopqrstuvwxyz0123456789'; + $randomString = ''; + + for ($i = 0; $i < $length; ++$i) { + $randomString .= $characters[random_int(0, mb_strlen($characters) - 1)]; + } + + return $randomString; + } +} diff --git a/app/Liquid/Tags/PluginRenderTag.php b/app/Liquid/Tags/PluginRenderTag.php new file mode 100644 index 0000000..e4c441a --- /dev/null +++ b/app/Liquid/Tags/PluginRenderTag.php @@ -0,0 +1,45 @@ + + */ + private const PARENT_CONTEXT_KEYS = ['trmnl', 'size', 'data', 'config']; + + protected function buildPartialContext(RenderContext $rootContext, string $templateName, array $variables = []): RenderContext + { + $partialContext = $rootContext->newIsolatedSubContext($templateName); + + foreach (self::PARENT_CONTEXT_KEYS as $key) { + $value = $rootContext->get($key); + if ($value !== null && ! $value instanceof MissingValue) { + $partialContext->set($key, $value); + } + } + + foreach ($variables as $key => $value) { + $partialContext->set($key, $value); + } + + foreach ($this->attributes as $key => $value) { + $partialContext->set($key, $rootContext->evaluate($value)); + } + + return $partialContext; + } +} diff --git a/app/Liquid/Tags/TemplateTag.php b/app/Liquid/Tags/TemplateTag.php new file mode 100644 index 0000000..94e08c1 --- /dev/null +++ b/app/Liquid/Tags/TemplateTag.php @@ -0,0 +1,100 @@ +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..82d63ed 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->deviceAutojoin = (bool) (auth()->user()->assign_new_devices ?? false); $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 99f5338..a5b0fdf 100644 --- a/app/Models/Device.php +++ b/app/Models/Device.php @@ -2,23 +2,50 @@ namespace App\Models; +use Carbon\Carbon; +use DateTimeInterface; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; +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', 'last_log_request' => 'json', 'proxy_cloud_response' => 'json', + 'width' => 'integer', + 'height' => 'integer', + 'rotate' => 'integer', + 'last_refreshed_at' => 'datetime', + 'sleep_mode_enabled' => 'boolean', + 'sleep_mode_from' => 'datetime:H:i', + 'sleep_mode_to' => 'datetime:H:i', + 'special_function' => 'string', + 'pause_until' => 'datetime', + 'maximum_compatibility' => 'boolean', ]; - public function getBatteryPercentAttribute() + public function getBatteryPercentAttribute(): int|float { $volts = $this->last_battery_voltage; @@ -29,7 +56,8 @@ class Device extends Model // Ensure the voltage is within range if ($volts <= $min_volt) { return 0; - } elseif ($volts >= $max_volt) { + } + if ($volts >= $max_volt) { return 100; } @@ -39,36 +67,90 @@ class Device extends Model return round($percent); } - public function getWifiStrenghAttribute() + /** + * Calculate battery voltage from percentage + * + * @param int $percent Battery percentage (0-100) + * @return float Calculated voltage + */ + public function calculateVoltageFromPercent(int $percent): float + { + // Define min and max voltage for Li-ion battery (3.0V empty, 4.2V full) + $min_volt = 3.0; + $max_volt = 4.2; + + // Ensure the percentage is within range + if ($percent <= 0) { + return $min_volt; + } + if ($percent >= 100) { + return $max_volt; + } + + // Calculate voltage + $voltage = $min_volt + (($percent / 100) * ($max_volt - $min_volt)); + + return round($voltage, 2); + } + + public function getWifiStrengthAttribute(): int { $rssi = $this->last_rssi_level; if ($rssi >= 0) { return 0; // No signal (0 bars) - } elseif ($rssi <= -80) { - return 1; // Weak signal (1 bar) - } elseif ($rssi <= -60) { - return 2; // Moderate signal (2 bars) - } else { - return 3; // Strong signal (3 bars) } + if ($rssi <= -80) { + return 1; // Weak signal (1 bar) + } + if ($rssi <= -60) { + return 2; // Moderate signal (2 bars) + } + + return 3; // Strong signal (3 bars) + } - public function getUpdateFirmwareAttribute() : bool + public function getUpdateFirmwareAttribute(): bool { - if ($this->proxy_cloud_response && $this->proxy_cloud_response['update_firmware']) { + if ($this->update_firmware_id) { return true; } - return false; + + return $this->proxy_cloud_response && $this->proxy_cloud_response['update_firmware']; } - public function getFirmwareUrlAttribute() : string | null + public function getFirmwareUrlAttribute(): ?string { + if ($this->update_firmware_id) { + $firmware = Firmware::find($this->update_firmware_id); + if ($firmware) { + if ($firmware->storage_location) { + return Storage::disk('public')->url($firmware->storage_location); + } + + return $firmware->url; + } + } + if ($this->proxy_cloud_response && $this->proxy_cloud_response['firmware_url']) { return $this->proxy_cloud_response['firmware_url']; } + return null; } + public function resetUpdateFirmwareFlag(): void + { + if ($this->proxy_cloud_response) { + $this->proxy_cloud_response = array_merge($this->proxy_cloud_response, ['update_firmware' => false]); + $this->save(); + } + if ($this->update_firmware_id) { + $this->update_firmware_id = null; + $this->save(); + } + } + public function playlists(): HasMany { return $this->hasMany(Playlist::class); @@ -77,6 +159,7 @@ class Device extends Model public function getNextPlaylistItem(): ?PlaylistItem { // Get all active playlists + /** @var \Illuminate\Support\Collection|Playlist[] $playlists */ $playlists = $this->playlists() ->where('is_active', true) ->get(); @@ -93,4 +176,112 @@ class Device extends Model return null; } + + public function playlist(): BelongsTo + { + return $this->belongsTo(Playlist::class); + } + + public function mirrorDevice(): BelongsTo + { + return $this->belongsTo(self::class, 'mirror_device_id'); + } + + public function updateFirmware(): BelongsTo + { + 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); + } + + public function user(): BelongsTo + { + return $this->belongsTo(User::class); + } + + public function isSleepModeActive(?DateTimeInterface $now = null): bool + { + if (! $this->sleep_mode_enabled || ! $this->sleep_mode_from || ! $this->sleep_mode_to) { + return false; + } + + $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 + ? $now->between($this->sleep_mode_from, $this->sleep_mode_to) + : ($now->gte($this->sleep_mode_from) || $now->lte($this->sleep_mode_to)); + } + + public function getSleepModeEndsInSeconds(?DateTimeInterface $now = null): ?int + { + if (! $this->sleep_mode_enabled || ! $this->sleep_mode_from || ! $this->sleep_mode_to) { + return null; + } + + $now = $now instanceof DateTimeInterface ? Carbon::instance($now) : now(); + $from = $this->sleep_mode_from; + $to = $this->sleep_mode_to; + + // Handle overnight ranges (e.g. 22:00 to 06:00) + if ($from < $to) { + // Normal range, same day + return $now->between($from, $to) ? (int) $now->diffInSeconds($to, false) : 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 + { + return $this->pause_until && $this->pause_until->isFuture(); + } } diff --git a/app/Models/DeviceLog.php b/app/Models/DeviceLog.php new file mode 100644 index 0000000..6f266ce --- /dev/null +++ b/app/Models/DeviceLog.php @@ -0,0 +1,27 @@ +belongsTo(Device::class); + } + + protected function casts(): array + { + return [ + 'log_entry' => 'array', + 'device_timestamp' => 'datetime', + ]; + } +} diff --git a/app/Models/DeviceModel.php b/app/Models/DeviceModel.php new file mode 100644 index 0000000..bdc4b98 --- /dev/null +++ b/app/Models/DeviceModel.php @@ -0,0 +1,128 @@ + $css_variables + * @property-read string|null $css_name + * @property-read DevicePalette|null $palette + */ +final class DeviceModel extends Model +{ + use HasFactory; + + protected $guarded = ['id']; + + protected $casts = [ + 'width' => 'integer', + 'height' => 'integer', + 'colors' => 'integer', + 'bit_depth' => 'integer', + 'scale_factor' => 'float', + 'rotation' => 'integer', + 'offset_x' => 'integer', + 'offset_y' => 'integer', + 'published_at' => 'datetime', + 'css_variables' => 'array', + ]; + + 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; + } + + /** + * Returns css_name for v2 (per-device sizing); for v1 returns 'og' to preserve legacy single-variant behaviour. + * + * @return Attribute + */ + protected function cssName(): Attribute + { + /** @var Attribute */ + return Attribute::get( + fn (mixed $value): ?string => config('app.puppeteer_window_size_strategy') === 'v2' ? ($value !== null ? (string) $value : null) : 'og' + ); + } + + public function palette(): BelongsTo + { + return $this->belongsTo(DevicePalette::class, 'palette_id'); + } + + /** + * Returns css_variables with --screen-w and --screen-h filled from width/height + * when puppeteer_window_size_strategy is v2 and they are not set. + * + * @return Attribute, array> + */ + protected function cssVariables(): Attribute + { + /** @var Attribute, array> */ + return Attribute::get( + /** @return array */ + function (mixed $value, array $attributes): array { + $vars = is_array($value) ? $value : (is_string($value) ? (json_decode($value, true) ?? []) : []); + + if (config('app.puppeteer_window_size_strategy') !== 'v2') { + return $vars; + } + + $width = $attributes['width'] ?? null; + $height = $attributes['height'] ?? null; + + if (empty($vars['--screen-w']) && $width !== null && $width !== '') { + $vars['--screen-w'] = is_numeric($width) ? (int) $width.'px' : (string) $width; + } + if (empty($vars['--screen-h']) && $height !== null && $height !== '') { + $vars['--screen-h'] = is_numeric($height) ? (int) $height.'px' : (string) $height; + } + + /** @var array $vars */ + return $vars; + }); + } +} 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/Firmware.php b/app/Models/Firmware.php new file mode 100644 index 0000000..63db578 --- /dev/null +++ b/app/Models/Firmware.php @@ -0,0 +1,25 @@ + 'boolean', + ]; + } + + public static function getLatest(): ?self + { + return self::where('latest', true)->first(); + } +} diff --git a/app/Models/Playlist.php b/app/Models/Playlist.php index c3744e9..b4daf5e 100644 --- a/app/Models/Playlist.php +++ b/app/Models/Playlist.php @@ -18,6 +18,7 @@ class Playlist extends Model 'weekdays' => 'array', 'active_from' => 'datetime:H:i', 'active_until' => 'datetime:H:i', + 'refresh_time' => 'integer', ]; public function device(): BelongsTo @@ -36,17 +37,36 @@ 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; } - // Check time range + if ($this->active_from !== null && $this->active_until !== null) { - if (! now()->between($this->active_from, $this->active_until)) { - return false; + // 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 ($activeFrom > $activeUntil) { + // Time range spans midnight (e.g., 09:01 to 03:58) + if ($now >= $activeFrom || $now <= $activeUntil) { + return true; + } + } elseif ($now >= $activeFrom && $now <= $activeUntil) { + return true; } + + return false; } return true; @@ -59,6 +79,7 @@ class Playlist extends Model } // Get active playlist items ordered by display order + /** @var \Illuminate\Support\Collection|PlaylistItem[] $playlistItems */ $playlistItems = $this->items() ->where('is_active', true) ->orderBy('order') diff --git a/app/Models/PlaylistItem.php b/app/Models/PlaylistItem.php index 4eba877..744a012 100644 --- a/app/Models/PlaylistItem.php +++ b/app/Models/PlaylistItem.php @@ -2,6 +2,7 @@ namespace App\Models; +use Exception; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; @@ -15,6 +16,7 @@ class PlaylistItem extends Model protected $casts = [ 'is_active' => 'boolean', 'last_displayed_at' => 'datetime', + 'mashup' => 'json', ]; public function playlist(): BelongsTo @@ -26,4 +28,193 @@ class PlaylistItem extends Model { return $this->belongsTo(Plugin::class); } + + /** + * Check if this playlist item is a mashup + */ + public function isMashup(): bool + { + return ! is_null($this->mashup); + } + + /** + * Get the mashup name if this is a mashup + */ + public function getMashupName(): ?string + { + return $this->mashup['mashup_name'] ?? null; + } + + /** + * Get the mashup layout type if this is a mashup + */ + public function getMashupLayoutType(): ?string + { + return $this->mashup['mashup_layout'] ?? null; + } + + /** + * Get all plugin IDs for this mashup + */ + public function getMashupPluginIds(): array + { + return $this->mashup['plugin_ids'] ?? []; + } + + /** + * Get the number of plugins required for the current layout + */ + public function getRequiredPluginCount(): int + { + if (! $this->isMashup()) { + return 1; + } + + return match ($this->getMashupLayoutType()) { + '1Lx1R', '1Tx1B' => 2, // Left-Right or Top-Bottom split + '1Lx2R', '2Lx1R', '2Tx1B', '1Tx2B' => 3, // Two on one side, one on other + '2x2' => 4, // Quadrant + default => 1, + }; + } + + /** + * Get the layout type (horizontal, vertical, or grid) + */ + public function getLayoutType(): string + { + if (! $this->isMashup()) { + return 'single'; + } + + return match ($this->getMashupLayoutType()) { + '1Lx1R', '1Lx2R', '2Lx1R' => 'vertical', + '1Tx1B', '2Tx1B', '1Tx2B' => 'horizontal', + '2x2' => 'grid', + default => 'single', + }; + } + + /** + * Get the layout size for a plugin based on its position + */ + public function getLayoutSize(int $position = 0): string + { + if (! $this->isMashup()) { + return 'full'; + } + + return match ($this->getMashupLayoutType()) { + '1Lx1R' => 'half_vertical', // Both sides are single plugins + '1Tx1B' => 'half_horizontal', // Both sides are single plugins + '2Lx1R' => match ($position) { + 0, 1 => 'quadrant', // Left side has 2 plugins + 2 => 'half_vertical', // Right side has 1 plugin + default => 'full' + }, + '1Lx2R' => match ($position) { + 0 => 'half_vertical', // Left side has 1 plugin + 1, 2 => 'quadrant', // Right side has 2 plugins + default => 'full' + }, + '2Tx1B' => match ($position) { + 0, 1 => 'quadrant', // Top side has 2 plugins + 2 => 'half_horizontal', // Bottom side has 1 plugin + default => 'full' + }, + '1Tx2B' => match ($position) { + 0 => 'half_horizontal', // Top side has 1 plugin + 1, 2 => 'quadrant', // Bottom side has 2 plugins + default => 'full' + }, + '2x2' => 'quadrant', // All positions are quadrants + default => 'full' + }; + } + + /** + * Render all plugins with appropriate layout + */ + public function render(?Device $device = null): string + { + if (! $this->isMashup()) { + return view('trmnl-layouts.single', [ + 'colorDepth' => $device?->colorDepth(), + 'deviceVariant' => $device?->deviceModel?->css_name ?? $device?->deviceVariant() ?? 'og', + 'scaleLevel' => $device?->scaleLevel(), + 'cssVariables' => $device?->deviceModel?->css_variables, + 'slot' => $this->plugin instanceof Plugin + ? $this->plugin->render('full', false, $device) + : throw new Exception('Invalid plugin instance'), + ])->render(); + } + + $pluginMarkups = []; + $pluginIds = $this->getMashupPluginIds(); + $plugins = Plugin::whereIn('id', $pluginIds)->get(); + + // Sort the collection to match plugin_ids order + $plugins = $plugins->sortBy(fn ($plugin): int|string|false => array_search($plugin->id, $pluginIds))->values(); + + foreach ($plugins as $index => $plugin) { + $size = $this->getLayoutSize($index); + $pluginMarkups[] = $plugin->render($size, false, $device); + } + + return view('trmnl-layouts.mashup', [ + 'colorDepth' => $device?->colorDepth(), + 'deviceVariant' => $device?->deviceModel?->css_name ?? $device?->deviceVariant() ?? 'og', + 'scaleLevel' => $device?->scaleLevel(), + 'cssVariables' => $device?->deviceModel?->css_variables, + 'mashupLayout' => $this->getMashupLayoutType(), + 'slot' => implode('', $pluginMarkups), + ])->render(); + } + + /** + * Available mashup layouts with their descriptions + */ + public static function getAvailableLayouts(): array + { + return [ + '1Lx1R' => '1 Left - 1 Right (2 plugins)', + '1Lx2R' => '1 Left - 2 Right (3 plugins)', + '2Lx1R' => '2 Left - 1 Right (3 plugins)', + '1Tx1B' => '1 Top - 1 Bottom (2 plugins)', + '2Tx1B' => '2 Top - 1 Bottom (3 plugins)', + '1Tx2B' => '1 Top - 2 Bottom (3 plugins)', + '2x2' => 'Quadrant (4 plugins)', + ]; + } + + /** + * Get the required number of plugins for a given layout + */ + public static function getRequiredPluginCountForLayout(string $layout): int + { + return match ($layout) { + '1Lx1R', '1Tx1B' => 2, + '1Lx2R', '2Lx1R', '2Tx1B', '1Tx2B' => 3, + '2x2' => 4, + default => 1, + }; + } + + /** + * Create a new mashup with the given layout and plugins + */ + public static function createMashup(Playlist $playlist, string $layout, array $pluginIds, string $name, $order): self + { + return static::create([ + 'playlist_id' => $playlist->id, + 'plugin_id' => $pluginIds[0], // First plugin is the main plugin + 'mashup' => [ + 'mashup_layout' => $layout, + 'mashup_name' => $name, + 'plugin_ids' => $pluginIds, + ], + 'is_active' => true, + 'order' => $order, + ]); + } } diff --git a/app/Models/Plugin.php b/app/Models/Plugin.php index df16f86..7090ec3 100644 --- a/app/Models/Plugin.php +++ b/app/Models/Plugin.php @@ -2,10 +2,34 @@ 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\PluginRenderTag; +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; +use Symfony\Component\Yaml\Yaml; class Plugin extends Model { @@ -17,21 +41,177 @@ class Plugin extends Model 'data_payload' => 'json', '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', + 'current_image_metadata' => 'array', ]; 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 any markup changes + if ($model->isDirty([ + 'render_markup', + 'render_markup_half_horizontal', + 'render_markup_half_vertical', + 'render_markup_quadrant', + 'render_markup_shared', + ])) { + $model->current_image = null; + $model->current_image_metadata = null; + } + }); + + // Sanitize configuration template on save + static::saving(function ($model): void { + $model->sanitizeTemplate(); + }); + } + + public const CUSTOM_FIELDS_KEY = 'custom_fields'; + + public function user() + { + return $this->belongsTo(User::class); + } + + /** + * YAML for the custom_fields editor + */ + public function getCustomFieldsEditorYaml(): string + { + $template = $this->configuration_template; + $list = $template[self::CUSTOM_FIELDS_KEY] ?? null; + if ($list === null || $list === []) { + return ''; + } + + return Yaml::dump($list, 4, 2); + } + + /** + * Parse editor YAML and return configuration_template for DB (custom_fields key). Returns null when empty. + */ + public static function configurationTemplateFromCustomFieldsYaml(string $yaml, ?array $existingTemplate): ?array + { + $list = $yaml !== '' ? Yaml::parse($yaml) : []; + if ($list === null || (is_array($list) && $list === [])) { + return null; + } + + $template = $existingTemplate ?? []; + $template[self::CUSTOM_FIELDS_KEY] = is_array($list) ? $list : []; + + return $template; + } + + /** + * Validate that each custom field entry has field_type and name. For use with parsed editor YAML. + * + * @param array> $list + * + * @throws \Illuminate\Validation\ValidationException + */ + public static function validateCustomFieldsList(array $list): void + { + $validator = \Illuminate\Support\Facades\Validator::make( + ['custom_fields' => $list], + [ + 'custom_fields' => ['required', 'array'], + 'custom_fields.*.field_type' => ['required', 'string'], + 'custom_fields.*.name' => ['required', 'string'], + ], + [ + 'custom_fields.*.field_type.required' => 'Each custom field must have a field_type.', + 'custom_fields.*.name.required' => 'Each custom field must have a name.', + ] + ); + + $validator->validate(); + } + + // 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()); + } if (! $this->data_payload_updated_at || ! $this->data_stale_minutes) { return true; } @@ -41,15 +221,567 @@ class Plugin extends Model public function updateDataPayload(): void { - if ($this->data_strategy === 'polling' && $this->polling_url) { - $response = Http::withHeaders(['User-Agent' => 'usetrmnl/byos_laravel', 'Accept' => 'application/json']) - ->get($this->polling_url) - ->json(); - - $this->update([ - 'data_payload' => $response, - 'data_payload_updated_at' => now(), - ]); + if ($this->data_strategy !== 'polling' || ! $this->polling_url) { + return; } + $headers = ['User-Agent' => 'usetrmnl/larapaper', 'Accept' => 'application/json']; + + // resolve headers + if ($this->polling_header) { + $resolvedHeader = $this->resolveLiquidVariables($this->polling_header); + $headerLines = explode("\n", mb_trim($resolvedHeader)); + foreach ($headerLines as $line) { + $parts = explode(':', $line, 2); + if (count($parts) === 2) { + $headers[mb_trim($parts[0])] = mb_trim($parts[1]); + } + } + } + + // resolve and clean URLs + $resolvedPollingUrls = $this->resolveLiquidVariables($this->polling_url); + $urls = array_values(array_filter( // array_values ensures 0, 1, 2... + array_map(trim(...), explode("\n", $resolvedPollingUrls)), + filled(...) + )); + + $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) { + $contentType = (array_key_exists('Content-Type', $headers)) + ? $headers['Content-Type'] + : 'application/json'; + + $resolvedBody = $this->resolveLiquidVariables($this->polling_body); + $httpRequest = $httpRequest->withBody($resolvedBody, $contentType); + } + + try { + $httpResponse = ($this->polling_verb === 'post') + ? $httpRequest->post($url) + : $httpRequest->get($url); + + $response = $this->parseResponse($httpResponse); + + // Nest if it's a sequential array + if (array_keys($response) === range(0, count($response) - 1)) { + $combinedResponse["IDX_{$index}"] = ['data' => $response]; + } else { + $combinedResponse["IDX_{$index}"] = $response; + } + } catch (Exception $e) { + Log::warning("Failed to fetch data from URL {$url}: ".$e->getMessage()); + $combinedResponse["IDX_{$index}"] = ['error' => 'Failed to fetch data']; + } + } + + // unwrap IDX_0 if only one URL + $finalPayload = (count($urls) === 1) ? reset($combinedResponse) : $combinedResponse; + + $this->update([ + 'data_payload' => $finalPayload, + 'data_payload_updated_at' => now(), + ]); + } + + 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(); + } + + /** + * Render the plugin's markup + * + * @throws LiquidException + */ + 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.'); + } + + $markup = $this->getMarkupForSize($size); + + if ($markup) { + $renderedContent = ''; + + if ($this->markup_language === 'liquid') { + // Get timezone from user or fall back to app timezone + $timezone = $this->user->timezone ?? config('app.timezone'); + + // Calculate UTC offset in seconds + $utcOffset = (string) Carbon::now($timezone)->getOffset(); + + // 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, + ], + 'device' => [ + 'friendly_id' => $device?->friendly_id, + 'percent_charged' => $device?->battery_percent, + 'wifi_strength' => $device?->wifi_strength, + 'height' => $device?->height, + 'width' => $device?->width, + ], + '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($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); + // Use plugin render tag so partials receive trmnl, size, data, config + $environment->tagRegistry->register(PluginRenderTag::class); + + // Apply Liquid replacements (including 'with' syntax conversion) + $processedMarkup = $this->applyLiquidReplacements($markup); + + $template = $environment->parseString($processedMarkup); + $liquidContext = $environment->newRenderContext(data: $context); + $renderedContent = $template->render($liquidContext); + } + } else { + // Get timezone from user or fall back to app timezone + $timezone = $this->user->timezone ?? config('app.timezone'); + + // Calculate UTC offset in seconds + $utcOffset = (string) Carbon::now($timezone)->getOffset(); + + $renderedContent = Blade::render($markup, [ + 'size' => $size, + 'data' => $this->data_payload, + 'config' => $this->configuration ?? [], + 'trmnl' => [ + 'system' => [ + 'timestamp_utc' => now()->utc()->timestamp, + ], + 'user' => [ + 'utc_offset' => $utcOffset, + 'name' => $this->user->name ?? 'Unknown User', + 'locale' => 'en', + 'time_zone_iana' => $timezone, + ], + 'device' => [ + 'friendly_id' => $device?->friendly_id, + 'percent_charged' => $device?->battery_percent, + 'wifi_strength' => $device?->wifi_strength, + 'height' => $device?->height, + 'width' => $device?->width, + ], + '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 : []), + ], + ], + ], + ]); + } + + if ($standalone) { + if ($size === 'full') { + return view('trmnl-layouts.single', [ + 'colorDepth' => $device?->colorDepth(), + 'deviceVariant' => $device?->deviceModel?->css_name ?? $device?->deviceVariant() ?? 'og', + 'noBleed' => $this->no_bleed, + 'darkMode' => $this->dark_mode, + 'scaleLevel' => $device?->scaleLevel(), + 'cssVariables' => $device?->deviceModel?->css_variables, + 'slot' => $renderedContent, + ])->render(); + } + + return view('trmnl-layouts.mashup', [ + 'mashupLayout' => $this->getPreviewMashupLayoutForSize($size), + 'colorDepth' => $device?->colorDepth(), + 'deviceVariant' => $device?->deviceModel?->css_name ?? $device?->deviceVariant() ?? 'og', + 'darkMode' => $this->dark_mode, + 'scaleLevel' => $device?->scaleLevel(), + 'cssVariables' => $device?->deviceModel?->css_variables, + 'slot' => $renderedContent, + ])->render(); + + } + + return $renderedContent; + } + + if ($this->render_markup_view) { + if ($standalone) { + $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?->deviceModel?->css_name ?? $device?->deviceVariant() ?? 'og', + 'noBleed' => $this->no_bleed, + 'darkMode' => $this->dark_mode, + 'scaleLevel' => $device?->scaleLevel(), + 'cssVariables' => $device?->deviceModel?->css_variables, + 'slot' => $renderedView, + ])->render(); + } + + return view('trmnl-layouts.mashup', [ + 'mashupLayout' => $this->getPreviewMashupLayoutForSize($size), + 'colorDepth' => $device?->colorDepth(), + 'deviceVariant' => $device?->deviceModel?->css_name ?? $device?->deviceVariant() ?? 'og', + 'darkMode' => $this->dark_mode, + 'scaleLevel' => $device?->scaleLevel(), + 'cssVariables' => $device?->deviceModel?->css_variables, + '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; + } + + /** + * Get the appropriate markup for a given size, including shared prepending logic + * + * @param string $size The layout size (full, half_horizontal, half_vertical, quadrant) + * @return string|null The markup code for the given size, with shared prepended if available + */ + public function getMarkupForSize(string $size): ?string + { + $markup = match ($size) { + 'full' => $this->render_markup, + 'half_horizontal' => $this->render_markup_half_horizontal ?? $this->render_markup, + 'half_vertical' => $this->render_markup_half_vertical ?? $this->render_markup, + 'quadrant' => $this->render_markup_quadrant ?? $this->render_markup, + default => $this->render_markup, + }; + + // Prepend shared markup if it exists + if ($markup && $this->render_markup_shared) { + $markup = $this->render_markup_shared."\n".$markup; + } + + return $markup; + } + + 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'], $attributes['trmnlp_id']); + + // 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) { + // 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 949cafa..8d7aa7e 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -8,12 +8,13 @@ use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Notifications\Notifiable; use Illuminate\Support\Str; +use Laravel\Fortify\TwoFactorAuthenticatable; use Laravel\Sanctum\HasApiTokens; class User extends Authenticatable // implements MustVerifyEmail { /** @use HasFactory<\Database\Factories\UserFactory> */ - use HasApiTokens, HasFactory, Notifiable; + use HasApiTokens, HasFactory, Notifiable, TwoFactorAuthenticatable; /** * The attributes that are mass assignable. @@ -25,6 +26,9 @@ class User extends Authenticatable // implements MustVerifyEmail 'email', 'password', 'assign_new_devices', + 'assign_new_device_id', + 'oidc_sub', + 'timezone', ]; /** @@ -71,4 +75,9 @@ class User extends Authenticatable // implements MustVerifyEmail { return $this->hasMany(Plugin::class); } + + public function routeNotificationForWebhook(): ?string + { + return config('services.webhook.notifications.url'); + } } diff --git a/app/Notifications/BatteryLow.php b/app/Notifications/BatteryLow.php new file mode 100644 index 0000000..17fb1da --- /dev/null +++ b/app/Notifications/BatteryLow.php @@ -0,0 +1,66 @@ + + */ + public function via(object $notifiable): array + { + return ['mail', WebhookChannel::class]; + } + + /** + * Get the mail representation of the notification. + */ + public function toMail(object $notifiable): MailMessage + { + return (new MailMessage)->markdown('mail.battery-low', ['device' => $this->device]); + } + + public function toWebhook(object $notifiable): WebhookMessage + { + return WebhookMessage::create() + ->data([ + 'topic' => config('services.webhook.notifications.topic', 'battery.low'), + 'message' => "Battery below {$this->device->battery_percent}% on device: {$this->device->name}", + 'device_id' => $this->device->id, + 'device_name' => $this->device->name, + 'battery_percent' => $this->device->battery_percent, + + ]) + ->userAgent(config('app.name')) + ->header('X-TrmnlByos-Event', 'battery.low'); + } + + /** + * Get the array representation of the notification. + * + * @return array + */ + public function toArray(object $notifiable): array + { + return [ + 'device_name' => $this->device->name, + 'battery_percent' => $this->device->battery_percent, + ]; + } +} diff --git a/app/Notifications/Channels/WebhookChannel.php b/app/Notifications/Channels/WebhookChannel.php new file mode 100644 index 0000000..796cb24 --- /dev/null +++ b/app/Notifications/Channels/WebhookChannel.php @@ -0,0 +1,54 @@ +routeNotificationFor('webhook', $notification); + + if (! $url) { + return null; + } + + if (! method_exists($notification, 'toWebhook')) { + throw new Exception('Notification does not implement toWebhook method.'); + } + + $webhookData = $notification->toWebhook($notifiable)->toArray(); + $response = $this->client->post($url, [ + 'query' => Arr::get($webhookData, 'query'), + 'body' => json_encode(Arr::get($webhookData, 'data')), + 'verify' => Arr::get($webhookData, 'verify'), + 'headers' => Arr::get($webhookData, 'headers'), + ]); + + if (! $response instanceof Response) { + throw new Exception('Webhook request did not return a valid GuzzleHttp\Psr7\Response.'); + } + + if ($response->getStatusCode() >= 300 || $response->getStatusCode() < 200) { + throw new Exception('Webhook request failed with status code: '.$response->getStatusCode()); + } + + return $response; + } +} diff --git a/app/Notifications/Messages/WebhookMessage.php b/app/Notifications/Messages/WebhookMessage.php new file mode 100644 index 0000000..6dc58eb --- /dev/null +++ b/app/Notifications/Messages/WebhookMessage.php @@ -0,0 +1,122 @@ +query = $query; + + return $this; + } + + /** + * Set the Webhook data to be JSON encoded. + * + * @param mixed $data + * @return $this + */ + public function data($data): self + { + $this->data = $data; + + return $this; + } + + /** + * Add a Webhook request custom header. + * + * @param string $name + * @param string $value + * @return $this + */ + public function header($name, $value): self + { + $this->headers[$name] = $value; + + return $this; + } + + /** + * Set the Webhook request UserAgent. + * + * @param string $userAgent + * @return $this + */ + public function userAgent($userAgent): self + { + $this->headers['User-Agent'] = $userAgent; + + return $this; + } + + /** + * Indicate that the request should be verified. + * + * @return $this + */ + public function verify($value = true): self + { + $this->verify = $value; + + return $this; + } + + public function toArray(): array + { + return [ + 'query' => $this->query, + 'data' => $this->data, + 'headers' => $this->headers, + 'verify' => $this->verify, + ]; + } +} diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 6609fa8..48178e8 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -2,7 +2,12 @@ namespace App\Providers; +use App\Services\OidcProvider; +use App\Services\QrCodeService; +use Illuminate\Http\Request; +use Illuminate\Support\Facades\URL; use Illuminate\Support\ServiceProvider; +use Laravel\Socialite\Facades\Socialite; class AppServiceProvider extends ServiceProvider { @@ -11,7 +16,7 @@ class AppServiceProvider extends ServiceProvider */ public function register(): void { - // + $this->app->bind('qr-code', fn () => new QrCodeService); } /** @@ -20,7 +25,33 @@ class AppServiceProvider extends ServiceProvider public function boot(): void { if (app()->isProduction() && config('app.force_https')) { - \URL::forceScheme('https'); + 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 (\Illuminate\Contracts\Foundation\Application $app): OidcProvider { + $config = $app->make('config')->get('services.oidc', []); + + return new OidcProvider( + $app->make(Request::class), + $config['client_id'] ?? null, + $config['client_secret'] ?? null, + $config['redirect'] ?? null, + $config['scopes'] ?? ['openid', 'profile', 'email'] + ); + }); } } diff --git a/app/Providers/FortifyServiceProvider.php b/app/Providers/FortifyServiceProvider.php new file mode 100644 index 0000000..bf7a1b3 --- /dev/null +++ b/app/Providers/FortifyServiceProvider.php @@ -0,0 +1,72 @@ +configureActions(); + $this->configureViews(); + $this->configureRateLimiting(); + } + + /** + * Configure Fortify actions. + */ + private function configureActions(): void + { + Fortify::resetUserPasswordsUsing(ResetUserPassword::class); + Fortify::createUsersUsing(CreateNewUser::class); + } + + /** + * Configure Fortify views. + */ + private function configureViews(): void + { + Fortify::loginView(fn (): Factory|View => view('pages::auth.login')); + Fortify::verifyEmailView(fn (): Factory|View => view('pages::auth.verify-email')); + Fortify::twoFactorChallengeView(fn (): Factory|View => view('pages::auth.two-factor-challenge')); + Fortify::confirmPasswordView(fn (): Factory|View => view('pages::auth.confirm-password')); + Fortify::registerView(fn (): Factory|View => view('pages::auth.register')); + Fortify::resetPasswordView(fn (): Factory|View => view('pages::auth.reset-password')); + Fortify::requestPasswordResetLinkView(fn (): Factory|View => view('pages::auth.forgot-password')); + } + + /** + * Configure rate limiting. + */ + private function configureRateLimiting(): void + { + RateLimiter::for('two-factor', fn (Request $request) => Limit::perMinute(5)->by($request->session()->get('login.id'))); + + RateLimiter::for('login', function (Request $request) { + $throttleKey = Str::transliterate(Str::lower($request->input(Fortify::username())).'|'.$request->ip()); + + return Limit::perMinute(5)->by($throttleKey); + }); + } +} diff --git a/app/Providers/VoltServiceProvider.php b/app/Providers/VoltServiceProvider.php deleted file mode 100644 index e61d984..0000000 --- a/app/Providers/VoltServiceProvider.php +++ /dev/null @@ -1,28 +0,0 @@ -find($deviceId); + $uuid = self::generateImageFromModel( + markup: $markup, + deviceModel: $device->deviceModel, + user: $device->user, + palette: $device->palette ?? $device->deviceModel?->palette, + device: $device + ); + + $device->update(['current_screen_image' => $uuid]); + Log::info("Device $device->id: updated with new image: $uuid"); + + return $uuid; + } + + /** + * Generate an image from markup using a DeviceModel + * + * @param string $markup The HTML markup to render + * @param DeviceModel|null $deviceModel The device model to use for image generation + * @param \App\Models\User|null $user Optional user for timezone settings + * @param \App\Models\DevicePalette|null $palette Optional palette, falls back to device model's palette + * @param Device|null $device Optional device for legacy devices without DeviceModel + * @return string The UUID of the generated image + */ + public static function generateImageFromModel( + string $markup, + ?DeviceModel $deviceModel = null, + ?\App\Models\User $user = null, + ?\App\Models\DevicePalette $palette = null, + ?Device $device = null + ): string { + $uuid = Uuid::uuid4()->toString(); + + try { + // Get image generation settings from DeviceModel or Device (for legacy devices) + $imageSettings = $deviceModel instanceof DeviceModel + ? self::getImageSettingsFromModel($deviceModel) + : ($device instanceof 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 + */ + private static function getImageSettings(Device $device): array + { + // If device has a DeviceModel, use its settings + if ($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 instanceof DeviceModel) { + return [ + 'width' => $deviceModel->width, + 'height' => $deviceModel->height, + 'colors' => $deviceModel->colors, + 'bit_depth' => $deviceModel->bit_depth, + 'scale_factor' => $deviceModel->scale_factor, + 'rotation' => $deviceModel->rotation, + 'mime_type' => $deviceModel->mime_type, + 'offset_x' => $deviceModel->offset_x, + 'offset_y' => $deviceModel->offset_y, + 'image_format' => self::determineImageFormatFromModel($deviceModel), + 'use_model_settings' => true, + ]; + } + + // Default settings if no device model provided + return [ + 'width' => 800, + 'height' => 480, + 'colors' => 2, + 'bit_depth' => 1, + 'scale_factor' => 1.0, + 'rotation' => 0, + 'mime_type' => 'image/png', + 'offset_x' => 0, + 'offset_y' => 0, + 'image_format' => ImageFormat::AUTO->value, + 'use_model_settings' => false, + ]; + } + + /** + * Determine the appropriate ImageFormat based on DeviceModel settings + */ + private static function determineImageFormatFromModel(DeviceModel $model): string + { + // Map DeviceModel settings to ImageFormat + if ($model->mime_type === 'image/bmp' && $model->bit_depth === 1) { + return ImageFormat::BMP3_1BIT_SRGB->value; + } + if ($model->mime_type === 'image/png' && $model->bit_depth === 8 && $model->colors === 2) { + return ImageFormat::PNG_8BIT_GRAYSCALE->value; + } + if ($model->mime_type === 'image/png' && $model->bit_depth === 8 && $model->colors === 256) { + return ImageFormat::PNG_8BIT_256C->value; + } + if ($model->mime_type === 'image/png' && $model->bit_depth === 2 && $model->colors === 4) { + return ImageFormat::PNG_2BIT_4C->value; + } + + // Default to AUTO for unknown combinations + return ImageFormat::AUTO->value; + } + + /** + * Get MIME type from ImageFormat + */ + private static function getMimeTypeFromImageFormat(string $imageFormat): string + { + 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', + }; + } + + /** + * Get colors from ImageFormat + */ + private static function getColorsFromImageFormat(string $imageFormat): int + { + 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, + }; + } + + /** + * Get bit depth from ImageFormat + */ + private static function getBitDepthFromImageFormat(string $imageFormat): int + { + 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, + }; + } + + /** + * Detect whether the provided HTML markup contains an tag with class "image-dither". + */ + private static function markupContainsDitherImage(string $markup): bool + { + if (mb_trim($markup) === '') { + return 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; + } + + 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 + { + $activeDeviceImageUuids = Device::pluck('current_screen_image')->filter()->toArray(); + $activePluginImageUuids = Plugin::pluck('current_image')->filter()->toArray(); + $activeImageUuids = array_merge($activeDeviceImageUuids, $activePluginImageUuids); + + $files = Storage::disk('public')->files('/images/generated/'); + foreach ($files as $file) { + if (basename($file) === '.gitignore') { + continue; + } + // Get filename without path and extension + $fileUuid = pathinfo($file, PATHINFO_FILENAME); + // If the UUID is not in use by any device, move it to archive + if (! in_array($fileUuid, $activeImageUuids)) { + Storage::disk('public')->delete($file); + } + } + } + + /** + * Ensure plugin image cache is valid for the current context. No-op for image_webhook. + * When deviceOrModel is provided (recipe only), clears cache if stored metadata does not match. + */ + public static function resetIfNotCacheable(?Plugin $plugin, Device|DeviceModel|null $deviceOrModel = null): void + { + if (! $plugin?->id || $plugin->plugin_type === 'image_webhook') { + return; + } + if ($deviceOrModel === null || $plugin->plugin_type !== 'recipe') { + return; + } + if ($plugin->current_image === null) { + return; + } + if (self::imageMetadataMatches($plugin->current_image_metadata, $deviceOrModel)) { + return; + } + $plugin->update([ + 'current_image' => null, + 'current_image_metadata' => null, + ]); + Log::debug("Plugin {$plugin->id}: cleared image cache due to metadata mismatch"); + } + + /** + * Build canonical image metadata from a Device for cache comparison. + * + * @return array{width: int, height: int, rotation: int, palette_id: int|null, mime_type: string} + */ + public static function buildImageMetadataFromDevice(Device $device): array + { + $device->loadMissing(['deviceModel', 'deviceModel.palette']); + $settings = self::getImageSettings($device); + $paletteId = $device->palette_id ?? $device->deviceModel?->palette_id; + + return [ + 'width' => $settings['width'], + 'height' => $settings['height'], + 'rotation' => $settings['rotation'] ?? 0, + 'palette_id' => $paletteId, + 'mime_type' => $settings['mime_type'], + ]; + } + + /** + * Build canonical image metadata from a DeviceModel for cache comparison. + * + * @return array{width: int, height: int, rotation: int, palette_id: int|null, mime_type: string} + */ + public static function buildImageMetadataFromDeviceModel(DeviceModel $model): array + { + return [ + 'width' => $model->width, + 'height' => $model->height, + 'rotation' => $model->rotation ?? 0, + 'palette_id' => $model->palette_id, + 'mime_type' => $model->mime_type, + ]; + } + + /** + * Check if stored metadata matches the current device or device model. + * Returns false if stored is null or empty so cache is regenerated and metadata is stored. + */ + public static function imageMetadataMatches(?array $stored, Device|DeviceModel $deviceOrModel): bool + { + if ($stored === null || $stored === []) { + return false; + } + + $current = $deviceOrModel instanceof Device + ? self::buildImageMetadataFromDevice($deviceOrModel) + : self::buildImageMetadataFromDeviceModel($deviceOrModel); + + foreach (['width', 'height', 'rotation', 'palette_id', 'mime_type'] as $key) { + if (($stored[$key] ?? null) !== ($current[$key] ?? null)) { + return false; + } + } + + return true; + } + + /** + * 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->deviceModel?->css_name ?? $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, + 'cssVariables' => $device->deviceModel?->css_variables ?? [], + ]; + + // 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 new file mode 100644 index 0000000..8ea2e44 --- /dev/null +++ b/app/Services/OidcProvider.php @@ -0,0 +1,158 @@ +baseUrl = str_replace('/.well-known/openid-configuration', '', $endpoint); + } else { + $this->baseUrl = mb_rtrim($endpoint, '/'); + } + + $this->scopes = $scopes ?: ['openid', 'profile', 'email']; + $this->loadOidcConfiguration(); + } + + /** + * Load OIDC configuration from the well-known endpoint. + */ + protected function loadOidcConfiguration() + { + try { + $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 (! 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(), $e->getCode(), $e); + } + } + + /** + * Get the authentication URL for the provider. + */ + protected function getAuthUrl($state) + { + 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); + } + + /** + * Get the token URL for the provider. + */ + protected function getTokenUrl() + { + 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']; + } + + /** + * Get the raw user for the given access token. + */ + protected function getUserByToken($token) + { + 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, + ], + ]); + + return json_decode($response->getBody(), true); + } + + /** + * Map the raw user array to a Socialite User instance. + */ + public function mapUserToObject(array $user) + { + return (new User)->setRaw($user)->map([ + 'id' => $user['sub'], + 'nickname' => $user['preferred_username'] ?? null, + 'name' => $user['name'] ?? null, + 'email' => $user['email'] ?? null, + 'avatar' => $user['picture'] ?? null, + ]); + } + + /** + * Get the access token response for the given code. + */ + public function getAccessTokenResponse($code) + { + $response = $this->getHttpClient()->post($this->getTokenUrl(), [ + 'headers' => ['Accept' => 'application/json'], + 'form_params' => $this->getTokenFields($code), + ]); + + return json_decode($response->getBody(), true); + } + + /** + * Get the POST fields for the token request. + */ + protected function getTokenFields($code) + { + return array_merge(parent::getTokenFields($code), [ + 'grant_type' => 'authorization_code', + ]); + } +} diff --git a/app/Services/Plugin/Parsers/IcalResponseParser.php b/app/Services/Plugin/Parsers/IcalResponseParser.php new file mode 100644 index 0000000..a1caedb --- /dev/null +++ b/app/Services/Plugin/Parsers/IcalResponseParser.php @@ -0,0 +1,116 @@ +header('Content-Type'); + $body = $response->body(); + + if (! $this->isIcalResponse($contentType, $body)) { + return null; + } + + try { + // Workaround for om/icalparser v4.0.0 bug where it fails if ORGANIZER or ATTENDEE has no parameters. + // When ORGANIZER or ATTENDEE has no parameters (no semicolon after the key), + // IcalParser::parseRow returns an empty string for $middle instead of an array, + // which causes a type error in a foreach loop in IcalParser::parseString. + $normalizedBody = preg_replace('/^(ORGANIZER|ATTENDEE):/m', '$1;CN=Unknown:', $body); + $this->parser->parseString($normalizedBody); + + $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..6556de8 --- /dev/null +++ b/app/Services/Plugin/Parsers/XmlResponseParser.php @@ -0,0 +1,67 @@ +header('Content-Type'); + + if (! $contentType || ! str_contains(mb_strtolower($contentType), 'xml')) { + return null; + } + + try { + $xml = $this->simplexml_load_string_strip_namespaces($response->body()); + if ($xml === false) { + throw new Exception('Invalid XML content'); + } + + return [$xml->getName() => $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; + } + + function simplexml_load_string_strip_namespaces($xml_response) { + $xml = simplexml_load_string($xml_response); + if ($xml === false) { + return false; + } + + $namespaces = array_keys($xml->getDocNamespaces(true)); + $namespaces = array_filter($namespaces, function($name) { return !empty($name); }); + if (count($namespaces) == 0) { + return $xml; + } + $namespaces = array_map(function($ns) { return "$ns:"; }, $namespaces); + + $xml_no_namespaces = str_replace( + array_merge(["xmlns="], $namespaces), + array_merge(["ns="], array_fill(0, count($namespaces), '')), + $xml_response + ); + return simplexml_load_string($xml_no_namespaces); + } +} diff --git a/app/Services/PluginExportService.php b/app/Services/PluginExportService.php new file mode 100644 index 0000000..be98461 --- /dev/null +++ b/app/Services/PluginExportService.php @@ -0,0 +1,182 @@ +generateSettingsYaml($plugin); + $settingsYaml = Yaml::dump($settings, 10, 2, Yaml::DUMP_MULTI_LINE_LITERAL_BLOCK); + File::put($tempDir.'/settings.yml', $settingsYaml); + + $extension = $plugin->markup_language === 'liquid' ? 'liquid' : 'blade.php'; + + // Export full template if it exists + if ($plugin->render_markup) { + $fullTemplate = $this->generateLayoutTemplate($plugin->render_markup); + File::put($tempDir.'/full.'.$extension, $fullTemplate); + } + + // Export layout-specific templates if they exist + if ($plugin->render_markup_half_horizontal) { + $halfHorizontalTemplate = $this->generateLayoutTemplate($plugin->render_markup_half_horizontal); + File::put($tempDir.'/half_horizontal.'.$extension, $halfHorizontalTemplate); + } + + if ($plugin->render_markup_half_vertical) { + $halfVerticalTemplate = $this->generateLayoutTemplate($plugin->render_markup_half_vertical); + File::put($tempDir.'/half_vertical.'.$extension, $halfVerticalTemplate); + } + + if ($plugin->render_markup_quadrant) { + $quadrantTemplate = $this->generateLayoutTemplate($plugin->render_markup_quadrant); + File::put($tempDir.'/quadrant.'.$extension, $quadrantTemplate); + } + + // Export shared template if it exists + if ($plugin->render_markup_shared) { + $sharedTemplate = $this->generateLayoutTemplate($plugin->render_markup_shared); + File::put($tempDir.'/shared.'.$extension, $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 template content from markup, removing wrapper divs if present + */ + private function generateLayoutTemplate(?string $markup): string + { + if (! $markup) { + return ''; + } + + // Remove the wrapper div if it exists (it will be added during import for liquid) + $markup = preg_replace('/^
\s*/', '', $markup); + $markup = preg_replace('/\s*<\/div>\s*$/', '', $markup); + + return mb_trim($markup); + } + + /** + * 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..f3e7a5c --- /dev/null +++ b/app/Services/PluginImportService.php @@ -0,0 +1,738 @@ +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 markup language from the first available file + $markupLanguage = 'blade'; + $firstTemplatePath = $filePaths['fullLiquidPath'] + ?? ($filePaths['halfHorizontalLiquidPath'] ?? null) + ?? ($filePaths['halfVerticalLiquidPath'] ?? null) + ?? ($filePaths['quadrantLiquidPath'] ?? null) + ?? ($filePaths['sharedLiquidPath'] ?? null) + ?? ($filePaths['sharedBladePath'] ?? null); + + if ($firstTemplatePath && pathinfo((string) $firstTemplatePath, PATHINFO_EXTENSION) === 'liquid') { + $markupLanguage = 'liquid'; + } + + // Read full markup (don't prepend shared - it will be prepended at render time) + $fullLiquid = null; + if (isset($filePaths['fullLiquidPath']) && $filePaths['fullLiquidPath']) { + $fullLiquid = File::get($filePaths['fullLiquidPath']); + if ($markupLanguage === 'liquid') { + $fullLiquid = '
'."\n".$fullLiquid."\n".'
'; + } + } + + // Read shared markup separately + $sharedMarkup = null; + if (isset($filePaths['sharedLiquidPath']) && $filePaths['sharedLiquidPath'] && File::exists($filePaths['sharedLiquidPath'])) { + $sharedMarkup = File::get($filePaths['sharedLiquidPath']); + } elseif (isset($filePaths['sharedBladePath']) && $filePaths['sharedBladePath'] && File::exists($filePaths['sharedBladePath'])) { + $sharedMarkup = File::get($filePaths['sharedBladePath']); + } + + // Read layout-specific markups + $halfHorizontalMarkup = null; + if (isset($filePaths['halfHorizontalLiquidPath']) && $filePaths['halfHorizontalLiquidPath'] && File::exists($filePaths['halfHorizontalLiquidPath'])) { + $halfHorizontalMarkup = File::get($filePaths['halfHorizontalLiquidPath']); + if ($markupLanguage === 'liquid') { + $halfHorizontalMarkup = '
'."\n".$halfHorizontalMarkup."\n".'
'; + } + } + + $halfVerticalMarkup = null; + if (isset($filePaths['halfVerticalLiquidPath']) && $filePaths['halfVerticalLiquidPath'] && File::exists($filePaths['halfVerticalLiquidPath'])) { + $halfVerticalMarkup = File::get($filePaths['halfVerticalLiquidPath']); + if ($markupLanguage === 'liquid') { + $halfVerticalMarkup = '
'."\n".$halfVerticalMarkup."\n".'
'; + } + } + + $quadrantMarkup = null; + if (isset($filePaths['quadrantLiquidPath']) && $filePaths['quadrantLiquidPath'] && File::exists($filePaths['quadrantLiquidPath'])) { + $quadrantMarkup = File::get($filePaths['quadrantLiquidPath']); + if ($markupLanguage === 'liquid') { + $quadrantMarkup = '
'."\n".$quadrantMarkup."\n".'
'; + } + } + + // 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 ?? null, + 'render_markup_half_horizontal' => $halfHorizontalMarkup, + 'render_markup_half_vertical' => $halfVerticalMarkup, + 'render_markup_quadrant' => $quadrantMarkup, + 'render_markup_shared' => $sharedMarkup, + '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 markup language from the first available file + $markupLanguage = 'blade'; + $firstTemplatePath = $filePaths['fullLiquidPath'] + ?? ($filePaths['halfHorizontalLiquidPath'] ?? null) + ?? ($filePaths['halfVerticalLiquidPath'] ?? null) + ?? ($filePaths['quadrantLiquidPath'] ?? null) + ?? ($filePaths['sharedLiquidPath'] ?? null) + ?? ($filePaths['sharedBladePath'] ?? null); + + if ($firstTemplatePath && pathinfo((string) $firstTemplatePath, PATHINFO_EXTENSION) === 'liquid') { + $markupLanguage = 'liquid'; + } + + // Read full markup (don't prepend shared - it will be prepended at render time) + $fullLiquid = null; + if (isset($filePaths['fullLiquidPath']) && $filePaths['fullLiquidPath']) { + $fullLiquid = File::get($filePaths['fullLiquidPath']); + if ($markupLanguage === 'liquid') { + $fullLiquid = '
'."\n".$fullLiquid."\n".'
'; + } + } + + // Read shared markup separately + $sharedMarkup = null; + if (isset($filePaths['sharedLiquidPath']) && $filePaths['sharedLiquidPath'] && File::exists($filePaths['sharedLiquidPath'])) { + $sharedMarkup = File::get($filePaths['sharedLiquidPath']); + } elseif (isset($filePaths['sharedBladePath']) && $filePaths['sharedBladePath'] && File::exists($filePaths['sharedBladePath'])) { + $sharedMarkup = File::get($filePaths['sharedBladePath']); + } + + // Read layout-specific markups + $halfHorizontalMarkup = null; + if (isset($filePaths['halfHorizontalLiquidPath']) && $filePaths['halfHorizontalLiquidPath'] && File::exists($filePaths['halfHorizontalLiquidPath'])) { + $halfHorizontalMarkup = File::get($filePaths['halfHorizontalLiquidPath']); + if ($markupLanguage === 'liquid') { + $halfHorizontalMarkup = '
'."\n".$halfHorizontalMarkup."\n".'
'; + } + } + + $halfVerticalMarkup = null; + if (isset($filePaths['halfVerticalLiquidPath']) && $filePaths['halfVerticalLiquidPath'] && File::exists($filePaths['halfVerticalLiquidPath'])) { + $halfVerticalMarkup = File::get($filePaths['halfVerticalLiquidPath']); + if ($markupLanguage === 'liquid') { + $halfVerticalMarkup = '
'."\n".$halfVerticalMarkup."\n".'
'; + } + } + + $quadrantMarkup = null; + if (isset($filePaths['quadrantLiquidPath']) && $filePaths['quadrantLiquidPath'] && File::exists($filePaths['quadrantLiquidPath'])) { + $quadrantMarkup = File::get($filePaths['quadrantLiquidPath']); + if ($markupLanguage === 'liquid') { + $quadrantMarkup = '
'."\n".$quadrantMarkup."\n".'
'; + } + } + + // 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 ?? null, + 'render_markup_half_horizontal' => $halfHorizontalMarkup, + 'render_markup_half_vertical' => $halfVerticalMarkup, + 'render_markup_quadrant' => $quadrantMarkup, + 'render_markup_shared' => $sharedMarkup, + '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; + $halfHorizontalLiquidPath = null; + $halfVerticalLiquidPath = null; + $quadrantLiquidPath = 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 for layout-specific files + if (File::exists($targetDir.'/half_horizontal.liquid')) { + $halfHorizontalLiquidPath = $targetDir.'/half_horizontal.liquid'; + } elseif (File::exists($targetDir.'/half_horizontal.blade.php')) { + $halfHorizontalLiquidPath = $targetDir.'/half_horizontal.blade.php'; + } + + if (File::exists($targetDir.'/half_vertical.liquid')) { + $halfVerticalLiquidPath = $targetDir.'/half_vertical.liquid'; + } elseif (File::exists($targetDir.'/half_vertical.blade.php')) { + $halfVerticalLiquidPath = $targetDir.'/half_vertical.blade.php'; + } + + if (File::exists($targetDir.'/quadrant.liquid')) { + $quadrantLiquidPath = $targetDir.'/quadrant.liquid'; + } elseif (File::exists($targetDir.'/quadrant.blade.php')) { + $quadrantLiquidPath = $targetDir.'/quadrant.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'; + } + + // Check for layout-specific files in src + if (File::exists($targetDir.'/src/half_horizontal.liquid')) { + $halfHorizontalLiquidPath = $targetDir.'/src/half_horizontal.liquid'; + } elseif (File::exists($targetDir.'/src/half_horizontal.blade.php')) { + $halfHorizontalLiquidPath = $targetDir.'/src/half_horizontal.blade.php'; + } + + if (File::exists($targetDir.'/src/half_vertical.liquid')) { + $halfVerticalLiquidPath = $targetDir.'/src/half_vertical.liquid'; + } elseif (File::exists($targetDir.'/src/half_vertical.blade.php')) { + $halfVerticalLiquidPath = $targetDir.'/src/half_vertical.blade.php'; + } + + if (File::exists($targetDir.'/src/quadrant.liquid')) { + $quadrantLiquidPath = $targetDir.'/src/quadrant.liquid'; + } elseif (File::exists($targetDir.'/src/quadrant.blade.php')) { + $quadrantLiquidPath = $targetDir.'/src/quadrant.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'; + } + + // Check for layout-specific files + if (File::exists($tempDir.'/src/half_horizontal.liquid')) { + $halfHorizontalLiquidPath = $tempDir.'/src/half_horizontal.liquid'; + } elseif (File::exists($tempDir.'/src/half_horizontal.blade.php')) { + $halfHorizontalLiquidPath = $tempDir.'/src/half_horizontal.blade.php'; + } + + if (File::exists($tempDir.'/src/half_vertical.liquid')) { + $halfVerticalLiquidPath = $tempDir.'/src/half_vertical.liquid'; + } elseif (File::exists($tempDir.'/src/half_vertical.blade.php')) { + $halfVerticalLiquidPath = $tempDir.'/src/half_vertical.blade.php'; + } + + if (File::exists($tempDir.'/src/quadrant.liquid')) { + $quadrantLiquidPath = $tempDir.'/src/quadrant.liquid'; + } elseif (File::exists($tempDir.'/src/quadrant.blade.php')) { + $quadrantLiquidPath = $tempDir.'/src/quadrant.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; + } elseif ($filename === 'half_horizontal.liquid' || $filename === 'half_horizontal.blade.php') { + $halfHorizontalLiquidPath = $filepath; + } elseif ($filename === 'half_vertical.liquid' || $filename === 'half_vertical.blade.php') { + $halfVerticalLiquidPath = $filepath; + } elseif ($filename === 'quadrant.liquid' || $filename === 'quadrant.blade.php') { + $quadrantLiquidPath = $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'; + } + + // Copy layout-specific files if they exist + if ($halfHorizontalLiquidPath) { + $extension = pathinfo((string) $halfHorizontalLiquidPath, PATHINFO_EXTENSION); + File::copy($halfHorizontalLiquidPath, $newSrcDir.'/half_horizontal.'.$extension); + $halfHorizontalLiquidPath = $newSrcDir.'/half_horizontal.'.$extension; + } + + if ($halfVerticalLiquidPath) { + $extension = pathinfo((string) $halfVerticalLiquidPath, PATHINFO_EXTENSION); + File::copy($halfVerticalLiquidPath, $newSrcDir.'/half_vertical.'.$extension); + $halfVerticalLiquidPath = $newSrcDir.'/half_vertical.'.$extension; + } + + if ($quadrantLiquidPath) { + $extension = pathinfo((string) $quadrantLiquidPath, PATHINFO_EXTENSION); + File::copy($quadrantLiquidPath, $newSrcDir.'/quadrant.'.$extension); + $quadrantLiquidPath = $newSrcDir.'/quadrant.'.$extension; + } + + // Update the paths + $settingsYamlPath = $newSrcDir.'/settings.yml'; + } + } + } + + return [ + 'settingsYamlPath' => $settingsYamlPath, + 'fullLiquidPath' => $fullLiquidPath, + 'sharedLiquidPath' => $sharedLiquidPath, + 'sharedBladePath' => $sharedBladePath, + 'halfHorizontalLiquidPath' => $halfHorizontalLiquidPath, + 'halfVerticalLiquidPath' => $halfVerticalLiquidPath, + 'quadrantLiquidPath' => $quadrantLiquidPath, + ]; + } + + /** + * 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/app/Services/QrCodeService.php b/app/Services/QrCodeService.php new file mode 100644 index 0000000..e20ae42 --- /dev/null +++ b/app/Services/QrCodeService.php @@ -0,0 +1,142 @@ +format = $format; + + return $this; + } + + /** + * Set the size of the QR code + * + * @param int $size The size in pixels + * @return $this + */ + public function size(int $size): self + { + $this->size = $size; + + return $this; + } + + /** + * Set the error correction level + * + * @param string $level Error correction level: 'l', 'm', 'q', 'h' + * @return $this + */ + public function errorCorrection(string $level): self + { + $this->errorCorrection = $level; + + return $this; + } + + /** + * Generate the QR code + * + * @param string $text The text to encode + * @return string The generated QR code (SVG string) + */ + public function generate(string $text): string + { + // Ensure format is set (default to SVG) + $format = $this->format ?? 'svg'; + + if ($format !== 'svg') { + throw new InvalidArgumentException("Format '{$format}' is not supported. Only 'svg' is currently supported."); + } + + // Calculate size and margin + // If size is not set, calculate from module size (default module size is 11) + if ($this->size === null) { + $moduleSize = 11; + $this->size = 29 * $moduleSize; + } + + // Map error correction level + $errorCorrectionLevel = ErrorCorrectionLevel::valueOf('M'); // default + if ($this->errorCorrection !== null) { + $errorCorrectionLevel = match (mb_strtoupper($this->errorCorrection)) { + 'L' => ErrorCorrectionLevel::valueOf('L'), + 'M' => ErrorCorrectionLevel::valueOf('M'), + 'Q' => ErrorCorrectionLevel::valueOf('Q'), + 'H' => ErrorCorrectionLevel::valueOf('H'), + default => ErrorCorrectionLevel::valueOf('M'), + }; + } + + // Create renderer style with size and margin + $rendererStyle = new RendererStyle($this->size, 0); + + // Create SVG renderer + $renderer = new ImageRenderer( + $rendererStyle, + new SvgImageBackEnd() + ); + + // Create writer + $writer = new Writer($renderer); + + // Generate SVG + $svg = $writer->writeString($text, 'ISO-8859-1', $errorCorrectionLevel); + + // Add class="qr-code" to the SVG element + $svg = $this->addQrCodeClass($svg); + + return $svg; + } + + /** + * Add the 'qr-code' class to the SVG element + * + * @param string $svg The SVG string + * @return string The SVG string with the class added + */ + protected function addQrCodeClass(string $svg): string + { + // Match + if (preg_match('/]*)>/', $svg, $matches)) { + $attributes = $matches[1]; + // Check if class already exists + if (mb_strpos($attributes, 'class=') === false) { + $svg = preg_replace('/]*)>/', '', $svg, 1); + } else { + // If class exists, add qr-code to it + $svg = preg_replace('/(]*class=["\'])([^"\']*)(["\'][^>]*>)/', '$1$2 qr-code$3', $svg, 1); + } + } else { + // Fallback: simple replacement if no attributes + $svg = preg_replace('//', '', $svg, 1); + } + + return $svg; + } +} diff --git a/app/Settings/UpdateSettings.php b/app/Settings/UpdateSettings.php new file mode 100644 index 0000000..d3ab45c --- /dev/null +++ b/app/Settings/UpdateSettings.php @@ -0,0 +1,15 @@ +=5.5" + }, + "require-dev": { + "phpunit/phpunit": "^4.8.35||^5.6.3||^9.5", + "yoast/phpunit-polyfills": "^1.0" + }, + "suggest": { + "ext-awscrt": "Make sure you install awscrt native extension to use any of the functionality." + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "AWS SDK Common Runtime Team", + "email": "aws-sdk-common-runtime@amazon.com" + } + ], + "description": "AWS Common Runtime for PHP", + "homepage": "https://github.com/awslabs/aws-crt-php", + "keywords": [ + "amazon", + "aws", + "crt", + "sdk" + ], + "support": { + "issues": "https://github.com/awslabs/aws-crt-php/issues", + "source": "https://github.com/awslabs/aws-crt-php/tree/v1.2.7" + }, + "time": "2024-10-18T22:15:13+00:00" + }, + { + "name": "aws/aws-sdk-php", + "version": "3.373.2", + "source": { + "type": "git", + "url": "https://github.com/aws/aws-sdk-php.git", + "reference": "483fba51c28b3a0c0647bf5100e0edca82090b18" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/483fba51c28b3a0c0647bf5100e0edca82090b18", + "reference": "483fba51c28b3a0c0647bf5100e0edca82090b18", + "shasum": "" + }, + "require": { + "aws/aws-crt-php": "^1.2.3", + "ext-json": "*", + "ext-pcre": "*", + "ext-simplexml": "*", + "guzzlehttp/guzzle": "^7.4.5", + "guzzlehttp/promises": "^2.0", + "guzzlehttp/psr7": "^2.4.5", + "mtdowling/jmespath.php": "^2.8.0", + "php": ">=8.1", + "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", + "aws/aws-php-sns-message-validator": "~1.0", + "behat/behat": "~3.0", + "composer/composer": "^2.7.8", + "dms/phpunit-arraysubset-asserts": "^v0.5.0", + "doctrine/cache": "~1.4", + "ext-dom": "*", + "ext-openssl": "*", + "ext-sockets": "*", + "phpunit/phpunit": "^10.0", + "psr/cache": "^2.0 || ^3.0", + "psr/simple-cache": "^2.0 || ^3.0", + "sebastian/comparator": "^1.2.3 || ^4.0 || ^5.0", + "yoast/phpunit-polyfills": "^2.0" + }, + "suggest": { + "aws/aws-php-sns-message-validator": "To validate incoming SNS notifications", + "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", + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" + } + }, + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "Aws\\": "src/" + }, + "exclude-from-classmap": [ + "src/data/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "Amazon Web Services", + "homepage": "https://aws.amazon.com" + } + ], + "description": "AWS SDK for PHP - Use Amazon Web Services in your PHP project", + "homepage": "https://aws.amazon.com/sdk-for-php", + "keywords": [ + "amazon", + "aws", + "cloud", + "dynamodb", + "ec2", + "glacier", + "s3", + "sdk" + ], + "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.373.2" + }, + "time": "2026-03-13T18:08:30+00:00" + }, + { + "name": "bacon/bacon-qr-code", + "version": "v3.0.3", + "source": { + "type": "git", + "url": "https://github.com/Bacon/BaconQrCode.git", + "reference": "36a1cb2b81493fa5b82e50bf8068bf84d1542563" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Bacon/BaconQrCode/zipball/36a1cb2b81493fa5b82e50bf8068bf84d1542563", + "reference": "36a1cb2b81493fa5b82e50bf8068bf84d1542563", + "shasum": "" + }, + "require": { + "dasprid/enum": "^1.0.3", + "ext-iconv": "*", + "php": "^8.1" + }, + "require-dev": { + "phly/keep-a-changelog": "^2.12", + "phpunit/phpunit": "^10.5.11 || ^11.0.4", + "spatie/phpunit-snapshot-assertions": "^5.1.5", + "spatie/pixelmatch-php": "^1.2.0", + "squizlabs/php_codesniffer": "^3.9" + }, + "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/v3.0.3" + }, + "time": "2025-11-19T17:15:36+00:00" + }, + { + "name": "bnussbau/laravel-trmnl-blade", + "version": "2.3.3", + "source": { + "type": "git", + "url": "https://github.com/bnussbau/laravel-trmnl-blade.git", + "reference": "777af3e26a6bee154efd87caf06131cc18c84452" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/bnussbau/laravel-trmnl-blade/zipball/777af3e26a6bee154efd87caf06131cc18c84452", + "reference": "777af3e26a6bee154efd87caf06131cc18c84452", "shasum": "" }, "require": { "illuminate/contracts": "^10.0||^11.0||^12.0", "php": "^8.2", - "spatie/laravel-package-tools": "^1.18", - "voku/simple_html_dom": "^4.8" + "spatie/laravel-package-tools": "^1.18" }, "require-dev": { - "larastan/larastan": "^2.9||^3.0", "laravel/pint": "^1.14", "nunomaduro/collision": "^8.1.1||^7.10.0", - "orchestra/testbench": "10.*||^9.0.0||^8.22.0", + "orchestra/testbench": "^10.0.0||^9.0.0||^8.22.0", "pestphp/pest": "^3.0", "pestphp/pest-plugin-arch": "^3.0", - "pestphp/pest-plugin-laravel": "^3.0", - "phpstan/extension-installer": "^1.3||^2.0", - "phpstan/phpstan-deprecation-rules": "^1.1||^2.0", - "phpstan/phpstan-phpunit": "^1.3||^2.0" + "pestphp/pest-plugin-laravel": "^3.0" }, "type": "library", "extra": { "laravel": { "aliases": { - "Trmnl": "Bnussbau\\LaravelTrmnl\\Facades\\LaravelTrmnl" + "TrmnlBlade": "Bnussbau\\TrmnlBlade\\Facades\\TrmnlBlade" }, "providers": [ - "Bnussbau\\LaravelTrmnl\\LaravelTrmnlServiceProvider" + "Bnussbau\\TrmnlBlade\\TrmnlBladeServiceProvider" ] } }, "autoload": { "psr-4": { - "Bnussbau\\LaravelTrmnl\\": "src/", - "Bnussbau\\LaravelTrmnl\\Database\\Factories\\": "database/factories/" + "Bnussbau\\TrmnlBlade\\": "src/", + "Bnussbau\\TrmnlBlade\\Database\\Factories\\": "database/factories/" } }, "notification-url": "https://packagist.org/downloads/", @@ -66,46 +267,127 @@ "role": "Developer" } ], - "description": "Develop TRMNL plugins with Laravel", - "homepage": "https://github.com/bnussbau/laravel-trmnl", + "description": "Blade Components on top of the TRMNL Design System", + "homepage": "https://github.com/bnussbau/laravel-trmnl-blade", "keywords": [ "Benjamin Nussbaum", "TRMNL", + "blade", + "design system", "laravel" ], "support": { - "issues": "https://github.com/bnussbau/laravel-trmnl/issues", - "source": "https://github.com/bnussbau/laravel-trmnl/tree/0.1.4" + "issues": "https://github.com/bnussbau/laravel-trmnl-blade/issues", + "source": "https://github.com/bnussbau/laravel-trmnl-blade/tree/2.3.3" }, "funding": [ { - "url": "https://usetrmnl.com/?ref=laravel-trmnl", + "url": "https://www.buymeacoffee.com/bnussbau", + "type": "buy_me_a_coffee" + }, + { + "url": "https://trmnl.com/?ref=laravel-trmnl", "type": "custom" + }, + { + "url": "https://github.com/bnussbau", + "type": "github" } ], - "time": "2025-02-23T12:50:40+00:00" + "time": "2026-03-03T06:47:48+00:00" }, { - "name": "brick/math", - "version": "0.12.3", + "name": "bnussbau/trmnl-pipeline-php", + "version": "0.8.0", "source": { "type": "git", - "url": "https://github.com/brick/math.git", - "reference": "866551da34e9a618e64a819ee1e01c20d8a588ba" + "url": "https://github.com/bnussbau/trmnl-pipeline-php.git", + "reference": "f7c86bf655d6f8ddd88e48575d0c9588c33eb07b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/brick/math/zipball/866551da34e9a618e64a819ee1e01c20d8a588ba", - "reference": "866551da34e9a618e64a819ee1e01c20d8a588ba", + "url": "https://api.github.com/repos/bnussbau/trmnl-pipeline-php/zipball/f7c86bf655d6f8ddd88e48575d0c9588c33eb07b", + "reference": "f7c86bf655d6f8ddd88e48575d0c9588c33eb07b", "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.8.0" + }, + "funding": [ + { + "url": "https://www.buymeacoffee.com/bnussbau", + "type": "buy_me_a_coffee" + }, + { + "url": "https://trmnl.com/?ref=laravel-trmnl", + "type": "custom" + }, + { + "url": "https://github.com/bnussbau", + "type": "github" + } + ], + "time": "2026-02-12T16:53:44+00:00" + }, + { + "name": "brick/math", + "version": "0.14.8", + "source": { + "type": "git", + "url": "https://github.com/brick/math.git", + "reference": "63422359a44b7f06cae63c3b429b59e8efcc0629" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/brick/math/zipball/63422359a44b7f06cae63c3b429b59e8efcc0629", + "reference": "63422359a44b7f06cae63c3b429b59e8efcc0629", + "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": { @@ -135,7 +417,7 @@ ], "support": { "issues": "https://github.com/brick/math/issues", - "source": "https://github.com/brick/math/tree/0.12.3" + "source": "https://github.com/brick/math/tree/0.14.8" }, "funding": [ { @@ -143,7 +425,7 @@ "type": "github" } ], - "time": "2025-02-28T13:11:00+00:00" + "time": "2026-02-10T14:33:43+00:00" }, { "name": "carbonphp/carbon-doctrine-types", @@ -214,6 +496,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", @@ -290,34 +622,81 @@ "time": "2024-07-08T12:26:09+00:00" }, { - "name": "doctrine/inflector", - "version": "2.0.10", + "name": "doctrine/deprecations", + "version": "1.1.6", "source": { "type": "git", - "url": "https://github.com/doctrine/inflector.git", - "reference": "5817d0659c5b50c9b950feb9af7b9668e2c436bc" + "url": "https://github.com/doctrine/deprecations.git", + "reference": "d4fe3e6fd9bb9e72557a19674f44d8ac7db4c6ca" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/inflector/zipball/5817d0659c5b50c9b950feb9af7b9668e2c436bc", - "reference": "5817d0659c5b50c9b950feb9af7b9668e2c436bc", + "url": "https://api.github.com/repos/doctrine/deprecations/zipball/d4fe3e6fd9bb9e72557a19674f44d8ac7db4c6ca", + "reference": "d4fe3e6fd9bb9e72557a19674f44d8ac7db4c6ca", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "conflict": { + "phpunit/phpunit": "<=7.5 || >=14" + }, + "require-dev": { + "doctrine/coding-standard": "^9 || ^12 || ^14", + "phpstan/phpstan": "1.4.10 || 2.1.30", + "phpstan/phpstan-phpunit": "^1.0 || ^2", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.6 || ^10.5 || ^11.5 || ^12.4 || ^13.0", + "psr/log": "^1 || ^2 || ^3" + }, + "suggest": { + "psr/log": "Allows logging deprecations via PSR-3 logger implementation" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Deprecations\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A small layer on top of trigger_error(E_USER_DEPRECATED) or PSR-3 logging with options to disable all deprecations or selectively for packages.", + "homepage": "https://www.doctrine-project.org/", + "support": { + "issues": "https://github.com/doctrine/deprecations/issues", + "source": "https://github.com/doctrine/deprecations/tree/1.1.6" + }, + "time": "2026-02-07T07:09:04+00:00" + }, + { + "name": "doctrine/inflector", + "version": "2.1.0", + "source": { + "type": "git", + "url": "https://github.com/doctrine/inflector.git", + "reference": "6d6c96277ea252fc1304627204c3d5e6e15faa3b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/inflector/zipball/6d6c96277ea252fc1304627204c3d5e6e15faa3b", + "reference": "6d6c96277ea252fc1304627204c3d5e6e15faa3b", "shasum": "" }, "require": { "php": "^7.2 || ^8.0" }, "require-dev": { - "doctrine/coding-standard": "^11.0", - "phpstan/phpstan": "^1.8", - "phpstan/phpstan-phpunit": "^1.1", - "phpstan/phpstan-strict-rules": "^1.3", - "phpunit/phpunit": "^8.5 || ^9.5", - "vimeo/psalm": "^4.25 || ^5.4" + "doctrine/coding-standard": "^12.0 || ^13.0", + "phpstan/phpstan": "^1.12 || ^2.0", + "phpstan/phpstan-phpunit": "^1.4 || ^2.0", + "phpstan/phpstan-strict-rules": "^1.6 || ^2.0", + "phpunit/phpunit": "^8.5 || ^12.2" }, "type": "library", "autoload": { "psr-4": { - "Doctrine\\Inflector\\": "lib/Doctrine/Inflector" + "Doctrine\\Inflector\\": "src" } }, "notification-url": "https://packagist.org/downloads/", @@ -362,7 +741,7 @@ ], "support": { "issues": "https://github.com/doctrine/inflector/issues", - "source": "https://github.com/doctrine/inflector/tree/2.0.10" + "source": "https://github.com/doctrine/inflector/tree/2.1.0" }, "funding": [ { @@ -378,7 +757,7 @@ "type": "tidelift" } ], - "time": "2024-02-18T20:23:39+00:00" + "time": "2025-08-10T19:31:58+00:00" }, { "name": "doctrine/lexer", @@ -459,29 +838,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": { @@ -512,7 +890,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": [ { @@ -520,20 +898,20 @@ "type": "github" } ], - "time": "2024-10-09T13:47:03+00:00" + "time": "2025-10-31T18:51:33+00:00" }, { "name": "egulias/email-validator", - "version": "4.0.3", + "version": "4.0.4", "source": { "type": "git", "url": "https://github.com/egulias/EmailValidator.git", - "reference": "b115554301161fa21467629f1e1391c1936de517" + "reference": "d42c8731f0624ad6bdc8d3e5e9a4524f68801cfa" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/egulias/EmailValidator/zipball/b115554301161fa21467629f1e1391c1936de517", - "reference": "b115554301161fa21467629f1e1391c1936de517", + "url": "https://api.github.com/repos/egulias/EmailValidator/zipball/d42c8731f0624ad6bdc8d3e5e9a4524f68801cfa", + "reference": "d42c8731f0624ad6bdc8d3e5e9a4524f68801cfa", "shasum": "" }, "require": { @@ -579,7 +957,7 @@ ], "support": { "issues": "https://github.com/egulias/EmailValidator/issues", - "source": "https://github.com/egulias/EmailValidator/tree/4.0.3" + "source": "https://github.com/egulias/EmailValidator/tree/4.0.4" }, "funding": [ { @@ -587,35 +965,159 @@ "type": "github" } ], - "time": "2024-12-27T00:36:43+00:00" + "time": "2025-03-06T22:45:56+00:00" }, { - "name": "fruitcake/php-cors", - "version": "v1.3.0", + "name": "ezyang/htmlpurifier", + "version": "v4.19.0", "source": { "type": "git", - "url": "https://github.com/fruitcake/php-cors.git", - "reference": "3d158f36e7875e2f040f37bc0573956240a5a38b" + "url": "https://github.com/ezyang/htmlpurifier.git", + "reference": "b287d2a16aceffbf6e0295559b39662612b77fcf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/fruitcake/php-cors/zipball/3d158f36e7875e2f040f37bc0573956240a5a38b", - "reference": "3d158f36e7875e2f040f37bc0573956240a5a38b", + "url": "https://api.github.com/repos/ezyang/htmlpurifier/zipball/b287d2a16aceffbf6e0295559b39662612b77fcf", + "reference": "b287d2a16aceffbf6e0295559b39662612b77fcf", "shasum": "" }, "require": { - "php": "^7.4|^8.0", - "symfony/http-foundation": "^4.4|^5.4|^6|^7" + "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": { - "phpstan/phpstan": "^1.4", + "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.3", + "source": { + "type": "git", + "url": "https://github.com/firebase/php-jwt.git", + "reference": "28aa0694bcfdfa5e2959c394d5a1ee7a5083629e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/firebase/php-jwt/zipball/28aa0694bcfdfa5e2959c394d5a1ee7a5083629e", + "reference": "28aa0694bcfdfa5e2959c394d5a1ee7a5083629e", + "shasum": "" + }, + "require": { + "php": "^8.0" + }, + "require-dev": { + "guzzlehttp/guzzle": "^7.4", + "phpspec/prophecy-phpunit": "^2.0", + "phpunit/phpunit": "^9.5", + "psr/cache": "^2.0||^3.0", + "psr/http-client": "^1.0", + "psr/http-factory": "^1.0" + }, + "suggest": { + "ext-sodium": "Support EdDSA (Ed25519) signatures", + "paragonie/sodium_compat": "Support EdDSA (Ed25519) signatures when libsodium is not present" + }, + "type": "library", + "autoload": { + "psr-4": { + "Firebase\\JWT\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Neuman Vong", + "email": "neuman+pear@twilio.com", + "role": "Developer" + }, + { + "name": "Anant Narayanan", + "email": "anant@php.net", + "role": "Developer" + } + ], + "description": "A simple library to encode and decode JSON Web Tokens (JWT) in PHP. Should conform to the current spec.", + "homepage": "https://github.com/firebase/php-jwt", + "keywords": [ + "jwt", + "php" + ], + "support": { + "issues": "https://github.com/firebase/php-jwt/issues", + "source": "https://github.com/firebase/php-jwt/tree/v7.0.3" + }, + "time": "2026-02-25T22:16:40+00:00" + }, + { + "name": "fruitcake/php-cors", + "version": "v1.4.0", + "source": { + "type": "git", + "url": "https://github.com/fruitcake/php-cors.git", + "reference": "38aaa6c3fd4c157ffe2a4d10aa8b9b16ba8de379" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/fruitcake/php-cors/zipball/38aaa6c3fd4c157ffe2a4d10aa8b9b16ba8de379", + "reference": "38aaa6c3fd4c157ffe2a4d10aa8b9b16ba8de379", + "shasum": "" + }, + "require": { + "php": "^8.1", + "symfony/http-foundation": "^5.4|^6.4|^7.3|^8" + }, + "require-dev": { + "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": { @@ -646,7 +1148,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": [ { @@ -658,28 +1160,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": { @@ -708,7 +1210,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": [ { @@ -720,26 +1222,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.2", + "version": "7.10.0", "source": { "type": "git", "url": "https://github.com/guzzle/guzzle.git", - "reference": "d281ed313b989f213357e3be1a179f02196ac99b" + "reference": "b51ac707cfa420b7bfd4e4d5e510ba8008e822b4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/guzzle/zipball/d281ed313b989f213357e3be1a179f02196ac99b", - "reference": "d281ed313b989f213357e3be1a179f02196ac99b", + "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" @@ -830,7 +1332,7 @@ ], "support": { "issues": "https://github.com/guzzle/guzzle/issues", - "source": "https://github.com/guzzle/guzzle/tree/7.9.2" + "source": "https://github.com/guzzle/guzzle/tree/7.10.0" }, "funding": [ { @@ -846,20 +1348,20 @@ "type": "tidelift" } ], - "time": "2024-07-24T11:22:20+00:00" + "time": "2025-08-23T22:36:01+00:00" }, { "name": "guzzlehttp/promises", - "version": "2.0.4", + "version": "2.3.0", "source": { "type": "git", "url": "https://github.com/guzzle/promises.git", - "reference": "f9c436286ab2892c7db7be8c8da4ef61ccf7b455" + "reference": "481557b130ef3790cf82b713667b43030dc9c957" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/promises/zipball/f9c436286ab2892c7db7be8c8da4ef61ccf7b455", - "reference": "f9c436286ab2892c7db7be8c8da4ef61ccf7b455", + "url": "https://api.github.com/repos/guzzle/promises/zipball/481557b130ef3790cf82b713667b43030dc9c957", + "reference": "481557b130ef3790cf82b713667b43030dc9c957", "shasum": "" }, "require": { @@ -867,7 +1369,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": { @@ -913,7 +1415,7 @@ ], "support": { "issues": "https://github.com/guzzle/promises/issues", - "source": "https://github.com/guzzle/promises/tree/2.0.4" + "source": "https://github.com/guzzle/promises/tree/2.3.0" }, "funding": [ { @@ -929,20 +1431,20 @@ "type": "tidelift" } ], - "time": "2024-10-17T10:06:22+00:00" + "time": "2025-08-22T14:34:08+00:00" }, { "name": "guzzlehttp/psr7", - "version": "2.7.0", + "version": "2.9.0", "source": { "type": "git", "url": "https://github.com/guzzle/psr7.git", - "reference": "a70f5c95fb43bc83f07c9c948baa0dc1829bf201" + "reference": "7d0ed42f28e42d61352a7a79de682e5e67fec884" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/psr7/zipball/a70f5c95fb43bc83f07c9c948baa0dc1829bf201", - "reference": "a70f5c95fb43bc83f07c9c948baa0dc1829bf201", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/7d0ed42f28e42d61352a7a79de682e5e67fec884", + "reference": "7d0ed42f28e42d61352a7a79de682e5e67fec884", "shasum": "" }, "require": { @@ -958,7 +1460,8 @@ "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" + "jshttp/mime-db": "1.54.0.1", + "phpunit/phpunit": "^8.5.44 || ^9.6.25" }, "suggest": { "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses" @@ -1029,7 +1532,7 @@ ], "support": { "issues": "https://github.com/guzzle/psr7/issues", - "source": "https://github.com/guzzle/psr7/tree/2.7.0" + "source": "https://github.com/guzzle/psr7/tree/2.9.0" }, "funding": [ { @@ -1045,20 +1548,20 @@ "type": "tidelift" } ], - "time": "2024-07-18T11:15:46+00:00" + "time": "2026-03-10T16:41:02+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": { @@ -1067,7 +1570,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", @@ -1115,7 +1618,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": [ { @@ -1131,35 +1634,47 @@ "type": "tidelift" } ], - "time": "2025-02-03T10:55:03+00:00" + "time": "2025-08-22T14:27:06+00:00" }, { - "name": "intervention/gif", - "version": "4.2.1", + "name": "hammerstone/sidecar", + "version": "v0.7.1", "source": { "type": "git", - "url": "https://github.com/Intervention/gif.git", - "reference": "6addac2c68b4bc0e37d0d3ccedda57eb84729c49" + "url": "https://github.com/aarondfrancis/sidecar.git", + "reference": "e30df1a441bd5a61d3da9342328926227c63610f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Intervention/gif/zipball/6addac2c68b4bc0e37d0d3ccedda57eb84729c49", - "reference": "6addac2c68b4bc0e37d0d3ccedda57eb84729c49", + "url": "https://api.github.com/repos/aarondfrancis/sidecar/zipball/e30df1a441bd5a61d3da9342328926227c63610f", + "reference": "e30df1a441bd5a61d3da9342328926227c63610f", "shasum": "" }, "require": { + "aws/aws-sdk-php": "^3.216.1", + "guzzlehttp/guzzle": "^6.5.8|^7.2", + "illuminate/console": "^8|^9|^10|^11|^12.0", + "illuminate/filesystem": "^8|^9|^10|^11|^12.0", + "illuminate/support": "^8|^9|^10|^11|^12.0", + "maennchen/zipstream-php": "^3.1", "php": "^8.1" }, "require-dev": { - "phpstan/phpstan": "^2.1", - "phpunit/phpunit": "^10.0 || ^11.0", - "slevomat/coding-standard": "~8.0", - "squizlabs/php_codesniffer": "^3.8" + "mockery/mockery": "^1.3.3", + "orchestra/testbench": "^5|^6|^7|^8|^9|^10.0", + "phpunit/phpunit": ">=8.5.23|^9|^10" }, "type": "library", + "extra": { + "laravel": { + "providers": [ + "Hammerstone\\Sidecar\\Providers\\SidecarServiceProvider" + ] + } + }, "autoload": { "psr-4": { - "Intervention\\Gif\\": "src" + "Hammerstone\\Sidecar\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -1168,72 +1683,128 @@ ], "authors": [ { - "name": "Oliver Vogel", - "email": "oliver@intervention.io", - "homepage": "https://intervention.io/" + "name": "Aaron Francis", + "email": "aaron@hammerstone.dev" } ], - "description": "Native PHP GIF Encoder/Decoder", - "homepage": "https://github.com/intervention/gif", - "keywords": [ - "animation", - "gd", - "gif", - "image" - ], + "description": "A Laravel package to deploy Lambda functions alongside your main application.", "support": { - "issues": "https://github.com/Intervention/gif/issues", - "source": "https://github.com/Intervention/gif/tree/4.2.1" + "issues": "https://github.com/aarondfrancis/sidecar/issues", + "source": "https://github.com/aarondfrancis/sidecar/tree/v0.7.1" }, - "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-01-05T10:52:39+00:00" + "time": "2025-08-22T14:58:51+00:00" }, { - "name": "intervention/image", - "version": "3.11.2", + "name": "keepsuit/laravel-liquid", + "version": "v0.5.5", "source": { "type": "git", - "url": "https://github.com/Intervention/image.git", - "reference": "ebbb711871fb261c064cf4c422f5f3c124fe1842" + "url": "https://github.com/keepsuit/laravel-liquid.git", + "reference": "7f9996cc2eac16489d33d76e265939c2d556bded" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Intervention/image/zipball/ebbb711871fb261c064cf4c422f5f3c124fe1842", - "reference": "ebbb711871fb261c064cf4c422f5f3c124fe1842", + "url": "https://api.github.com/repos/keepsuit/laravel-liquid/zipball/7f9996cc2eac16489d33d76e265939c2d556bded", + "reference": "7f9996cc2eac16489d33d76e265939c2d556bded", + "shasum": "" + }, + "require": { + "illuminate/contracts": "^10.0 || ^11.0 || ^12.0", + "keepsuit/liquid": "^0.7 || ^0.8 || ^0.9", + "php": "^8.1", + "spatie/laravel-package-tools": "^1.16", + "symfony/var-exporter": "^6.3 || ^7.0 || ^8.0" + }, + "require-dev": { + "itsgoingd/clockwork": "^5.3", + "larastan/larastan": "^3.0", + "laravel/pint": "^1.0", + "mockery/mockery": "^1.6", + "nunomaduro/collision": "^7.8 || ^8.0 || ^9.0", + "orchestra/testbench": "^8.14 || ^9.0 || ^10.0", + "pestphp/pest": "^2.13 || ^3.0", + "pestphp/pest-plugin-arch": "^2.5 || ^3.0", + "pestphp/pest-plugin-laravel": "^2.2 || ^3.0", + "phpstan/extension-installer": "^1.1", + "phpstan/phpstan-deprecation-rules": "^2.0", + "spatie/laravel-ray": "^1.26" + }, + "type": "library", + "extra": { + "laravel": { + "aliases": { + "Liquid": "Keepsuit\\LaravelLiquid\\Facades\\Liquid" + }, + "providers": [ + "Keepsuit\\LaravelLiquid\\LiquidServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "Keepsuit\\LaravelLiquid\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabio Capucci", + "email": "f.capucci@keepsuit.com", + "role": "Developer" + } + ], + "description": "Liquid template engine for Laravel", + "homepage": "https://github.com/keepsuit/laravel-liquid", + "keywords": [ + "keepsuit", + "laravel", + "liquid" + ], + "support": { + "issues": "https://github.com/keepsuit/laravel-liquid/issues", + "source": "https://github.com/keepsuit/laravel-liquid/tree/v0.5.5" + }, + "time": "2026-03-04T16:46:40+00:00" + }, + { + "name": "keepsuit/liquid", + "version": "v0.9.1", + "source": { + "type": "git", + "url": "https://github.com/keepsuit/php-liquid.git", + "reference": "844d88540524f99d9039916e0ef688b7f222ebc0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/keepsuit/php-liquid/zipball/844d88540524f99d9039916e0ef688b7f222ebc0", + "reference": "844d88540524f99d9039916e0ef688b7f222ebc0", "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." + "laravel/pint": "^1.2", + "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 || ^8.0", + "symfony/var-exporter": "^6.1 || ^7.0 || ^8.0", + "symfony/yaml": "^6.1 || ^7.0 || ^8.0" }, "type": "library", "autoload": { "psr-4": { - "Intervention\\Image\\": "src" + "Keepsuit\\Liquid\\": "src" } }, "notification-url": "https://packagist.org/downloads/", @@ -1242,57 +1813,102 @@ ], "authors": [ { - "name": "Oliver Vogel", - "email": "oliver@intervention.io", - "homepage": "https://intervention.io/" + "name": "Fabio Capucci", + "email": "f.capucci@keepsuit.com", + "role": "Developer" } ], - "description": "PHP image manipulation", - "homepage": "https://image.intervention.io/", + "description": "PHP implementation of liquid markup language", + "homepage": "https://github.com/keepsuit/php-liquid", "keywords": [ - "gd", - "image", - "imagick", - "resize", - "thumbnail", - "watermark" + "keepsuit", + "liquid" ], "support": { - "issues": "https://github.com/Intervention/image/issues", - "source": "https://github.com/Intervention/image/tree/3.11.2" + "issues": "https://github.com/keepsuit/php-liquid/issues", + "source": "https://github.com/keepsuit/php-liquid/tree/v0.9.1" }, - "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-02-27T13:08:55+00:00" + "time": "2025-12-01T12:01:51+00:00" }, { - "name": "laravel/framework", - "version": "v12.2.0", + "name": "laravel/fortify", + "version": "v1.36.1", "source": { "type": "git", - "url": "https://github.com/laravel/framework.git", - "reference": "2fb06941bc69ea92f28b2888535ab144ee006889" + "url": "https://github.com/laravel/fortify.git", + "reference": "cad8bfeb63f6818f173d40090725c565c92651d4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/framework/zipball/2fb06941bc69ea92f28b2888535ab144ee006889", - "reference": "2fb06941bc69ea92f28b2888535ab144ee006889", + "url": "https://api.github.com/repos/laravel/fortify/zipball/cad8bfeb63f6818f173d40090725c565c92651d4", + "reference": "cad8bfeb63f6818f173d40090725c565c92651d4", "shasum": "" }, "require": { - "brick/math": "^0.11|^0.12", + "bacon/bacon-qr-code": "^3.0", + "ext-json": "*", + "illuminate/console": "^10.0|^11.0|^12.0|^13.0", + "illuminate/support": "^10.0|^11.0|^12.0|^13.0", + "php": "^8.1", + "pragmarx/google2fa": "^9.0" + }, + "require-dev": { + "orchestra/testbench": "^8.36|^9.15|^10.8|^11.0", + "phpstan/phpstan": "^1.10" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Laravel\\Fortify\\FortifyServiceProvider" + ] + }, + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Laravel\\Fortify\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "Backend controllers and scaffolding for Laravel authentication.", + "keywords": [ + "auth", + "laravel" + ], + "support": { + "issues": "https://github.com/laravel/fortify/issues", + "source": "https://github.com/laravel/fortify" + }, + "time": "2026-03-10T19:59:49+00:00" + }, + { + "name": "laravel/framework", + "version": "v12.54.1", + "source": { + "type": "git", + "url": "https://github.com/laravel/framework.git", + "reference": "325497463e7599cd14224c422c6e5dd2fe832868" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/framework/zipball/325497463e7599cd14224c422c6e5dd2fe832868", + "reference": "325497463e7599cd14224c422c6e5dd2fe832868", + "shasum": "" + }, + "require": { + "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", @@ -1309,7 +1925,7 @@ "guzzlehttp/uri-template": "^1.0", "laravel/prompts": "^0.3.0", "laravel/serializable-closure": "^1.3|^2.0", - "league/commonmark": "^2.6", + "league/commonmark": "^2.8.1", "league/flysystem": "^3.25.1", "league/flysystem-local": "^3.25.1", "league/uri": "^7.5.1", @@ -1328,7 +1944,9 @@ "symfony/http-kernel": "^7.2.0", "symfony/mailer": "^7.2.0", "symfony/mime": "^7.2.0", - "symfony/polyfill-php83": "^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", @@ -1364,6 +1982,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", @@ -1373,6 +1992,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", @@ -1396,13 +2016,14 @@ "league/flysystem-read-only": "^3.25.1", "league/flysystem-sftp-v3": "^3.25.1", "mockery/mockery": "^1.6.10", - "orchestra/testbench-core": "^10.0.0", - "pda/pheanstalk": "^5.0.6", + "opis/json-schema": "^2.4.1", + "orchestra/testbench-core": "^10.9.0", + "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", - "resend/resend-php": "^0.10.0", + "predis/predis": "^2.3|^3.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", @@ -1421,7 +2042,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).", @@ -1433,10 +2054,10 @@ "pda/pheanstalk": "Required to use the beanstalk queue driver (^5.0).", "php-http/discovery": "Required to use PSR-7 bridging features (^1.15).", "phpunit/phpunit": "Required to use assertions and run tests (^10.5.35|^11.5.3|^12.0.1).", - "predis/predis": "Required to use the predis connector (^2.3).", + "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).", @@ -1458,6 +2079,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" ], @@ -1466,7 +2088,8 @@ "Illuminate\\Support\\": [ "src/Illuminate/Macroable/", "src/Illuminate/Collections/", - "src/Illuminate/Conditionable/" + "src/Illuminate/Conditionable/", + "src/Illuminate/Reflection/" ] } }, @@ -1490,38 +2113,38 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2025-03-12T14:38:20+00:00" + "time": "2026-03-10T20:25:56+00:00" }, { "name": "laravel/prompts", - "version": "v0.3.5", + "version": "v0.3.14", "source": { "type": "git", "url": "https://github.com/laravel/prompts.git", - "reference": "57b8f7efe40333cdb925700891c7d7465325d3b1" + "reference": "9f0e371244eedfe2ebeaa72c79c54bb5df6e0176" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/prompts/zipball/57b8f7efe40333cdb925700891c7d7465325d3b1", - "reference": "57b8f7efe40333cdb925700891c7d7465325d3b1", + "url": "https://api.github.com/repos/laravel/prompts/zipball/9f0e371244eedfe2ebeaa72c79c54bb5df6e0176", + "reference": "9f0e371244eedfe2ebeaa72c79c54bb5df6e0176", "shasum": "" }, "require": { "composer-runtime-api": "^2.2", "ext-mbstring": "*", "php": "^8.1", - "symfony/console": "^6.2|^7.0" + "symfony/console": "^6.2|^7.0|^8.0" }, "conflict": { "illuminate/console": ">=10.17.0 <10.25.0", "laravel/framework": ">=10.17.0 <10.25.0" }, "require-dev": { - "illuminate/collections": "^10.0|^11.0|^12.0", + "illuminate/collections": "^10.0|^11.0|^12.0|^13.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." @@ -1547,38 +2170,37 @@ "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.5" + "source": "https://github.com/laravel/prompts/tree/v0.3.14" }, - "time": "2025-02-11T13:34:40+00:00" + "time": "2026-03-01T09:02:38+00:00" }, { "name": "laravel/sanctum", - "version": "v4.0.8", + "version": "v4.3.1", "source": { "type": "git", "url": "https://github.com/laravel/sanctum.git", - "reference": "ec1dd9ddb2ab370f79dfe724a101856e0963f43c" + "reference": "e3b85d6e36ad00e5db2d1dcc27c81ffdf15cbf76" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/sanctum/zipball/ec1dd9ddb2ab370f79dfe724a101856e0963f43c", - "reference": "ec1dd9ddb2ab370f79dfe724a101856e0963f43c", + "url": "https://api.github.com/repos/laravel/sanctum/zipball/e3b85d6e36ad00e5db2d1dcc27c81ffdf15cbf76", + "reference": "e3b85d6e36ad00e5db2d1dcc27c81ffdf15cbf76", "shasum": "" }, "require": { "ext-json": "*", - "illuminate/console": "^11.0|^12.0", - "illuminate/contracts": "^11.0|^12.0", - "illuminate/database": "^11.0|^12.0", - "illuminate/support": "^11.0|^12.0", + "illuminate/console": "^11.0|^12.0|^13.0", + "illuminate/contracts": "^11.0|^12.0|^13.0", + "illuminate/database": "^11.0|^12.0|^13.0", + "illuminate/support": "^11.0|^12.0|^13.0", "php": "^8.2", - "symfony/console": "^7.0" + "symfony/console": "^7.0|^8.0" }, "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|^11.0", + "phpstan/phpstan": "^1.10" }, "type": "library", "extra": { @@ -1613,31 +2235,31 @@ "issues": "https://github.com/laravel/sanctum/issues", "source": "https://github.com/laravel/sanctum" }, - "time": "2025-01-26T19:34:36+00:00" + "time": "2026-02-07T17:19:31+00:00" }, { "name": "laravel/serializable-closure", - "version": "v2.0.3", + "version": "v2.0.10", "source": { "type": "git", "url": "https://github.com/laravel/serializable-closure.git", - "reference": "f379c13663245f7aa4512a7869f62eb14095f23f" + "reference": "870fc81d2f879903dfc5b60bf8a0f94a1609e669" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/serializable-closure/zipball/f379c13663245f7aa4512a7869f62eb14095f23f", - "reference": "f379c13663245f7aa4512a7869f62eb14095f23f", + "url": "https://api.github.com/repos/laravel/serializable-closure/zipball/870fc81d2f879903dfc5b60bf8a0f94a1609e669", + "reference": "870fc81d2f879903dfc5b60bf8a0f94a1609e669", "shasum": "" }, "require": { "php": "^8.1" }, "require-dev": { - "illuminate/support": "^10.0|^11.0|^12.0", + "illuminate/support": "^10.0|^11.0|^12.0|^13.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" + "symfony/var-dumper": "^6.2.0|^7.0.0|^8.0.0" }, "type": "library", "extra": { @@ -1674,20 +2296,92 @@ "issues": "https://github.com/laravel/serializable-closure/issues", "source": "https://github.com/laravel/serializable-closure" }, - "time": "2025-02-11T15:03:05+00:00" + "time": "2026-02-20T19:59:49+00:00" }, { - "name": "laravel/tinker", - "version": "v2.10.1", + "name": "laravel/socialite", + "version": "v5.25.0", "source": { "type": "git", - "url": "https://github.com/laravel/tinker.git", - "reference": "22177cc71807d38f2810c6204d8f7183d88a57d3" + "url": "https://github.com/laravel/socialite.git", + "reference": "231f572e1a37c9ca1fb8085e9fb8608285beafb3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/tinker/zipball/22177cc71807d38f2810c6204d8f7183d88a57d3", - "reference": "22177cc71807d38f2810c6204d8f7183d88a57d3", + "url": "https://api.github.com/repos/laravel/socialite/zipball/231f572e1a37c9ca1fb8085e9fb8608285beafb3", + "reference": "231f572e1a37c9ca1fb8085e9fb8608285beafb3", + "shasum": "" + }, + "require": { + "ext-json": "*", + "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|^13.0", + "illuminate/http": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0|^13.0", + "illuminate/support": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0|^13.0", + "league/oauth1-client": "^1.11", + "php": "^7.2|^8.0", + "phpseclib/phpseclib": "^3.0" + }, + "require-dev": { + "mockery/mockery": "^1.0", + "orchestra/testbench": "^4.18|^5.20|^6.47|^7.55|^8.36|^9.15|^10.8|^11.0", + "phpstan/phpstan": "^1.12.23", + "phpunit/phpunit": "^8.0|^9.3|^10.4|^11.5|^12.0" + }, + "type": "library", + "extra": { + "laravel": { + "aliases": { + "Socialite": "Laravel\\Socialite\\Facades\\Socialite" + }, + "providers": [ + "Laravel\\Socialite\\SocialiteServiceProvider" + ] + }, + "branch-alias": { + "dev-master": "5.x-dev" + } + }, + "autoload": { + "psr-4": { + "Laravel\\Socialite\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "Laravel wrapper around OAuth 1 & OAuth 2 libraries.", + "homepage": "https://laravel.com", + "keywords": [ + "laravel", + "oauth" + ], + "support": { + "issues": "https://github.com/laravel/socialite/issues", + "source": "https://github.com/laravel/socialite" + }, + "time": "2026-02-27T13:56:35+00:00" + }, + { + "name": "laravel/tinker", + "version": "v2.11.1", + "source": { + "type": "git", + "url": "https://github.com/laravel/tinker.git", + "reference": "c9f80cc835649b5c1842898fb043f8cc098dd741" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/tinker/zipball/c9f80cc835649b5c1842898fb043f8cc098dd741", + "reference": "c9f80cc835649b5c1842898fb043f8cc098dd741", "shasum": "" }, "require": { @@ -1696,7 +2390,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", @@ -1738,22 +2432,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.1" }, - "time": "2025-01-27T14:24:01+00:00" + "time": "2026-02-06T14:12:35+00:00" }, { "name": "league/commonmark", - "version": "2.6.1", + "version": "2.8.1", "source": { "type": "git", "url": "https://github.com/thephpleague/commonmark.git", - "reference": "d990688c91cedfb69753ffc2512727ec646df2ad" + "reference": "84b1ca48347efdbe775426f108622a42735a6579" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/d990688c91cedfb69753ffc2512727ec646df2ad", - "reference": "d990688c91cedfb69753ffc2512727ec646df2ad", + "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/84b1ca48347efdbe775426f108622a42735a6579", + "reference": "84b1ca48347efdbe775426f108622a42735a6579", "shasum": "" }, "require": { @@ -1778,11 +2472,11 @@ "phpstan/phpstan": "^1.8.2", "phpunit/phpunit": "^9.5.21 || ^10.5.9 || ^11.0.0", "scrutinizer/ocular": "^1.8.1", - "symfony/finder": "^5.3 | ^6.0 | ^7.0", - "symfony/process": "^5.4 | ^6.0 | ^7.0", - "symfony/yaml": "^2.3 | ^3.0 | ^4.0 | ^5.0 | ^6.0 | ^7.0", + "symfony/finder": "^5.3 | ^6.0 | ^7.0 || ^8.0", + "symfony/process": "^5.4 | ^6.0 | ^7.0 || ^8.0", + "symfony/yaml": "^2.3 | ^3.0 | ^4.0 | ^5.0 | ^6.0 | ^7.0 || ^8.0", "unleashedtech/php-coding-standard": "^3.1.1", - "vimeo/psalm": "^4.24.0 || ^5.0.0" + "vimeo/psalm": "^4.24.0 || ^5.0.0 || ^6.0.0" }, "suggest": { "symfony/yaml": "v2.3+ required if using the Front Matter extension" @@ -1790,7 +2484,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "2.7-dev" + "dev-main": "2.9-dev" } }, "autoload": { @@ -1847,7 +2541,7 @@ "type": "tidelift" } ], - "time": "2024-12-29T14:10:59+00:00" + "time": "2026-03-05T21:37:03+00:00" }, { "name": "league/config", @@ -1933,16 +2627,16 @@ }, { "name": "league/flysystem", - "version": "3.29.1", + "version": "3.32.0", "source": { "type": "git", "url": "https://github.com/thephpleague/flysystem.git", - "reference": "edc1bb7c86fab0776c3287dbd19b5fa278347319" + "reference": "254b1595b16b22dbddaaef9ed6ca9fdac4956725" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/edc1bb7c86fab0776c3287dbd19b5fa278347319", - "reference": "edc1bb7c86fab0776c3287dbd19b5fa278347319", + "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/254b1595b16b22dbddaaef9ed6ca9fdac4956725", + "reference": "254b1595b16b22dbddaaef9ed6ca9fdac4956725", "shasum": "" }, "require": { @@ -1966,13 +2660,13 @@ "composer/semver": "^3.0", "ext-fileinfo": "*", "ext-ftp": "*", - "ext-mongodb": "^1.3", + "ext-mongodb": "^1.3|^2", "ext-zip": "*", "friendsofphp/php-cs-fixer": "^3.5", "google/cloud-storage": "^1.23", "guzzlehttp/psr7": "^2.6", "microsoft/azure-storage-blob": "^1.1", - "mongodb/mongodb": "^1.2", + "mongodb/mongodb": "^1.2|^2", "phpseclib/phpseclib": "^3.0.36", "phpstan/phpstan": "^1.10", "phpunit/phpunit": "^9.5.11|^10.0", @@ -2010,22 +2704,22 @@ ], "support": { "issues": "https://github.com/thephpleague/flysystem/issues", - "source": "https://github.com/thephpleague/flysystem/tree/3.29.1" + "source": "https://github.com/thephpleague/flysystem/tree/3.32.0" }, - "time": "2024-10-08T08:58:34+00:00" + "time": "2026-02-25T17:01:41+00:00" }, { "name": "league/flysystem-local", - "version": "3.29.0", + "version": "3.31.0", "source": { "type": "git", "url": "https://github.com/thephpleague/flysystem-local.git", - "reference": "e0e8d52ce4b2ed154148453d321e97c8e931bd27" + "reference": "2f669db18a4c20c755c2bb7d3a7b0b2340488079" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/flysystem-local/zipball/e0e8d52ce4b2ed154148453d321e97c8e931bd27", - "reference": "e0e8d52ce4b2ed154148453d321e97c8e931bd27", + "url": "https://api.github.com/repos/thephpleague/flysystem-local/zipball/2f669db18a4c20c755c2bb7d3a7b0b2340488079", + "reference": "2f669db18a4c20c755c2bb7d3a7b0b2340488079", "shasum": "" }, "require": { @@ -2059,9 +2753,9 @@ "local" ], "support": { - "source": "https://github.com/thephpleague/flysystem-local/tree/3.29.0" + "source": "https://github.com/thephpleague/flysystem-local/tree/3.31.0" }, - "time": "2024-08-09T21:24:39+00:00" + "time": "2026-01-23T15:30:45+00:00" }, { "name": "league/mime-type-detection", @@ -2120,34 +2814,171 @@ "time": "2024-09-21T08:32:55+00:00" }, { - "name": "league/uri", - "version": "7.5.1", + "name": "league/oauth1-client", + "version": "v1.11.0", "source": { "type": "git", - "url": "https://github.com/thephpleague/uri.git", - "reference": "81fb5145d2644324614cc532b28efd0215bda430" + "url": "https://github.com/thephpleague/oauth1-client.git", + "reference": "f9c94b088837eb1aae1ad7c4f23eb65cc6993055" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/uri/zipball/81fb5145d2644324614cc532b28efd0215bda430", - "reference": "81fb5145d2644324614cc532b28efd0215bda430", + "url": "https://api.github.com/repos/thephpleague/oauth1-client/zipball/f9c94b088837eb1aae1ad7c4f23eb65cc6993055", + "reference": "f9c94b088837eb1aae1ad7c4f23eb65cc6993055", "shasum": "" }, "require": { - "league/uri-interfaces": "^7.5", - "php": "^8.1" + "ext-json": "*", + "ext-openssl": "*", + "guzzlehttp/guzzle": "^6.0|^7.0", + "guzzlehttp/psr7": "^1.7|^2.0", + "php": ">=7.1||>=8.0" + }, + "require-dev": { + "ext-simplexml": "*", + "friendsofphp/php-cs-fixer": "^2.17", + "mockery/mockery": "^1.3.3", + "phpstan/phpstan": "^0.12.42", + "phpunit/phpunit": "^7.5||9.5" + }, + "suggest": { + "ext-simplexml": "For decoding XML-based responses." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev", + "dev-develop": "2.0-dev" + } + }, + "autoload": { + "psr-4": { + "League\\OAuth1\\Client\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ben Corlett", + "email": "bencorlett@me.com", + "homepage": "http://www.webcomm.com.au", + "role": "Developer" + } + ], + "description": "OAuth 1.0 Client Library", + "keywords": [ + "Authentication", + "SSO", + "authorization", + "bitbucket", + "identity", + "idp", + "oauth", + "oauth1", + "single sign on", + "trello", + "tumblr", + "twitter" + ], + "support": { + "issues": "https://github.com/thephpleague/oauth1-client/issues", + "source": "https://github.com/thephpleague/oauth1-client/tree/v1.11.0" + }, + "time": "2024-12-10T19:59:05+00:00" + }, + { + "name": "league/pipeline", + "version": "1.1.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/pipeline.git", + "reference": "9069ddfdbd5582f8a563e00cffdbeffb9a0acd01" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/pipeline/zipball/9069ddfdbd5582f8a563e00cffdbeffb9a0acd01", + "reference": "9069ddfdbd5582f8a563e00cffdbeffb9a0acd01", + "shasum": "" + }, + "require": { + "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.8.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/uri.git", + "reference": "4436c6ec8d458e4244448b069cc572d088230b76" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/uri/zipball/4436c6ec8d458e4244448b069cc572d088230b76", + "reference": "4436c6ec8d458e4244448b069cc572d088230b76", + "shasum": "" + }, + "require": { + "league/uri-interfaces": "^7.8", + "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", - "jeremykendall/php-domain-parser": "to resolve Public Suffix and Top Level Domain", - "league/uri-components": "Needed to easily manipulate URI objects components", + "ext-uri": "to use the PHP native URI class", + "jeremykendall/php-domain-parser": "to further parse the URI host and resolve its Public Suffix and Top Level Domain", + "league/uri-components": "to provide additional tools to manipulate URI objects components", + "league/uri-polyfill": "to backport the PHP URI extension for older versions of PHP", "php-64bit": "to improve IPV4 host parsing", + "rowbot/url": "to handle URLs using the WHATWG URL Living Standard specification", "symfony/polyfill-intl-idn": "to handle IDN host via the Symfony polyfill if ext-intl is not present" }, "type": "library", @@ -2175,6 +3006,7 @@ "description": "URI manipulation library", "homepage": "https://uri.thephpleague.com", "keywords": [ + "URN", "data-uri", "file-uri", "ftp", @@ -2187,9 +3019,11 @@ "psr-7", "query-string", "querystring", + "rfc2141", "rfc3986", "rfc3987", "rfc6570", + "rfc8141", "uri", "uri-template", "url", @@ -2199,7 +3033,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.8.0" }, "funding": [ { @@ -2207,26 +3041,25 @@ "type": "github" } ], - "time": "2024-12-08T08:40:02+00:00" + "time": "2026-01-14T17:24:56+00:00" }, { "name": "league/uri-interfaces", - "version": "7.5.0", + "version": "7.8.0", "source": { "type": "git", "url": "https://github.com/thephpleague/uri-interfaces.git", - "reference": "08cfc6c4f3d811584fb09c37e2849e6a7f9b0742" + "reference": "c5c5cd056110fc8afaba29fa6b72a43ced42acd4" }, "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/c5c5cd056110fc8afaba29fa6b72a43ced42acd4", + "reference": "c5c5cd056110fc8afaba29fa6b72a43ced42acd4", "shasum": "" }, "require": { "ext-filter": "*", "php": "^8.1", - "psr/http-factory": "^1", "psr/http-message": "^1.1 || ^2.0" }, "suggest": { @@ -2234,6 +3067,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 URLs using the WHATWG URL Living Standard specification", "symfony/polyfill-intl-idn": "to handle IDN host via the Symfony polyfill if ext-intl is not present" }, "type": "library", @@ -2258,7 +3092,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", @@ -2283,7 +3117,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.8.0" }, "funding": [ { @@ -2291,30 +3125,33 @@ "type": "github" } ], - "time": "2024-12-08T08:18:47+00:00" + "time": "2026-01-15T06:54:53+00:00" }, { "name": "livewire/flux", - "version": "v2.0.6", + "version": "v2.13.0", "source": { "type": "git", "url": "https://github.com/livewire/flux.git", - "reference": "afd046a72c7a10e6ff56f3e9c16eb390c3db0cc5" + "reference": "741be2d4526e90b97c7a59e079a2f27ecdce2461" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/livewire/flux/zipball/afd046a72c7a10e6ff56f3e9c16eb390c3db0cc5", - "reference": "afd046a72c7a10e6ff56f3e9c16eb390c3db0cc5", + "url": "https://api.github.com/repos/livewire/flux/zipball/741be2d4526e90b97c7a59e079a2f27ecdce2461", + "reference": "741be2d4526e90b97c7a59e079a2f27ecdce2461", "shasum": "" }, "require": { - "illuminate/console": "^10.0|^11.0|^12.0", - "illuminate/support": "^10.0|^11.0|^12.0", - "illuminate/view": "^10.0|^11.0|^12.0", + "illuminate/console": "^10.0|^11.0|^12.0|^13.0", + "illuminate/support": "^10.0|^11.0|^12.0|^13.0", + "illuminate/view": "^10.0|^11.0|^12.0|^13.0", "laravel/prompts": "^0.1|^0.2|^0.3", - "livewire/livewire": "^3.5.19", + "livewire/livewire": "^3.7.4|^4.0", "php": "^8.1", - "symfony/console": "^6.0|^7.0" + "symfony/console": "^6.0|^7.0|^8.0" + }, + "conflict": { + "livewire/blaze": "<1.0.0-beta.2" }, "type": "library", "extra": { @@ -2352,42 +3189,42 @@ ], "support": { "issues": "https://github.com/livewire/flux/issues", - "source": "https://github.com/livewire/flux/tree/v2.0.6" + "source": "https://github.com/livewire/flux/tree/v2.13.0" }, - "time": "2025-03-12T20:53:07+00:00" + "time": "2026-03-03T03:32:35+00:00" }, { "name": "livewire/livewire", - "version": "v3.6.2", + "version": "v4.2.1", "source": { "type": "git", "url": "https://github.com/livewire/livewire.git", - "reference": "8f8914731f5eb43b6bb145d87c8d5a9edfc89313" + "reference": "93e972fa42c1b34fff1550093ab94f778d81ea5a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/livewire/livewire/zipball/8f8914731f5eb43b6bb145d87c8d5a9edfc89313", - "reference": "8f8914731f5eb43b6bb145d87c8d5a9edfc89313", + "url": "https://api.github.com/repos/livewire/livewire/zipball/93e972fa42c1b34fff1550093ab94f778d81ea5a", + "reference": "93e972fa42c1b34fff1550093ab94f778d81ea5a", "shasum": "" }, "require": { - "illuminate/database": "^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", + "illuminate/database": "^10.0|^11.0|^12.0|^13.0", + "illuminate/routing": "^10.0|^11.0|^12.0|^13.0", + "illuminate/support": "^10.0|^11.0|^12.0|^13.0", + "illuminate/validation": "^10.0|^11.0|^12.0|^13.0", "laravel/prompts": "^0.1.24|^0.2|^0.3", "league/mime-type-detection": "^1.9", "php": "^8.1", - "symfony/console": "^6.0|^7.0", - "symfony/http-kernel": "^6.2|^7.0" + "symfony/console": "^6.0|^7.0|^8.0", + "symfony/http-kernel": "^6.2|^7.0|^8.0" }, "require-dev": { "calebporzio/sushi": "^2.1", - "laravel/framework": "^10.15.0|^11.0|^12.0", + "laravel/framework": "^10.15.0|^11.0|^12.0|^13.0", "mockery/mockery": "^1.3.1", - "orchestra/testbench": "^8.21.0|^9.0|^10.0", - "orchestra/testbench-dusk": "^8.24|^9.1|^10.0", - "phpunit/phpunit": "^10.4|^11.5", + "orchestra/testbench": "^8.21.0|^9.0|^10.0|^11.0", + "orchestra/testbench-dusk": "^8.24|^9.1|^10.0|^11.0", + "phpunit/phpunit": "^10.4|^11.5|^12.5", "psy/psysh": "^0.11.22|^0.12" }, "type": "library", @@ -2422,7 +3259,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.2" + "source": "https://github.com/livewire/livewire/tree/v4.2.1" }, "funding": [ { @@ -2430,51 +3267,45 @@ "type": "github" } ], - "time": "2025-03-12T20:24:15+00:00" + "time": "2026-02-28T00:01:19+00:00" }, { - "name": "livewire/volt", - "version": "v1.7.0", + "name": "maennchen/zipstream-php", + "version": "3.2.1", "source": { "type": "git", - "url": "https://github.com/livewire/volt.git", - "reference": "94091094aa745c8636f9c7bed1e2da2d2a3f32b3" + "url": "https://github.com/maennchen/ZipStream-PHP.git", + "reference": "682f1098a8fddbaf43edac2306a691c7ad508ec5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/livewire/volt/zipball/94091094aa745c8636f9c7bed1e2da2d2a3f32b3", - "reference": "94091094aa745c8636f9c7bed1e2da2d2a3f32b3", + "url": "https://api.github.com/repos/maennchen/ZipStream-PHP/zipball/682f1098a8fddbaf43edac2306a691c7ad508ec5", + "reference": "682f1098a8fddbaf43edac2306a691c7ad508ec5", "shasum": "" }, "require": { - "laravel/framework": "^10.38.2|^11.0|^12.0", - "livewire/livewire": "^3.6.1", - "php": "^8.1" + "ext-mbstring": "*", + "ext-zlib": "*", + "php-64bit": "^8.3" }, "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", - "phpstan/phpstan": "^1.10" + "brianium/paratest": "^7.7", + "ext-zip": "*", + "friendsofphp/php-cs-fixer": "^3.86", + "guzzlehttp/guzzle": "^7.5", + "mikey179/vfsstream": "^1.6", + "php-coveralls/php-coveralls": "^2.5", + "phpunit/phpunit": "^12.0", + "vimeo/psalm": "^6.0" + }, + "suggest": { + "guzzlehttp/psr7": "^2.4", + "psr/http-message": "^2.0" }, "type": "library", - "extra": { - "laravel": { - "providers": [ - "Livewire\\Volt\\VoltServiceProvider" - ] - }, - "branch-alias": { - "dev-master": "1.x-dev" - } - }, "autoload": { - "files": [ - "functions.php" - ], "psr-4": { - "Livewire\\Volt\\": "src/" + "ZipStream\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -2483,39 +3314,51 @@ ], "authors": [ { - "name": "Taylor Otwell", - "email": "taylor@laravel.com" + "name": "Paul Duncan", + "email": "pabs@pablotron.org" }, { - "name": "Nuno Maduro", - "email": "nuno@laravel.com" + "name": "Jonatan MΓ€nnchen", + "email": "jonatan@maennchen.ch" + }, + { + "name": "Jesse Donat", + "email": "donatj@gmail.com" + }, + { + "name": "AndrΓ‘s KolesΓ‘r", + "email": "kolesar@kolesar.hu" } ], - "description": "An elegantly crafted functional API for Laravel Livewire.", - "homepage": "https://github.com/livewire/volt", + "description": "ZipStream is a library for dynamically streaming dynamic zip files from PHP without writing to the disk at all on the server.", "keywords": [ - "laravel", - "livewire", - "volt" + "stream", + "zip" ], "support": { - "issues": "https://github.com/livewire/volt/issues", - "source": "https://github.com/livewire/volt" + "issues": "https://github.com/maennchen/ZipStream-PHP/issues", + "source": "https://github.com/maennchen/ZipStream-PHP/tree/3.2.1" }, - "time": "2025-03-05T15:20:55+00:00" + "funding": [ + { + "url": "https://github.com/maennchen", + "type": "github" + } + ], + "time": "2025-12-10T09:58:31+00:00" }, { "name": "monolog/monolog", - "version": "3.8.1", + "version": "3.10.0", "source": { "type": "git", "url": "https://github.com/Seldaek/monolog.git", - "reference": "aef6ee73a77a66e404dd6540934a9ef1b3c855b4" + "reference": "b321dd6749f0bf7189444158a3ce785cc16d69b0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Seldaek/monolog/zipball/aef6ee73a77a66e404dd6540934a9ef1b3c855b4", - "reference": "aef6ee73a77a66e404dd6540934a9ef1b3c855b4", + "url": "https://api.github.com/repos/Seldaek/monolog/zipball/b321dd6749f0bf7189444158a3ce785cc16d69b0", + "reference": "b321dd6749f0bf7189444158a3ce785cc16d69b0", "shasum": "" }, "require": { @@ -2533,7 +3376,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", @@ -2593,7 +3436,7 @@ ], "support": { "issues": "https://github.com/Seldaek/monolog/issues", - "source": "https://github.com/Seldaek/monolog/tree/3.8.1" + "source": "https://github.com/Seldaek/monolog/tree/3.10.0" }, "funding": [ { @@ -2605,20 +3448,86 @@ "type": "tidelift" } ], - "time": "2024-12-05T17:15:07+00:00" + "time": "2026-01-02T08:56:05+00:00" }, { - "name": "nesbot/carbon", - "version": "3.8.6", + "name": "mtdowling/jmespath.php", + "version": "2.8.0", "source": { "type": "git", - "url": "https://github.com/CarbonPHP/carbon.git", - "reference": "ff2f20cf83bd4d503720632ce8a426dc747bf7fd" + "url": "https://github.com/jmespath/jmespath.php.git", + "reference": "a2a865e05d5f420b50cc2f85bb78d565db12a6bc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/CarbonPHP/carbon/zipball/ff2f20cf83bd4d503720632ce8a426dc747bf7fd", - "reference": "ff2f20cf83bd4d503720632ce8a426dc747bf7fd", + "url": "https://api.github.com/repos/jmespath/jmespath.php/zipball/a2a865e05d5f420b50cc2f85bb78d565db12a6bc", + "reference": "a2a865e05d5f420b50cc2f85bb78d565db12a6bc", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0", + "symfony/polyfill-mbstring": "^1.17" + }, + "require-dev": { + "composer/xdebug-handler": "^3.0.3", + "phpunit/phpunit": "^8.5.33" + }, + "bin": [ + "bin/jp.php" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.8-dev" + } + }, + "autoload": { + "files": [ + "src/JmesPath.php" + ], + "psr-4": { + "JmesPath\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + } + ], + "description": "Declaratively specify how to extract elements from a JSON document", + "keywords": [ + "json", + "jsonpath" + ], + "support": { + "issues": "https://github.com/jmespath/jmespath.php/issues", + "source": "https://github.com/jmespath/jmespath.php/tree/2.8.0" + }, + "time": "2024-09-04T18:46:31+00:00" + }, + { + "name": "nesbot/carbon", + "version": "3.11.3", + "source": { + "type": "git", + "url": "https://github.com/CarbonPHP/carbon.git", + "reference": "6a7e652845bb018c668220c2a545aded8594fbbf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/CarbonPHP/carbon/zipball/6a7e652845bb018c668220c2a545aded8594fbbf", + "reference": "6a7e652845bb018c668220c2a545aded8594fbbf", "shasum": "" }, "require": { @@ -2626,9 +3535,9 @@ "ext-json": "*", "php": "^8.1", "psr/clock": "^1.0", - "symfony/clock": "^6.3 || ^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" @@ -2636,14 +3545,13 @@ "require-dev": { "doctrine/dbal": "^3.6.3 || ^4.0", "doctrine/orm": "^2.15.2 || ^3.0", - "friendsofphp/php-cs-fixer": "^3.57.2", + "friendsofphp/php-cs-fixer": "^v3.87.1", "kylekatarnls/multi-tester": "^2.5.3", - "ondrejmirtes/better-reflection": "^6.25.0.4", "phpmd/phpmd": "^2.15.0", - "phpstan/extension-installer": "^1.3.1", - "phpstan/phpstan": "^1.11.2", - "phpunit/phpunit": "^10.5.20", - "squizlabs/php_codesniffer": "^3.9.0" + "phpstan/extension-installer": "^1.4.3", + "phpstan/phpstan": "^2.1.22", + "phpunit/phpunit": "^10.5.53", + "squizlabs/php_codesniffer": "^3.13.4 || ^4.0.0" }, "bin": [ "bin/carbon" @@ -2686,14 +3594,14 @@ } ], "description": "An API extension for DateTime that supports 281 different languages.", - "homepage": "https://carbon.nesbot.com", + "homepage": "https://carbonphp.github.io/carbon/", "keywords": [ "date", "datetime", "time" ], "support": { - "docs": "https://carbon.nesbot.com/docs", + "docs": "https://carbonphp.github.io/carbon/guide/getting-started/introduction.html", "issues": "https://github.com/CarbonPHP/carbon/issues", "source": "https://github.com/CarbonPHP/carbon" }, @@ -2711,29 +3619,31 @@ "type": "tidelift" } ], - "time": "2025-02-20T17:33:38+00:00" + "time": "2026-03-11T17:23:39+00:00" }, { "name": "nette/schema", - "version": "v1.3.2", + "version": "v1.3.5", "source": { "type": "git", "url": "https://github.com/nette/schema.git", - "reference": "da801d52f0354f70a638673c4a0f04e16529431d" + "reference": "f0ab1a3cda782dbc5da270d28545236aa80c4002" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nette/schema/zipball/da801d52f0354f70a638673c4a0f04e16529431d", - "reference": "da801d52f0354f70a638673c4a0f04e16529431d", + "url": "https://api.github.com/repos/nette/schema/zipball/f0ab1a3cda782dbc5da270d28545236aa80c4002", + "reference": "f0ab1a3cda782dbc5da270d28545236aa80c4002", "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", + "nette/phpstan-rules": "^1.0", + "nette/tester": "^2.6", + "phpstan/extension-installer": "^1.4@stable", + "phpstan/phpstan": "^2.1.39@stable", "tracy/tracy": "^2.8" }, "type": "library", @@ -2743,6 +3653,9 @@ } }, "autoload": { + "psr-4": { + "Nette\\": "src" + }, "classmap": [ "src/" ] @@ -2771,35 +3684,37 @@ ], "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.5" }, - "time": "2024-10-06T23:10:23+00:00" + "time": "2026-02-23T03:47:12+00:00" }, { "name": "nette/utils", - "version": "v4.0.5", + "version": "v4.1.3", "source": { "type": "git", "url": "https://github.com/nette/utils.git", - "reference": "736c567e257dbe0fcf6ce81b4d6dbe05c6899f96" + "reference": "bb3ea637e3d131d72acc033cfc2746ee893349fe" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nette/utils/zipball/736c567e257dbe0fcf6ce81b4d6dbe05c6899f96", - "reference": "736c567e257dbe0fcf6ce81b4d6dbe05c6899f96", + "url": "https://api.github.com/repos/nette/utils/zipball/bb3ea637e3d131d72acc033cfc2746ee893349fe", + "reference": "bb3ea637e3d131d72acc033cfc2746ee893349fe", "shasum": "" }, "require": { - "php": "8.0 - 8.4" + "php": "8.2 - 8.5" }, "conflict": { "nette/finder": "<3", "nette/schema": "<1.2.2" }, "require-dev": { - "jetbrains/phpstorm-attributes": "dev-master", + "jetbrains/phpstorm-attributes": "^1.2", + "nette/phpstan-rules": "^1.0", "nette/tester": "^2.5", - "phpstan/phpstan": "^1.0", + "phpstan/extension-installer": "^1.4@stable", + "phpstan/phpstan": "^2.1@stable", "tracy/tracy": "^2.9" }, "suggest": { @@ -2813,10 +3728,13 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "4.0-dev" + "dev-master": "4.1-dev" } }, "autoload": { + "psr-4": { + "Nette\\": "src" + }, "classmap": [ "src/" ] @@ -2857,22 +3775,22 @@ ], "support": { "issues": "https://github.com/nette/utils/issues", - "source": "https://github.com/nette/utils/tree/v4.0.5" + "source": "https://github.com/nette/utils/tree/v4.1.3" }, - "time": "2024-08-07T15:39:19+00:00" + "time": "2026-02-13T03:05:33+00:00" }, { "name": "nikic/php-parser", - "version": "v5.4.0", + "version": "v5.7.0", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "447a020a1f875a434d62f2a401f53b82a396e494" + "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/447a020a1f875a434d62f2a401f53b82a396e494", - "reference": "447a020a1f875a434d62f2a401f53b82a396e494", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/dca41cd15c2ac9d055ad70dbfd011130757d1f82", + "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82", "shasum": "" }, "require": { @@ -2891,7 +3809,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "5.0-dev" + "dev-master": "5.x-dev" } }, "autoload": { @@ -2915,37 +3833,37 @@ ], "support": { "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v5.4.0" + "source": "https://github.com/nikic/PHP-Parser/tree/v5.7.0" }, - "time": "2024-12-30T11:07:19+00:00" + "time": "2025-12-06T11:56:16+00:00" }, { "name": "nunomaduro/termwind", - "version": "v2.3.0", + "version": "v2.4.0", "source": { "type": "git", "url": "https://github.com/nunomaduro/termwind.git", - "reference": "52915afe6a1044e8b9cee1bcff836fb63acf9cda" + "reference": "712a31b768f5daea284c2169a7d227031001b9a8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nunomaduro/termwind/zipball/52915afe6a1044e8b9cee1bcff836fb63acf9cda", - "reference": "52915afe6a1044e8b9cee1bcff836fb63acf9cda", + "url": "https://api.github.com/repos/nunomaduro/termwind/zipball/712a31b768f5daea284c2169a7d227031001b9a8", + "reference": "712a31b768f5daea284c2169a7d227031001b9a8", "shasum": "" }, "require": { "ext-mbstring": "*", "php": "^8.2", - "symfony/console": "^7.1.8" + "symfony/console": "^7.4.4 || ^8.0.4" }, "require-dev": { - "illuminate/console": "^11.33.2", - "laravel/pint": "^1.18.2", + "illuminate/console": "^11.47.0", + "laravel/pint": "^1.27.1", "mockery/mockery": "^1.6.12", - "pestphp/pest": "^2.36.0", - "phpstan/phpstan": "^1.12.11", - "phpstan/phpstan-strict-rules": "^1.6.1", - "symfony/var-dumper": "^7.1.8", + "pestphp/pest": "^2.36.0 || ^3.8.4 || ^4.3.2", + "phpstan/phpstan": "^1.12.32", + "phpstan/phpstan-strict-rules": "^1.6.2", + "symfony/var-dumper": "^7.3.5 || ^8.0.4", "thecodingmachine/phpstan-strict-rules": "^1.0.0" }, "type": "library", @@ -2977,7 +3895,7 @@ "email": "enunomaduro@gmail.com" } ], - "description": "Its like Tailwind CSS, but for the console.", + "description": "It's like Tailwind CSS, but for the console.", "keywords": [ "cli", "console", @@ -2988,7 +3906,7 @@ ], "support": { "issues": "https://github.com/nunomaduro/termwind/issues", - "source": "https://github.com/nunomaduro/termwind/tree/v2.3.0" + "source": "https://github.com/nunomaduro/termwind/tree/v2.4.0" }, "funding": [ { @@ -3004,20 +3922,301 @@ "type": "github" } ], - "time": "2024-11-21T10:39:51+00:00" + "time": "2026-02-16T23:10:27+00:00" }, { - "name": "phpoption/phpoption", - "version": "1.9.3", + "name": "om/icalparser", + "version": "v4.0.0", "source": { "type": "git", - "url": "https://github.com/schmittjoh/php-option.git", - "reference": "e3fac8b24f56113f7cb96af14958c0dd16330f54" + "url": "https://github.com/OzzyCzech/icalparser.git", + "reference": "3e60e2edf0bdfed6c81b69dccf0b7f2852cd87a6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/schmittjoh/php-option/zipball/e3fac8b24f56113f7cb96af14958c0dd16330f54", - "reference": "e3fac8b24f56113f7cb96af14958c0dd16330f54", + "url": "https://api.github.com/repos/OzzyCzech/icalparser/zipball/3e60e2edf0bdfed6c81b69dccf0b7f2852cd87a6", + "reference": "3e60e2edf0bdfed6c81b69dccf0b7f2852cd87a6", + "shasum": "" + }, + "require": { + "php": "^8.4" + }, + "require-dev": { + "nette/tester": "^2.6.0" + }, + "suggest": { + "ext-dom": "for timezone tool" + }, + "type": "library", + "autoload": { + "psr-4": { + "om\\": "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/v4.0.0" + }, + "time": "2026-01-29T16:45:33+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": { + "infection/infection": "^0", + "nikic/php-fuzzer": "^0", + "phpunit/phpunit": "^9|^10|^11", + "vimeo/psalm": "^4|^5|^6" + }, + "type": "library", + "autoload": { + "psr-4": { + "ParagonIE\\ConstantTime\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Paragon Initiative Enterprises", + "email": "security@paragonie.com", + "homepage": "https://paragonie.com", + "role": "Maintainer" + }, + { + "name": "Steve 'Sc00bz' Thomas", + "email": "steve@tobtu.com", + "homepage": "https://www.tobtu.com", + "role": "Original Developer" + } + ], + "description": "Constant-time Implementations of RFC 4648 Encoding (Base-64, Base-32, Base-16)", + "keywords": [ + "base16", + "base32", + "base32_decode", + "base32_encode", + "base64", + "base64_decode", + "base64_encode", + "bin2hex", + "encoding", + "hex", + "hex2bin", + "rfc4648" + ], + "support": { + "email": "info@paragonie.com", + "issues": "https://github.com/paragonie/constant_time_encoding/issues", + "source": "https://github.com/paragonie/constant_time_encoding" + }, + "time": "2025-09-24T15:06:41+00:00" + }, + { + "name": "paragonie/random_compat", + "version": "v9.99.100", + "source": { + "type": "git", + "url": "https://github.com/paragonie/random_compat.git", + "reference": "996434e5492cb4c3edcb9168db6fbb1359ef965a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/paragonie/random_compat/zipball/996434e5492cb4c3edcb9168db6fbb1359ef965a", + "reference": "996434e5492cb4c3edcb9168db6fbb1359ef965a", + "shasum": "" + }, + "require": { + "php": ">= 7" + }, + "require-dev": { + "phpunit/phpunit": "4.*|5.*", + "vimeo/psalm": "^1" + }, + "suggest": { + "ext-libsodium": "Provides a modern crypto API that can be used to generate random bytes." + }, + "type": "library", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Paragon Initiative Enterprises", + "email": "security@paragonie.com", + "homepage": "https://paragonie.com" + } + ], + "description": "PHP 5.x polyfill for random_bytes() and random_int() from PHP 7", + "keywords": [ + "csprng", + "polyfill", + "pseudorandom", + "random" + ], + "support": { + "email": "info@paragonie.com", + "issues": "https://github.com/paragonie/random_compat/issues", + "source": "https://github.com/paragonie/random_compat" + }, + "time": "2020-10-15T08:29:30+00:00" + }, + { + "name": "phpdocumentor/reflection-common", + "version": "2.2.0", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/ReflectionCommon.git", + "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/1d01c49d4ed62f25aa84a747ad35d5a16924662b", + "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-2.x": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jaap van Otterdijk", + "email": "opensource@ijaap.nl" + } + ], + "description": "Common reflection classes used by phpdocumentor to reflect the code structure", + "homepage": "http://www.phpdoc.org", + "keywords": [ + "FQSEN", + "phpDocumentor", + "phpdoc", + "reflection", + "static analysis" + ], + "support": { + "issues": "https://github.com/phpDocumentor/ReflectionCommon/issues", + "source": "https://github.com/phpDocumentor/ReflectionCommon/tree/2.x" + }, + "time": "2020-06-27T09:03:43+00:00" + }, + { + "name": "phpdocumentor/type-resolver", + "version": "1.12.0", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/TypeResolver.git", + "reference": "92a98ada2b93d9b201a613cb5a33584dde25f195" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/92a98ada2b93d9b201a613cb5a33584dde25f195", + "reference": "92a98ada2b93d9b201a613cb5a33584dde25f195", + "shasum": "" + }, + "require": { + "doctrine/deprecations": "^1.0", + "php": "^7.3 || ^8.0", + "phpdocumentor/reflection-common": "^2.0", + "phpstan/phpdoc-parser": "^1.18|^2.0" + }, + "require-dev": { + "ext-tokenizer": "*", + "phpbench/phpbench": "^1.2", + "phpstan/extension-installer": "^1.1", + "phpstan/phpstan": "^1.8", + "phpstan/phpstan-phpunit": "^1.1", + "phpunit/phpunit": "^9.5", + "rector/rector": "^0.13.9", + "vimeo/psalm": "^4.25" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-1.x": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mike van Riel", + "email": "me@mikevanriel.com" + } + ], + "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.12.0" + }, + "time": "2025-11-21T15:09:14+00:00" + }, + { + "name": "phpoption/phpoption", + "version": "1.9.5", + "source": { + "type": "git", + "url": "https://github.com/schmittjoh/php-option.git", + "reference": "75365b91986c2405cf5e1e012c5595cd487a98be" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/schmittjoh/php-option/zipball/75365b91986c2405cf5e1e012c5595cd487a98be", + "reference": "75365b91986c2405cf5e1e012c5595cd487a98be", "shasum": "" }, "require": { @@ -3025,7 +4224,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": { @@ -3067,7 +4266,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": [ { @@ -3079,7 +4278,216 @@ "type": "tidelift" } ], - "time": "2024-07-20T21:41:07+00:00" + "time": "2025-12-27T19:41:33+00:00" + }, + { + "name": "phpseclib/phpseclib", + "version": "3.0.49", + "source": { + "type": "git", + "url": "https://github.com/phpseclib/phpseclib.git", + "reference": "6233a1e12584754e6b5daa69fe1289b47775c1b9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/6233a1e12584754e6b5daa69fe1289b47775c1b9", + "reference": "6233a1e12584754e6b5daa69fe1289b47775c1b9", + "shasum": "" + }, + "require": { + "paragonie/constant_time_encoding": "^1|^2|^3", + "paragonie/random_compat": "^1.4|^2.0|^9.99.99", + "php": ">=5.6.1" + }, + "require-dev": { + "phpunit/phpunit": "*" + }, + "suggest": { + "ext-dom": "Install the DOM extension to load XML formatted public keys.", + "ext-gmp": "Install the GMP (GNU Multiple Precision) extension in order to speed up arbitrary precision integer arithmetic operations.", + "ext-libsodium": "SSH2/SFTP can make use of some algorithms provided by the libsodium-php extension.", + "ext-mcrypt": "Install the Mcrypt extension in order to speed up a few other cryptographic operations.", + "ext-openssl": "Install the OpenSSL extension in order to speed up a wide variety of cryptographic operations." + }, + "type": "library", + "autoload": { + "files": [ + "phpseclib/bootstrap.php" + ], + "psr-4": { + "phpseclib3\\": "phpseclib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jim Wigginton", + "email": "terrafrost@php.net", + "role": "Lead Developer" + }, + { + "name": "Patrick Monnerat", + "email": "pm@datasphere.ch", + "role": "Developer" + }, + { + "name": "Andreas Fischer", + "email": "bantu@phpbb.com", + "role": "Developer" + }, + { + "name": "Hans-JΓΌrgen Petrich", + "email": "petrich@tronic-media.com", + "role": "Developer" + }, + { + "name": "Graham Campbell", + "email": "graham@alt-three.com", + "role": "Developer" + } + ], + "description": "PHP Secure Communications Library - Pure-PHP implementations of RSA, AES, SSH2, SFTP, X.509 etc.", + "homepage": "http://phpseclib.sourceforge.net", + "keywords": [ + "BigInteger", + "aes", + "asn.1", + "asn1", + "blowfish", + "crypto", + "cryptography", + "encryption", + "rsa", + "security", + "sftp", + "signature", + "signing", + "ssh", + "twofish", + "x.509", + "x509" + ], + "support": { + "issues": "https://github.com/phpseclib/phpseclib/issues", + "source": "https://github.com/phpseclib/phpseclib/tree/3.0.49" + }, + "funding": [ + { + "url": "https://github.com/terrafrost", + "type": "github" + }, + { + "url": "https://www.patreon.com/phpseclib", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpseclib/phpseclib", + "type": "tidelift" + } + ], + "time": "2026-01-27T09:17:28+00:00" + }, + { + "name": "phpstan/phpdoc-parser", + "version": "2.3.2", + "source": { + "type": "git", + "url": "https://github.com/phpstan/phpdoc-parser.git", + "reference": "a004701b11273a26cd7955a61d67a7f1e525a45a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/a004701b11273a26cd7955a61d67a7f1e525a45a", + "reference": "a004701b11273a26cd7955a61d67a7f1e525a45a", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "doctrine/annotations": "^2.0", + "nikic/php-parser": "^5.3.0", + "php-parallel-lint/php-parallel-lint": "^1.2", + "phpstan/extension-installer": "^1.0", + "phpstan/phpstan": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpstan/phpstan-strict-rules": "^2.0", + "phpunit/phpunit": "^9.6", + "symfony/process": "^5.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "PHPStan\\PhpDocParser\\": [ + "src/" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "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.3.2" + }, + "time": "2026-01-25T14:56:51+00:00" + }, + { + "name": "pragmarx/google2fa", + "version": "v9.0.0", + "source": { + "type": "git", + "url": "https://github.com/antonioribeiro/google2fa.git", + "reference": "e6bc62dd6ae83acc475f57912e27466019a1f2cf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/antonioribeiro/google2fa/zipball/e6bc62dd6ae83acc475f57912e27466019a1f2cf", + "reference": "e6bc62dd6ae83acc475f57912e27466019a1f2cf", + "shasum": "" + }, + "require": { + "paragonie/constant_time_encoding": "^1.0|^2.0|^3.0", + "php": "^7.1|^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^1.9", + "phpunit/phpunit": "^7.5.15|^8.5|^9.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "PragmaRX\\Google2FA\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Antonio Carlos Ribeiro", + "email": "acr@antoniocarlosribeiro.com", + "role": "Creator & Designer" + } + ], + "description": "A One Time Password Authentication package, compatible with Google Authenticator.", + "keywords": [ + "2fa", + "Authentication", + "Two Factor Authentication", + "google2fa" + ], + "support": { + "issues": "https://github.com/antonioribeiro/google2fa/issues", + "source": "https://github.com/antonioribeiro/google2fa/tree/v9.0.0" + }, + "time": "2025-09-19T22:51:08+00:00" }, { "name": "psr/clock", @@ -3495,16 +4903,16 @@ }, { "name": "psy/psysh", - "version": "v0.12.7", + "version": "v0.12.21", "source": { "type": "git", "url": "https://github.com/bobthecow/psysh.git", - "reference": "d73fa3c74918ef4522bb8a3bf9cab39161c4b57c" + "reference": "4821fab5b7cd8c49a673a9fd5754dc9162bb9e97" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/bobthecow/psysh/zipball/d73fa3c74918ef4522bb8a3bf9cab39161c4b57c", - "reference": "d73fa3c74918ef4522bb8a3bf9cab39161c4b57c", + "url": "https://api.github.com/repos/bobthecow/psysh/zipball/4821fab5b7cd8c49a673a9fd5754dc9162bb9e97", + "reference": "4821fab5b7cd8c49a673a9fd5754dc9162bb9e97", "shasum": "" }, "require": { @@ -3512,18 +4920,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": [ @@ -3554,12 +4963,11 @@ "authors": [ { "name": "Justin Hileman", - "email": "justin@justinhileman.info", - "homepage": "http://justinhileman.com" + "email": "justin@justinhileman.info" } ], "description": "An interactive shell for modern PHP.", - "homepage": "http://psysh.org", + "homepage": "https://psysh.org", "keywords": [ "REPL", "console", @@ -3568,9 +4976,9 @@ ], "support": { "issues": "https://github.com/bobthecow/psysh/issues", - "source": "https://github.com/bobthecow/psysh/tree/v0.12.7" + "source": "https://github.com/bobthecow/psysh/tree/v0.12.21" }, - "time": "2024-12-10T01:58:33+00:00" + "time": "2026-03-06T21:21:28+00:00" }, { "name": "ralouphie/getallheaders", @@ -3618,16 +5026,16 @@ }, { "name": "ramsey/collection", - "version": "2.1.0", + "version": "2.1.1", "source": { "type": "git", "url": "https://github.com/ramsey/collection.git", - "reference": "3c5990b8a5e0b79cd1cf11c2dc1229e58e93f109" + "reference": "344572933ad0181accbf4ba763e85a0306a8c5e2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/ramsey/collection/zipball/3c5990b8a5e0b79cd1cf11c2dc1229e58e93f109", - "reference": "3c5990b8a5e0b79cd1cf11c2dc1229e58e93f109", + "url": "https://api.github.com/repos/ramsey/collection/zipball/344572933ad0181accbf4ba763e85a0306a8c5e2", + "reference": "344572933ad0181accbf4ba763e85a0306a8c5e2", "shasum": "" }, "require": { @@ -3688,27 +5096,26 @@ ], "support": { "issues": "https://github.com/ramsey/collection/issues", - "source": "https://github.com/ramsey/collection/tree/2.1.0" + "source": "https://github.com/ramsey/collection/tree/2.1.1" }, - "time": "2025-03-02T04:48:29+00:00" + "time": "2025-03-22T05:38:12+00:00" }, { "name": "ramsey/uuid", - "version": "4.7.6", + "version": "4.9.2", "source": { "type": "git", "url": "https://github.com/ramsey/uuid.git", - "reference": "91039bc1faa45ba123c4328958e620d382ec7088" + "reference": "8429c78ca35a09f27565311b98101e2826affde0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/ramsey/uuid/zipball/91039bc1faa45ba123c4328958e620d382ec7088", - "reference": "91039bc1faa45ba123c4328958e620d382ec7088", + "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", - "ext-json": "*", + "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" }, @@ -3716,26 +5123,23 @@ "rhumsaa/uuid": "self.version" }, "require-dev": { - "captainhook/captainhook": "^5.10", + "captainhook/captainhook": "^5.25", "captainhook/plugin-composer": "^5.3", - "dealerdirect/phpcodesniffer-composer-installer": "^0.7.0", - "doctrine/annotations": "^1.8", - "ergebnis/composer-normalize": "^2.15", - "mockery/mockery": "^1.3", + "dealerdirect/phpcodesniffer-composer-installer": "^1.0", + "ergebnis/composer-normalize": "^2.47", + "mockery/mockery": "^1.6", "paragonie/random-lib": "^2", - "php-mock/php-mock": "^2.2", - "php-mock/php-mock-mockery": "^1.3", - "php-parallel-lint/php-parallel-lint": "^1.1", - "phpbench/phpbench": "^1.0", - "phpstan/extension-installer": "^1.1", - "phpstan/phpstan": "^1.8", - "phpstan/phpstan-mockery": "^1.1", - "phpstan/phpstan-phpunit": "^1.1", - "phpunit/phpunit": "^8.5 || ^9", - "ramsey/composer-repl": "^1.4", - "slevomat/coding-standard": "^8.4", - "squizlabs/php_codesniffer": "^3.5", - "vimeo/psalm": "^4.9" + "php-mock/php-mock": "^2.6", + "php-mock/php-mock-mockery": "^1.5", + "php-parallel-lint/php-parallel-lint": "^1.4.0", + "phpbench/phpbench": "^1.2.14", + "phpstan/extension-installer": "^1.4", + "phpstan/phpstan": "^2.1", + "phpstan/phpstan-mockery": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpunit/phpunit": "^9.6", + "slevomat/coding-standard": "^8.18", + "squizlabs/php_codesniffer": "^3.13" }, "suggest": { "ext-bcmath": "Enables faster math with arbitrary-precision integers using BCMath.", @@ -3770,32 +5174,22 @@ ], "support": { "issues": "https://github.com/ramsey/uuid/issues", - "source": "https://github.com/ramsey/uuid/tree/4.7.6" + "source": "https://github.com/ramsey/uuid/tree/4.9.2" }, - "funding": [ - { - "url": "https://github.com/ramsey", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/ramsey/uuid", - "type": "tidelift" - } - ], - "time": "2024-04-27T21:32:50+00:00" + "time": "2025-12-14T04:43:48+00:00" }, { "name": "spatie/browsershot", - "version": "5.0.8", + "version": "5.2.3", "source": { "type": "git", "url": "https://github.com/spatie/browsershot.git", - "reference": "0102971ae974022ec4a7a149e8924ea355b52cc3" + "reference": "d2e4ac7c69162999940172a674bf83ddc5ac59ea" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/spatie/browsershot/zipball/0102971ae974022ec4a7a149e8924ea355b52cc3", - "reference": "0102971ae974022ec4a7a149e8924ea355b52cc3", + "url": "https://api.github.com/repos/spatie/browsershot/zipball/d2e4ac7c69162999940172a674bf83ddc5ac59ea", + "reference": "d2e4ac7c69162999940172a674bf83ddc5ac59ea", "shasum": "" }, "require": { @@ -3803,13 +5197,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": { @@ -3842,7 +5236,7 @@ "webpage" ], "support": { - "source": "https://github.com/spatie/browsershot/tree/5.0.8" + "source": "https://github.com/spatie/browsershot/tree/5.2.3" }, "funding": [ { @@ -3850,32 +5244,33 @@ "type": "github" } ], - "time": "2025-02-17T09:56:12+00:00" + "time": "2026-02-18T16:10:58+00:00" }, { "name": "spatie/laravel-package-tools", - "version": "1.19.0", + "version": "1.93.0", "source": { "type": "git", "url": "https://github.com/spatie/laravel-package-tools.git", - "reference": "1c9c30ac6a6576b8d15c6c37b6cf23d748df2faa" + "reference": "0d097bce95b2bf6802fb1d83e1e753b0f5a948e7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/spatie/laravel-package-tools/zipball/1c9c30ac6a6576b8d15c6c37b6cf23d748df2faa", - "reference": "1c9c30ac6a6576b8d15c6c37b6cf23d748df2faa", + "url": "https://api.github.com/repos/spatie/laravel-package-tools/zipball/0d097bce95b2bf6802fb1d83e1e753b0f5a948e7", + "reference": "0d097bce95b2bf6802fb1d83e1e753b0f5a948e7", "shasum": "" }, "require": { - "illuminate/contracts": "^9.28|^10.0|^11.0|^12.0", - "php": "^8.0" + "illuminate/contracts": "^10.0|^11.0|^12.0|^13.0", + "php": "^8.1" }, "require-dev": { "mockery/mockery": "^1.5", - "orchestra/testbench": "^7.7|^8.0|^9.0|^10.0", - "pestphp/pest": "^1.23|^2.1|^3.1", - "phpunit/phpunit": "^9.5.24|^10.5|^11.5", - "spatie/pest-plugin-test-time": "^1.1|^2.2" + "orchestra/testbench": "^8.0|^9.2|^10.0|^11.0", + "pestphp/pest": "^2.1|^3.1|^4.0", + "phpunit/php-code-coverage": "^10.0|^11.0|^12.0", + "phpunit/phpunit": "^10.5|^11.5|^12.5", + "spatie/pest-plugin-test-time": "^2.2|^3.0" }, "type": "library", "autoload": { @@ -3902,7 +5297,7 @@ ], "support": { "issues": "https://github.com/spatie/laravel-package-tools/issues", - "source": "https://github.com/spatie/laravel-package-tools/tree/1.19.0" + "source": "https://github.com/spatie/laravel-package-tools/tree/1.93.0" }, "funding": [ { @@ -3910,42 +5305,57 @@ "type": "github" } ], - "time": "2025-02-06T14:58:20+00:00" + "time": "2026-02-21T12:49:54+00:00" }, { - "name": "spatie/pest-expectations", - "version": "1.10.1", + "name": "spatie/laravel-settings", + "version": "3.7.0", "source": { "type": "git", - "url": "https://github.com/spatie/pest-expectations.git", - "reference": "e498ebd92a1a9fb786656edf77fa569e9b39210e" + "url": "https://github.com/spatie/laravel-settings.git", + "reference": "83b179e8097645a30d402d75ba3c19621464494d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/spatie/pest-expectations/zipball/e498ebd92a1a9fb786656edf77fa569e9b39210e", - "reference": "e498ebd92a1a9fb786656edf77fa569e9b39210e", + "url": "https://api.github.com/repos/spatie/laravel-settings/zipball/83b179e8097645a30d402d75ba3c19621464494d", + "reference": "83b179e8097645a30d402d75ba3c19621464494d", "shasum": "" }, "require": { - "illuminate/database": "^10.7|^11.0|^12.0", - "php": "^8.2" + "ext-json": "*", + "illuminate/database": "^11.0|^12.0", + "php": "^8.2", + "phpdocumentor/type-resolver": "^1.5", + "spatie/temporary-directory": "^1.3|^2.0" }, "require-dev": { - "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" + "ext-redis": "*", + "mockery/mockery": "^1.4", + "orchestra/testbench": "^9.0|^10.0", + "pestphp/pest": "^2.0|^3.0", + "pestphp/pest-plugin-laravel": "^2.0|^3.0", + "phpstan/extension-installer": "^1.1", + "phpstan/phpstan-deprecation-rules": "^1.0", + "phpstan/phpstan-phpunit": "^1.0", + "spatie/laravel-data": "^2.0.0|^4.0.0", + "spatie/pest-plugin-snapshots": "^2.0", + "spatie/phpunit-snapshot-assertions": "^4.2|^5.0", + "spatie/ray": "^1.36" + }, + "suggest": { + "spatie/data-transfer-object": "Allows for DTO casting to settings. (deprecated)" }, "type": "library", + "extra": { + "laravel": { + "providers": [ + "Spatie\\LaravelSettings\\LaravelSettingsServiceProvider" + ] + } + }, "autoload": { - "files": [ - "src/PestExpectations.php", - "src/Helpers.php" - ], "psr-4": { - "Spatie\\PestExpectations\\": "src" + "Spatie\\LaravelSettings\\": "src" } }, "notification-url": "https://packagist.org/downloads/", @@ -3954,41 +5364,46 @@ ], "authors": [ { - "name": "Freek Van der Herten", - "email": "freek@spatie.be", + "name": "Ruben Van Assche", + "email": "ruben@spatie.be", + "homepage": "https://spatie.be", "role": "Developer" } ], - "description": "A collection of handy custom Pest customisations", - "homepage": "https://github.com/spatie/pest-expectations", + "description": "Store your application settings", + "homepage": "https://github.com/spatie/laravel-settings", "keywords": [ - "pest-expectations", + "laravel-settings", "spatie" ], "support": { - "issues": "https://github.com/spatie/pest-expectations/issues", - "source": "https://github.com/spatie/pest-expectations/tree/1.10.1" + "issues": "https://github.com/spatie/laravel-settings/issues", + "source": "https://github.com/spatie/laravel-settings/tree/3.7.0" }, "funding": [ + { + "url": "https://spatie.be/open-source/support-us", + "type": "custom" + }, { "url": "https://github.com/spatie", "type": "github" } ], - "time": "2025-03-12T19:34:55+00:00" + "time": "2026-02-09T15:22:32+00:00" }, { "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": { @@ -4024,7 +5439,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": [ { @@ -4036,26 +5451,91 @@ "type": "github" } ], - "time": "2025-01-13T13:04:43+00:00" + "time": "2026-01-12T07:42:22+00:00" }, { - "name": "symfony/clock", - "version": "v7.2.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" @@ -4094,7 +5574,7 @@ "time" ], "support": { - "source": "https://github.com/symfony/clock/tree/v7.2.0" + "source": "https://github.com/symfony/clock/tree/v8.0.0" }, "funding": [ { @@ -4105,32 +5585,37 @@ "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.2.1", + "version": "v7.4.7", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "fefcc18c0f5d0efe3ab3152f15857298868dc2c3" + "reference": "e1e6770440fb9c9b0cf725f81d1361ad1835329d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/fefcc18c0f5d0efe3ab3152f15857298868dc2c3", - "reference": "fefcc18c0f5d0efe3ab3152f15857298868dc2c3", + "url": "https://api.github.com/repos/symfony/console/zipball/e1e6770440fb9c9b0cf725f81d1361ad1835329d", + "reference": "e1e6770440fb9c9b0cf725f81d1361ad1835329d", "shasum": "" }, "require": { "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", "symfony/polyfill-mbstring": "~1.0", "symfony/service-contracts": "^2.5|^3", - "symfony/string": "^6.4|^7.0" + "symfony/string": "^7.2|^8.0" }, "conflict": { "symfony/dependency-injection": "<6.4", @@ -4144,16 +5629,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": { @@ -4187,7 +5672,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v7.2.1" + "source": "https://github.com/symfony/console/tree/v7.4.7" }, "funding": [ { @@ -4198,29 +5683,33 @@ "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-12-11T03:49:26+00:00" + "time": "2026-03-06T14:06:20+00:00" }, { "name": "symfony/css-selector", - "version": "v7.2.0", + "version": "v8.0.6", "source": { "type": "git", "url": "https://github.com/symfony/css-selector.git", - "reference": "601a5ce9aaad7bf10797e3663faefce9e26c24e2" + "reference": "2a178bf80f05dbbe469a337730eba79d61315262" }, "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/2a178bf80f05dbbe469a337730eba79d61315262", + "reference": "2a178bf80f05dbbe469a337730eba79d61315262", "shasum": "" }, "require": { - "php": ">=8.2" + "php": ">=8.4" }, "type": "library", "autoload": { @@ -4252,7 +5741,7 @@ "description": "Converts CSS selectors to XPath expressions", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/css-selector/tree/v7.2.0" + "source": "https://github.com/symfony/css-selector/tree/v8.0.6" }, "funding": [ { @@ -4263,25 +5752,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": "2026-02-17T13:07:04+00:00" }, { "name": "symfony/deprecation-contracts", - "version": "v3.5.1", + "version": "v3.6.0", "source": { "type": "git", "url": "https://github.com/symfony/deprecation-contracts.git", - "reference": "74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6" + "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6", - "reference": "74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/63afe740e99a13ba87ec199bb07bbdee937a5b62", + "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62", "shasum": "" }, "require": { @@ -4294,7 +5787,7 @@ "name": "symfony/contracts" }, "branch-alias": { - "dev-main": "3.5-dev" + "dev-main": "3.6-dev" } }, "autoload": { @@ -4319,7 +5812,7 @@ "description": "A generic function and convention to trigger deprecation notices", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/deprecation-contracts/tree/v3.5.1" + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.6.0" }, "funding": [ { @@ -4335,35 +5828,38 @@ "type": "tidelift" } ], - "time": "2024-09-25T14:20:29+00:00" + "time": "2024-09-25T14:21:43+00:00" }, { "name": "symfony/error-handler", - "version": "v7.2.4", + "version": "v7.4.4", "source": { "type": "git", "url": "https://github.com/symfony/error-handler.git", - "reference": "aabf79938aa795350c07ce6464dd1985607d95d5" + "reference": "8da531f364ddfee53e36092a7eebbbd0b775f6b8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/error-handler/zipball/aabf79938aa795350c07ce6464dd1985607d95d5", - "reference": "aabf79938aa795350c07ce6464dd1985607d95d5", + "url": "https://api.github.com/repos/symfony/error-handler/zipball/8da531f364ddfee53e36092a7eebbbd0b775f6b8", + "reference": "8da531f364ddfee53e36092a7eebbbd0b775f6b8", "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|^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": [ "Resources/bin/patch-type-declarations" @@ -4394,7 +5890,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.2.4" + "source": "https://github.com/symfony/error-handler/tree/v7.4.4" }, "funding": [ { @@ -4405,33 +5901,37 @@ "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-02T20:27:07+00:00" + "time": "2026-01-20T16:42:42+00:00" }, { "name": "symfony/event-dispatcher", - "version": "v7.2.0", + "version": "v8.0.4", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "910c5db85a5356d0fea57680defec4e99eb9c8c1" + "reference": "99301401da182b6cfaa4700dbe9987bb75474b47" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/910c5db85a5356d0fea57680defec4e99eb9c8c1", - "reference": "910c5db85a5356d0fea57680defec4e99eb9c8c1", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/99301401da182b6cfaa4700dbe9987bb75474b47", + "reference": "99301401da182b6cfaa4700dbe9987bb75474b47", "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": { @@ -4440,13 +5940,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": { @@ -4474,7 +5975,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.2.0" + "source": "https://github.com/symfony/event-dispatcher/tree/v8.0.4" }, "funding": [ { @@ -4485,25 +5986,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": "2026-01-05T11:45:55+00:00" }, { "name": "symfony/event-dispatcher-contracts", - "version": "v3.5.1", + "version": "v3.6.0", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher-contracts.git", - "reference": "7642f5e970b672283b7823222ae8ef8bbc160b9f" + "reference": "59eb412e93815df44f05f342958efa9f46b1e586" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/7642f5e970b672283b7823222ae8ef8bbc160b9f", - "reference": "7642f5e970b672283b7823222ae8ef8bbc160b9f", + "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/59eb412e93815df44f05f342958efa9f46b1e586", + "reference": "59eb412e93815df44f05f342958efa9f46b1e586", "shasum": "" }, "require": { @@ -4517,7 +6022,7 @@ "name": "symfony/contracts" }, "branch-alias": { - "dev-main": "3.5-dev" + "dev-main": "3.6-dev" } }, "autoload": { @@ -4550,7 +6055,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.5.1" + "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.6.0" }, "funding": [ { @@ -4566,27 +6071,97 @@ "type": "tidelift" } ], - "time": "2024-09-25T14:20:29+00:00" + "time": "2024-09-25T14:21:43+00:00" }, { - "name": "symfony/finder", - "version": "v7.2.2", + "name": "symfony/filesystem", + "version": "v8.0.6", "source": { "type": "git", - "url": "https://github.com/symfony/finder.git", - "reference": "87a71856f2f56e4100373e92529eed3171695cfb" + "url": "https://github.com/symfony/filesystem.git", + "reference": "7bf9162d7a0dff98d079b72948508fa48018a770" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/87a71856f2f56e4100373e92529eed3171695cfb", - "reference": "87a71856f2f56e4100373e92529eed3171695cfb", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/7bf9162d7a0dff98d079b72948508fa48018a770", + "reference": "7bf9162d7a0dff98d079b72948508fa48018a770", + "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.6" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-02-25T16:59:43+00:00" + }, + { + "name": "symfony/finder", + "version": "v7.4.6", + "source": { + "type": "git", + "url": "https://github.com/symfony/finder.git", + "reference": "8655bf1076b7a3a346cb11413ffdabff50c7ffcf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/finder/zipball/8655bf1076b7a3a346cb11413ffdabff50c7ffcf", + "reference": "8655bf1076b7a3a346cb11413ffdabff50c7ffcf", "shasum": "" }, "require": { "php": ">=8.2" }, "require-dev": { - "symfony/filesystem": "^6.4|^7.0" + "symfony/filesystem": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { @@ -4614,7 +6189,7 @@ "description": "Finds files and directories via an intuitive fluent interface", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/finder/tree/v7.2.2" + "source": "https://github.com/symfony/finder/tree/v7.4.6" }, "funding": [ { @@ -4625,32 +6200,35 @@ "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-12-30T19:00:17+00:00" + "time": "2026-01-29T09:40:50+00:00" }, { "name": "symfony/http-foundation", - "version": "v7.2.3", + "version": "v7.4.7", "source": { "type": "git", "url": "https://github.com/symfony/http-foundation.git", - "reference": "ee1b504b8926198be89d05e5b6fc4c3810c090f0" + "reference": "f94b3e7b7dafd40e666f0c9ff2084133bae41e81" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-foundation/zipball/ee1b504b8926198be89d05e5b6fc4c3810c090f0", - "reference": "ee1b504b8926198be89d05e5b6fc4c3810c090f0", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/f94b3e7b7dafd40e666f0c9ff2084133bae41e81", + "reference": "f94b3e7b7dafd40e666f0c9ff2084133bae41e81", "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", @@ -4659,12 +6237,13 @@ "require-dev": { "doctrine/dbal": "^3.6|^4", "predis/predis": "^1.1|^2.0", - "symfony/cache": "^6.4.12|^7.1.5", - "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": { @@ -4692,7 +6271,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.2.3" + "source": "https://github.com/symfony/http-foundation/tree/v7.4.7" }, "funding": [ { @@ -4703,34 +6282,38 @@ "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-01-17T10:56:55+00:00" + "time": "2026-03-06T13:15:18+00:00" }, { "name": "symfony/http-kernel", - "version": "v7.2.4", + "version": "v7.4.7", "source": { "type": "git", "url": "https://github.com/symfony/http-kernel.git", - "reference": "9f1103734c5789798fefb90e91de4586039003ed" + "reference": "3b3fcf386c809be990c922e10e4c620d6367cab1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-kernel/zipball/9f1103734c5789798fefb90e91de4586039003ed", - "reference": "9f1103734c5789798fefb90e91de4586039003ed", + "url": "https://api.github.com/repos/symfony/http-kernel/zipball/3b3fcf386c809be990c922e10e4c620d6367cab1", + "reference": "3b3fcf386c809be990c922e10e4c620d6367cab1", "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": "^6.4|^7.0", - "symfony/http-foundation": "^6.4|^7.0", + "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": { @@ -4740,6 +6323,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", @@ -4757,27 +6341,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.1|^7.0.1|^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", @@ -4806,7 +6390,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.2.4" + "source": "https://github.com/symfony/http-kernel/tree/v7.4.7" }, "funding": [ { @@ -4817,25 +6401,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-26T11:01:22+00:00" + "time": "2026-03-06T16:33:18+00:00" }, { "name": "symfony/mailer", - "version": "v7.2.3", + "version": "v7.4.6", "source": { "type": "git", "url": "https://github.com/symfony/mailer.git", - "reference": "f3871b182c44997cf039f3b462af4a48fb85f9d3" + "reference": "b02726f39a20bc65e30364f5c750c4ddbf1f58e9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/mailer/zipball/f3871b182c44997cf039f3b462af4a48fb85f9d3", - "reference": "f3871b182c44997cf039f3b462af4a48fb85f9d3", + "url": "https://api.github.com/repos/symfony/mailer/zipball/b02726f39a20bc65e30364f5c750c4ddbf1f58e9", + "reference": "b02726f39a20bc65e30364f5c750c4ddbf1f58e9", "shasum": "" }, "require": { @@ -4843,8 +6431,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": { @@ -4855,10 +6443,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": { @@ -4886,7 +6474,7 @@ "description": "Helps sending emails", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/mailer/tree/v7.2.3" + "source": "https://github.com/symfony/mailer/tree/v7.4.6" }, "funding": [ { @@ -4897,48 +6485,53 @@ "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-01-27T11:08:17+00:00" + "time": "2026-02-25T16:50:00+00:00" }, { "name": "symfony/mime", - "version": "v7.2.4", + "version": "v7.4.7", "source": { "type": "git", "url": "https://github.com/symfony/mime.git", - "reference": "87ca22046b78c3feaff04b337f33b38510fd686b" + "reference": "da5ab4fde3f6c88ab06e96185b9922f48b677cd1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/mime/zipball/87ca22046b78c3feaff04b337f33b38510fd686b", - "reference": "87ca22046b78c3feaff04b337f33b38510fd686b", + "url": "https://api.github.com/repos/symfony/mime/zipball/da5ab4fde3f6c88ab06e96185b9922f48b677cd1", + "reference": "da5ab4fde3f6c88ab06e96185b9922f48b677cd1", "shasum": "" }, "require": { "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", "symfony/polyfill-intl-idn": "^1.10", "symfony/polyfill-mbstring": "^1.0" }, "conflict": { "egulias/email-validator": "~3.0.0", - "phpdocumentor/reflection-docblock": "<3.2.2", - "phpdocumentor/type-resolver": "<1.4.0", + "phpdocumentor/reflection-docblock": "<5.2|>=7", + "phpdocumentor/type-resolver": "<1.5.1", "symfony/mailer": "<6.4", "symfony/serializer": "<6.4.3|>7.0,<7.0.3" }, "require-dev": { "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" + "phpdocumentor/reflection-docblock": "^5.2|^6.0", + "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": { @@ -4970,7 +6563,7 @@ "mime-type" ], "support": { - "source": "https://github.com/symfony/mime/tree/v7.2.4" + "source": "https://github.com/symfony/mime/tree/v7.4.7" }, "funding": [ { @@ -4981,16 +6574,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-02-19T08:51:20+00:00" + "time": "2026-03-05T15:24:09+00:00" }, { "name": "symfony/polyfill-ctype", - "version": "v1.31.0", + "version": "v1.33.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-ctype.git", @@ -5049,7 +6646,7 @@ "portable" ], "support": { - "source": "https://github.com/symfony/polyfill-ctype/tree/v1.31.0" + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.33.0" }, "funding": [ { @@ -5060,6 +6657,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" @@ -5069,16 +6670,16 @@ }, { "name": "symfony/polyfill-intl-grapheme", - "version": "v1.31.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": { @@ -5127,7 +6728,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.31.0" + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.33.0" }, "funding": [ { @@ -5138,25 +6739,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-06-27T09:58:17+00:00" }, { "name": "symfony/polyfill-intl-idn", - "version": "v1.31.0", + "version": "v1.33.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-idn.git", - "reference": "c36586dcf89a12315939e00ec9b4474adcb1d773" + "reference": "9614ac4d8061dc257ecc64cba1b140873dce8ad3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/c36586dcf89a12315939e00ec9b4474adcb1d773", - "reference": "c36586dcf89a12315939e00ec9b4474adcb1d773", + "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/9614ac4d8061dc257ecc64cba1b140873dce8ad3", + "reference": "9614ac4d8061dc257ecc64cba1b140873dce8ad3", "shasum": "" }, "require": { @@ -5210,7 +6815,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.31.0" + "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.33.0" }, "funding": [ { @@ -5221,16 +6826,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": "2024-09-10T14:38:51+00:00" }, { "name": "symfony/polyfill-intl-normalizer", - "version": "v1.31.0", + "version": "v1.33.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-normalizer.git", @@ -5291,7 +6900,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.31.0" + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.33.0" }, "funding": [ { @@ -5302,6 +6911,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" @@ -5311,19 +6924,20 @@ }, { "name": "symfony/polyfill-mbstring", - "version": "v1.31.0", + "version": "v1.33.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341" + "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/85181ba99b2345b0ef10ce42ecac37612d9fd341", - "reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/6d857f4d76bd4b343eac26d6b539585d2bc56493", + "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493", "shasum": "" }, "require": { + "ext-iconv": "*", "php": ">=7.2" }, "provide": { @@ -5371,7 +6985,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.31.0" + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.33.0" }, "funding": [ { @@ -5382,25 +6996,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": "2024-12-23T08:48:59+00:00" }, { "name": "symfony/polyfill-php80", - "version": "v1.31.0", + "version": "v1.33.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php80.git", - "reference": "60328e362d4c2c802a54fcbf04f9d3fb892b4cf8" + "reference": "0cc9dd0f17f61d8131e7df6b84bd344899fe2608" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/60328e362d4c2c802a54fcbf04f9d3fb892b4cf8", - "reference": "60328e362d4c2c802a54fcbf04f9d3fb892b4cf8", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/0cc9dd0f17f61d8131e7df6b84bd344899fe2608", + "reference": "0cc9dd0f17f61d8131e7df6b84bd344899fe2608", "shasum": "" }, "require": { @@ -5451,7 +7069,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php80/tree/v1.31.0" + "source": "https://github.com/symfony/polyfill-php80/tree/v1.33.0" }, "funding": [ { @@ -5462,25 +7080,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-01-02T08:10:11+00:00" }, { "name": "symfony/polyfill-php83", - "version": "v1.31.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": { @@ -5527,7 +7149,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php83/tree/v1.31.0" + "source": "https://github.com/symfony/polyfill-php83/tree/v1.33.0" }, "funding": [ { @@ -5538,16 +7160,180 @@ "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.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php84.git", + "reference": "d8ced4d875142b6a7426000426b8abc631d6b191" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php84/zipball/d8ced4d875142b6a7426000426b8abc631d6b191", + "reference": "d8ced4d875142b6a7426000426b8abc631d6b191", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php84\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.4+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php84/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-06-24T13:30:11+00:00" + }, + { + "name": "symfony/polyfill-php85", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php85.git", + "reference": "d4e5fcd4ab3d998ab16c0db48e6cbb9a01993f91" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php85/zipball/d4e5fcd4ab3d998ab16c0db48e6cbb9a01993f91", + "reference": "d4e5fcd4ab3d998ab16c0db48e6cbb9a01993f91", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php85\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.5+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php85/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-06-23T16:12:55+00:00" }, { "name": "symfony/polyfill-uuid", - "version": "v1.31.0", + "version": "v1.33.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-uuid.git", @@ -5606,7 +7392,7 @@ "uuid" ], "support": { - "source": "https://github.com/symfony/polyfill-uuid/tree/v1.31.0" + "source": "https://github.com/symfony/polyfill-uuid/tree/v1.33.0" }, "funding": [ { @@ -5617,6 +7403,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" @@ -5626,16 +7416,16 @@ }, { "name": "symfony/process", - "version": "v7.2.4", + "version": "v7.4.5", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "d8f411ff3c7ddc4ae9166fb388d1190a2df5b5cf" + "reference": "608476f4604102976d687c483ac63a79ba18cc97" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/d8f411ff3c7ddc4ae9166fb388d1190a2df5b5cf", - "reference": "d8f411ff3c7ddc4ae9166fb388d1190a2df5b5cf", + "url": "https://api.github.com/repos/symfony/process/zipball/608476f4604102976d687c483ac63a79ba18cc97", + "reference": "608476f4604102976d687c483ac63a79ba18cc97", "shasum": "" }, "require": { @@ -5667,7 +7457,7 @@ "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/process/tree/v7.2.4" + "source": "https://github.com/symfony/process/tree/v7.4.5" }, "funding": [ { @@ -5678,25 +7468,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-05T08:33:46+00:00" + "time": "2026-01-26T15:07:59+00:00" }, { "name": "symfony/routing", - "version": "v7.2.3", + "version": "v7.4.6", "source": { "type": "git", "url": "https://github.com/symfony/routing.git", - "reference": "ee9a67edc6baa33e5fae662f94f91fd262930996" + "reference": "238d749c56b804b31a9bf3e26519d93b65a60938" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/routing/zipball/ee9a67edc6baa33e5fae662f94f91fd262930996", - "reference": "ee9a67edc6baa33e5fae662f94f91fd262930996", + "url": "https://api.github.com/repos/symfony/routing/zipball/238d749c56b804b31a9bf3e26519d93b65a60938", + "reference": "238d749c56b804b31a9bf3e26519d93b65a60938", "shasum": "" }, "require": { @@ -5710,11 +7504,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": { @@ -5748,7 +7542,7 @@ "url" ], "support": { - "source": "https://github.com/symfony/routing/tree/v7.2.3" + "source": "https://github.com/symfony/routing/tree/v7.4.6" }, "funding": [ { @@ -5759,25 +7553,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-01-17T10:56:55+00:00" + "time": "2026-02-25T16:50:00+00:00" }, { "name": "symfony/service-contracts", - "version": "v3.5.1", + "version": "v3.6.1", "source": { "type": "git", "url": "https://github.com/symfony/service-contracts.git", - "reference": "e53260aabf78fb3d63f8d79d69ece59f80d5eda0" + "reference": "45112560a3ba2d715666a509a0bc9521d10b6c43" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/service-contracts/zipball/e53260aabf78fb3d63f8d79d69ece59f80d5eda0", - "reference": "e53260aabf78fb3d63f8d79d69ece59f80d5eda0", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/45112560a3ba2d715666a509a0bc9521d10b6c43", + "reference": "45112560a3ba2d715666a509a0bc9521d10b6c43", "shasum": "" }, "require": { @@ -5795,7 +7593,7 @@ "name": "symfony/contracts" }, "branch-alias": { - "dev-main": "3.5-dev" + "dev-main": "3.6-dev" } }, "autoload": { @@ -5831,7 +7629,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/service-contracts/tree/v3.5.1" + "source": "https://github.com/symfony/service-contracts/tree/v3.6.1" }, "funding": [ { @@ -5842,44 +7640,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": "2024-09-25T14:20:29+00:00" + "time": "2025-07-15T11:30:57+00:00" }, { "name": "symfony/string", - "version": "v7.2.0", + "version": "v8.0.6", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "446e0d146f991dde3e73f45f2c97a9faad773c82" + "reference": "6c9e1108041b5dce21a9a4984b531c4923aa9ec4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/446e0d146f991dde3e73f45f2c97a9faad773c82", - "reference": "446e0d146f991dde3e73f45f2c97a9faad773c82", + "url": "https://api.github.com/repos/symfony/string/zipball/6c9e1108041b5dce21a9a4984b531c4923aa9ec4", + "reference": "6c9e1108041b5dce21a9a4984b531c4923aa9ec4", "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": { @@ -5918,7 +7719,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v7.2.0" + "source": "https://github.com/symfony/string/tree/v8.0.6" }, "funding": [ { @@ -5929,60 +7730,58 @@ "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-11-13T13:31:26+00:00" + "time": "2026-02-09T10:14:57+00:00" }, { "name": "symfony/translation", - "version": "v7.2.4", + "version": "v8.0.6", "source": { "type": "git", "url": "https://github.com/symfony/translation.git", - "reference": "283856e6981286cc0d800b53bd5703e8e363f05a" + "reference": "13ff19bcf2bea492d3c2fbeaa194dd6f4599ce1b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/translation/zipball/283856e6981286cc0d800b53bd5703e8e363f05a", - "reference": "283856e6981286cc0d800b53bd5703e8e363f05a", + "url": "https://api.github.com/repos/symfony/translation/zipball/13ff19bcf2bea492d3c2fbeaa194dd6f4599ce1b", + "reference": "13ff19bcf2bea492d3c2fbeaa194dd6f4599ce1b", "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": { - "symfony/config": "<6.4", - "symfony/console": "<6.4", - "symfony/dependency-injection": "<6.4", + "nikic/php-parser": "<5.0", "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" }, "require-dev": { - "nikic/php-parser": "^4.18|^5.0", + "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": { @@ -6013,7 +7812,7 @@ "description": "Provides tools to internationalize your application", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/translation/tree/v7.2.4" + "source": "https://github.com/symfony/translation/tree/v8.0.6" }, "funding": [ { @@ -6024,25 +7823,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-13T10:27:23+00:00" + "time": "2026-02-17T13:07:04+00:00" }, { "name": "symfony/translation-contracts", - "version": "v3.5.1", + "version": "v3.6.1", "source": { "type": "git", "url": "https://github.com/symfony/translation-contracts.git", - "reference": "4667ff3bd513750603a09c8dedbea942487fb07c" + "reference": "65a8bc82080447fae78373aa10f8d13b38338977" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/translation-contracts/zipball/4667ff3bd513750603a09c8dedbea942487fb07c", - "reference": "4667ff3bd513750603a09c8dedbea942487fb07c", + "url": "https://api.github.com/repos/symfony/translation-contracts/zipball/65a8bc82080447fae78373aa10f8d13b38338977", + "reference": "65a8bc82080447fae78373aa10f8d13b38338977", "shasum": "" }, "require": { @@ -6055,7 +7858,7 @@ "name": "symfony/contracts" }, "branch-alias": { - "dev-main": "3.5-dev" + "dev-main": "3.6-dev" } }, "autoload": { @@ -6091,7 +7894,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/translation-contracts/tree/v3.5.1" + "source": "https://github.com/symfony/translation-contracts/tree/v3.6.1" }, "funding": [ { @@ -6102,25 +7905,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:20:29+00:00" + "time": "2025-07-15T13:41:35+00:00" }, { "name": "symfony/uid", - "version": "v7.2.0", + "version": "v7.4.4", "source": { "type": "git", "url": "https://github.com/symfony/uid.git", - "reference": "2d294d0c48df244c71c105a169d0190bfb080426" + "reference": "7719ce8aba76be93dfe249192f1fbfa52c588e36" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/uid/zipball/2d294d0c48df244c71c105a169d0190bfb080426", - "reference": "2d294d0c48df244c71c105a169d0190bfb080426", + "url": "https://api.github.com/repos/symfony/uid/zipball/7719ce8aba76be93dfe249192f1fbfa52c588e36", + "reference": "7719ce8aba76be93dfe249192f1fbfa52c588e36", "shasum": "" }, "require": { @@ -6128,7 +7935,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": { @@ -6165,7 +7972,7 @@ "uuid" ], "support": { - "source": "https://github.com/symfony/uid/tree/v7.2.0" + "source": "https://github.com/symfony/uid/tree/v7.4.4" }, "funding": [ { @@ -6176,40 +7983,44 @@ "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": "2026-01-03T23:30:35+00:00" }, { "name": "symfony/var-dumper", - "version": "v7.2.3", + "version": "v7.4.6", "source": { "type": "git", "url": "https://github.com/symfony/var-dumper.git", - "reference": "82b478c69745d8878eb60f9a049a4d584996f73a" + "reference": "045321c440ac18347b136c63d2e9bf28a2dc0291" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-dumper/zipball/82b478c69745d8878eb60f9a049a4d584996f73a", - "reference": "82b478c69745d8878eb60f9a049a4d584996f73a", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/045321c440ac18347b136c63d2e9bf28a2dc0291", + "reference": "045321c440ac18347b136c63d2e9bf28a2dc0291", "shasum": "" }, "require": { "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", "symfony/polyfill-mbstring": "~1.0" }, "conflict": { "symfony/console": "<6.4" }, "require-dev": { - "ext-iconv": "*", - "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": [ @@ -6248,7 +8059,7 @@ "dump" ], "support": { - "source": "https://github.com/symfony/var-dumper/tree/v7.2.3" + "source": "https://github.com/symfony/var-dumper/tree/v7.4.6" }, "funding": [ { @@ -6259,32 +8070,192 @@ "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-01-17T11:39:41+00:00" + "time": "2026-02-15T10:53:20+00:00" }, { - "name": "tijsverkoyen/css-to-inline-styles", - "version": "v2.3.0", + "name": "symfony/var-exporter", + "version": "v8.0.0", "source": { "type": "git", - "url": "https://github.com/tijsverkoyen/CssToInlineStyles.git", - "reference": "0d72ac1c00084279c1816675284073c5a337c20d" + "url": "https://github.com/symfony/var-exporter.git", + "reference": "7345f46c251f2eb27c7b3ebdb5bb076b3ffcae04" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/tijsverkoyen/CssToInlineStyles/zipball/0d72ac1c00084279c1816675284073c5a337c20d", - "reference": "0d72ac1c00084279c1816675284073c5a337c20d", + "url": "https://api.github.com/repos/symfony/var-exporter/zipball/7345f46c251f2eb27c7b3ebdb5bb076b3ffcae04", + "reference": "7345f46c251f2eb27c7b3ebdb5bb076b3ffcae04", + "shasum": "" + }, + "require": { + "php": ">=8.4" + }, + "require-dev": { + "symfony/property-access": "^7.4|^8.0", + "symfony/serializer": "^7.4|^8.0", + "symfony/var-dumper": "^7.4|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\VarExporter\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Allows exporting any serializable PHP data structure to plain PHP code", + "homepage": "https://symfony.com", + "keywords": [ + "clone", + "construct", + "export", + "hydrate", + "instantiate", + "lazy-loading", + "proxy", + "serialize" + ], + "support": { + "source": "https://github.com/symfony/var-exporter/tree/v8.0.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-11-05T18:53:00+00:00" + }, + { + "name": "symfony/yaml", + "version": "v7.4.6", + "source": { + "type": "git", + "url": "https://github.com/symfony/yaml.git", + "reference": "58751048de17bae71c5aa0d13cb19d79bca26391" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/yaml/zipball/58751048de17bae71c5aa0d13cb19d79bca26391", + "reference": "58751048de17bae71c5aa0d13cb19d79bca26391", + "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.6" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-02-09T09:33:46+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", @@ -6317,32 +8288,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.1", + "version": "v5.6.3", "source": { "type": "git", "url": "https://github.com/vlucas/phpdotenv.git", - "reference": "a59a13791077fe3d44f90e7133eb68e7d22eaff2" + "reference": "955e7815d677a3eaa7075231212f2110983adecc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/a59a13791077fe3d44f90e7133eb68e7d22eaff2", - "reference": "a59a13791077fe3d44f90e7133eb68e7d22eaff2", + "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", @@ -6391,7 +8362,7 @@ ], "support": { "issues": "https://github.com/vlucas/phpdotenv/issues", - "source": "https://github.com/vlucas/phpdotenv/tree/v5.6.1" + "source": "https://github.com/vlucas/phpdotenv/tree/v5.6.3" }, "funding": [ { @@ -6403,7 +8374,7 @@ "type": "tidelift" } ], - "time": "2024-07-20T21:52:34+00:00" + "time": "2025-12-27T19:49:13+00:00" }, { "name": "voku/portable-ascii", @@ -6480,120 +8451,56 @@ "time": "2024-11-21T01:49:47+00:00" }, { - "name": "voku/simple_html_dom", - "version": "4.8.10", + "name": "wnx/sidecar-browsershot", + "version": "v2.9.0", "source": { "type": "git", - "url": "https://github.com/voku/simple_html_dom.git", - "reference": "716822ed52ed3a1881542be07a786270de390e99" + "url": "https://github.com/stefanzweifel/sidecar-browsershot.git", + "reference": "352083995009bec142ff0c7ae4b2883831be0685" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/voku/simple_html_dom/zipball/716822ed52ed3a1881542be07a786270de390e99", - "reference": "716822ed52ed3a1881542be07a786270de390e99", + "url": "https://api.github.com/repos/stefanzweifel/sidecar-browsershot/zipball/352083995009bec142ff0c7ae4b2883831be0685", + "reference": "352083995009bec142ff0c7ae4b2883831be0685", "shasum": "" }, "require": { - "ext-dom": "*", - "ext-libxml": "*", - "ext-simplexml": "*", - "php": ">=7.0.0", - "symfony/css-selector": "~3.0 || ~4.0 || ~5.0 || ~6.0 || ~7.0" + "hammerstone/sidecar": "^0.7.1 | ^0.8.0", + "illuminate/contracts": "^12.0 | ^13.0", + "php": "^8.4", + "spatie/browsershot": "^5.0", + "spatie/laravel-package-tools": "^1.9.2" }, "require-dev": { - "phpunit/phpunit": "~6.0 || ~7.0 || ~9.0" - }, - "suggest": { - "voku/portable-utf8": "If you need e.g. UTF-8 fixed output." - }, - "type": "library", - "autoload": { - "psr-4": { - "voku\\helper\\": "src/voku/helper/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "dimabdc", - "email": "support@titor.ru", - "homepage": "https://github.com/dimabdc", - "role": "Developer" - }, - { - "name": "Lars Moelleken", - "homepage": "https://www.moelleken.org/", - "role": "Fork-Maintainer" - } - ], - "description": "Simple HTML DOM package.", - "homepage": "https://github.com/voku/simple_html_dom", - "keywords": [ - "HTML Parser", - "dom", - "php dom" - ], - "support": { - "issues": "https://github.com/voku/simple_html_dom/issues", - "source": "https://github.com/voku/simple_html_dom/tree/4.8.10" - }, - "funding": [ - { - "url": "https://www.paypal.me/moelleken", - "type": "custom" - }, - { - "url": "https://github.com/voku", - "type": "github" - }, - { - "url": "https://www.patreon.com/voku", - "type": "patreon" - }, - { - "url": "https://tidelift.com/funding/github/packagist/voku/simple_html_dom", - "type": "tidelift" - } - ], - "time": "2024-07-03T16:05:14+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" + "ext-imagick": "*", + "laravel/pint": "^1.13", + "league/flysystem-aws-s3-v3": "^1.0|^2.0|^3.0", + "nunomaduro/collision": "^7.0|^8.0", + "orchestra/testbench": "^10.0 | ^11.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|^2.0", + "phpstan/phpstan-phpunit": "^1.0|^2.0", + "phpunit/phpunit": "^11.0 | ^12.0", + "spatie/image": "^3.3", + "spatie/pixelmatch-php": "^1.2" }, "type": "library", "extra": { - "branch-alias": { - "dev-master": "1.10-dev" + "laravel": { + "aliases": { + "SidecarBrowsershot": "Wnx\\SidecarBrowsershot\\Facades\\SidecarBrowsershot" + }, + "providers": [ + "Wnx\\SidecarBrowsershot\\SidecarBrowsershotServiceProvider" + ] } }, "autoload": { "psr-4": { - "Webmozart\\Assert\\": "src/" + "Wnx\\SidecarBrowsershot\\": "src", + "Wnx\\SidecarBrowsershot\\Database\\Factories\\": "database/factories" } }, "notification-url": "https://packagist.org/downloads/", @@ -6602,36 +8509,47 @@ ], "authors": [ { - "name": "Bernhard Schussek", - "email": "bschussek@gmail.com" + "name": "Stefan Zweifel", + "email": "stefan@stefanzweifel.dev", + "role": "Developer" } ], - "description": "Assertions to validate method input/output with nice error messages.", + "description": "A Sidecar function to run Browsershot on Lambda.", + "homepage": "https://github.com/stefanzweifel/sidecar-browsershot", "keywords": [ - "assert", - "check", - "validate" + "browsershot", + "lambda", + "laravel", + "sidecar", + "sidecar-browsershot", + "wnx" ], "support": { - "issues": "https://github.com/webmozarts/assert/issues", - "source": "https://github.com/webmozarts/assert/tree/1.11.0" + "issues": "https://github.com/stefanzweifel/sidecar-browsershot/issues", + "source": "https://github.com/stefanzweifel/sidecar-browsershot/tree/v2.9.0" }, - "time": "2022-06-03T18:03:27+00:00" + "funding": [ + { + "url": "https://github.com/stefanzweifel", + "type": "github" + } + ], + "time": "2026-03-13T20:12:58+00:00" } ], "packages-dev": [ { "name": "brianium/paratest", - "version": "v7.7.0", + "version": "v7.19.0", "source": { "type": "git", "url": "https://github.com/paratestphp/paratest.git", - "reference": "4fb3f73bc5a4c3146bac2850af7dc72435a32daf" + "reference": "7c6c29af7c4b406b49ce0c6b0a3a81d3684474e6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/paratestphp/paratest/zipball/4fb3f73bc5a4c3146bac2850af7dc72435a32daf", - "reference": "4fb3f73bc5a4c3146bac2850af7dc72435a32daf", + "url": "https://api.github.com/repos/paratestphp/paratest/zipball/7c6c29af7c4b406b49ce0c6b0a3a81d3684474e6", + "reference": "7c6c29af7c4b406b49ce0c6b0a3a81d3684474e6", "shasum": "" }, "require": { @@ -6639,27 +8557,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.8", - "phpunit/php-file-iterator": "^5.1.0", - "phpunit/php-timer": "^7.0.1", - "phpunit/phpunit": "^11.5.1", - "sebastian/environment": "^7.2.0", - "symfony/console": "^6.4.14 || ^7.2.1", - "symfony/process": "^6.4.14 || ^7.2.0" + "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.3 || ^13.0.1", + "phpunit/php-file-iterator": "^6.0.1 || ^7", + "phpunit/php-timer": "^8 || ^9", + "phpunit/phpunit": "^12.5.9 || ^13", + "sebastian/environment": "^8.0.3 || ^9", + "symfony/console": "^7.4.4 || ^8.0.4", + "symfony/process": "^7.4.5 || ^8.0.5" }, "require-dev": { - "doctrine/coding-standard": "^12.0.0", + "doctrine/coding-standard": "^14.0.0", + "ext-pcntl": "*", "ext-pcov": "*", "ext-posix": "*", - "phpstan/phpstan": "^2.0.3", - "phpstan/phpstan-deprecation-rules": "^2.0.1", - "phpstan/phpstan-phpunit": "^2.0.1", - "phpstan/phpstan-strict-rules": "^2", - "squizlabs/php_codesniffer": "^3.11.1", - "symfony/filesystem": "^6.4.13 || ^7.2.0" + "phpstan/phpstan": "^2.1.38", + "phpstan/phpstan-deprecation-rules": "^2.0.3", + "phpstan/phpstan-phpunit": "^2.0.12", + "phpstan/phpstan-strict-rules": "^2.0.8", + "symfony/filesystem": "^7.4.0 || ^8.0.1" }, "bin": [ "bin/paratest", @@ -6699,7 +8617,7 @@ ], "support": { "issues": "https://github.com/paratestphp/paratest/issues", - "source": "https://github.com/paratestphp/paratest/tree/v7.7.0" + "source": "https://github.com/paratestphp/paratest/tree/v7.19.0" }, "funding": [ { @@ -6711,52 +8629,7 @@ "type": "paypal" } ], - "time": "2024-12-11T14:50:44+00:00" - }, - { - "name": "doctrine/deprecations", - "version": "1.1.4", - "source": { - "type": "git", - "url": "https://github.com/doctrine/deprecations.git", - "reference": "31610dbb31faa98e6b5447b62340826f54fbc4e9" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/doctrine/deprecations/zipball/31610dbb31faa98e6b5447b62340826f54fbc4e9", - "reference": "31610dbb31faa98e6b5447b62340826f54fbc4e9", - "shasum": "" - }, - "require": { - "php": "^7.1 || ^8.0" - }, - "require-dev": { - "doctrine/coding-standard": "^9 || ^12", - "phpstan/phpstan": "1.4.10 || 2.0.3", - "phpstan/phpstan-phpunit": "^1.0 || ^2", - "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", - "psr/log": "^1 || ^2 || ^3" - }, - "suggest": { - "psr/log": "Allows logging deprecations via PSR-3 logger implementation" - }, - "type": "library", - "autoload": { - "psr-4": { - "Doctrine\\Deprecations\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "description": "A small layer on top of trigger_error(E_USER_DEPRECATED) or PSR-3 logging with options to disable all deprecations or selectively for packages.", - "homepage": "https://www.doctrine-project.org/", - "support": { - "issues": "https://github.com/doctrine/deprecations/issues", - "source": "https://github.com/doctrine/deprecations/tree/1.1.4" - }, - "time": "2024-12-07T21:18:45+00:00" + "time": "2026-02-06T10:53:26+00:00" }, { "name": "fakerphp/faker", @@ -6823,16 +8696,16 @@ }, { "name": "fidry/cpu-core-counter", - "version": "1.2.0", + "version": "1.3.0", "source": { "type": "git", "url": "https://github.com/theofidry/cpu-core-counter.git", - "reference": "8520451a140d3f46ac33042715115e290cf5785f" + "reference": "db9508f7b1474469d9d3c53b86f817e344732678" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/theofidry/cpu-core-counter/zipball/8520451a140d3f46ac33042715115e290cf5785f", - "reference": "8520451a140d3f46ac33042715115e290cf5785f", + "url": "https://api.github.com/repos/theofidry/cpu-core-counter/zipball/db9508f7b1474469d9d3c53b86f817e344732678", + "reference": "db9508f7b1474469d9d3c53b86f817e344732678", "shasum": "" }, "require": { @@ -6842,10 +8715,10 @@ "fidry/makefile": "^0.2.0", "fidry/php-cs-fixer-config": "^1.1.2", "phpstan/extension-installer": "^1.2.0", - "phpstan/phpstan": "^1.9.2", - "phpstan/phpstan-deprecation-rules": "^1.0.0", - "phpstan/phpstan-phpunit": "^1.2.2", - "phpstan/phpstan-strict-rules": "^1.4.4", + "phpstan/phpstan": "^2.0", + "phpstan/phpstan-deprecation-rules": "^2.0.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpstan/phpstan-strict-rules": "^2.0", "phpunit/phpunit": "^8.5.31 || ^9.5.26", "webmozarts/strict-phpunit": "^7.5" }, @@ -6872,7 +8745,7 @@ ], "support": { "issues": "https://github.com/theofidry/cpu-core-counter/issues", - "source": "https://github.com/theofidry/cpu-core-counter/tree/1.2.0" + "source": "https://github.com/theofidry/cpu-core-counter/tree/1.3.0" }, "funding": [ { @@ -6880,20 +8753,20 @@ "type": "github" } ], - "time": "2024-08-06T10:04:20+00:00" + "time": "2025-08-14T07:29:31+00:00" }, { "name": "filp/whoops", - "version": "2.17.0", + "version": "2.18.4", "source": { "type": "git", "url": "https://github.com/filp/whoops.git", - "reference": "075bc0c26631110584175de6523ab3f1652eb28e" + "reference": "d2102955e48b9fd9ab24280a7ad12ed552752c4d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/filp/whoops/zipball/075bc0c26631110584175de6523ab3f1652eb28e", - "reference": "075bc0c26631110584175de6523ab3f1652eb28e", + "url": "https://api.github.com/repos/filp/whoops/zipball/d2102955e48b9fd9ab24280a7ad12ed552752c4d", + "reference": "d2102955e48b9fd9ab24280a7ad12ed552752c4d", "shasum": "" }, "require": { @@ -6943,7 +8816,7 @@ ], "support": { "issues": "https://github.com/filp/whoops/issues", - "source": "https://github.com/filp/whoops/tree/2.17.0" + "source": "https://github.com/filp/whoops/tree/2.18.4" }, "funding": [ { @@ -6951,24 +8824,24 @@ "type": "github" } ], - "time": "2025-01-25T12:00:00+00:00" + "time": "2025-08-08T12:00:00+00:00" }, { "name": "hamcrest/hamcrest-php", - "version": "v2.0.1", + "version": "v2.1.1", "source": { "type": "git", "url": "https://github.com/hamcrest/hamcrest-php.git", - "reference": "8c3d0a3f6af734494ad8f6fbbee0ba92422859f3" + "reference": "f8b1c0173b22fa6ec77a81fe63e5b01eba7e6487" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/hamcrest/hamcrest-php/zipball/8c3d0a3f6af734494ad8f6fbbee0ba92422859f3", - "reference": "8c3d0a3f6af734494ad8f6fbbee0ba92422859f3", + "url": "https://api.github.com/repos/hamcrest/hamcrest-php/zipball/f8b1c0173b22fa6ec77a81fe63e5b01eba7e6487", + "reference": "f8b1c0173b22fa6ec77a81fe63e5b01eba7e6487", "shasum": "" }, "require": { - "php": "^5.3|^7.0|^8.0" + "php": "^7.4|^8.0" }, "replace": { "cordoval/hamcrest-php": "*", @@ -6976,8 +8849,8 @@ "kodova/hamcrest-php": "*" }, "require-dev": { - "phpunit/php-file-iterator": "^1.4 || ^2.0", - "phpunit/phpunit": "^4.8.36 || ^5.7 || ^6.5 || ^7.0" + "phpunit/php-file-iterator": "^1.4 || ^2.0 || ^3.0", + "phpunit/phpunit": "^4.8.36 || ^5.7 || ^6.5 || ^7.0 || ^8.0 || ^9.0" }, "type": "library", "extra": { @@ -7000,22 +8873,63 @@ ], "support": { "issues": "https://github.com/hamcrest/hamcrest-php/issues", - "source": "https://github.com/hamcrest/hamcrest-php/tree/v2.0.1" + "source": "https://github.com/hamcrest/hamcrest-php/tree/v2.1.1" }, - "time": "2020-07-09T08:09:16+00:00" + "time": "2025-04-30T06:54:44+00:00" }, { - "name": "jean85/pretty-package-versions", - "version": "2.1.0", + "name": "iamcal/sql-parser", + "version": "v0.7", "source": { "type": "git", - "url": "https://github.com/Jean85/pretty-package-versions.git", - "reference": "3c4e5f62ba8d7de1734312e4fff32f67a8daaf10" + "url": "https://github.com/iamcal/SQLParser.git", + "reference": "610392f38de49a44dab08dc1659960a29874c4b8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Jean85/pretty-package-versions/zipball/3c4e5f62ba8d7de1734312e4fff32f67a8daaf10", - "reference": "3c4e5f62ba8d7de1734312e4fff32f67a8daaf10", + "url": "https://api.github.com/repos/iamcal/SQLParser/zipball/610392f38de49a44dab08dc1659960a29874c4b8", + "reference": "610392f38de49a44dab08dc1659960a29874c4b8", + "shasum": "" + }, + "require-dev": { + "php-coveralls/php-coveralls": "^1.0", + "phpunit/phpunit": "^5|^6|^7|^8|^9" + }, + "type": "library", + "autoload": { + "psr-4": { + "iamcal\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Cal Henderson", + "email": "cal@iamcal.com" + } + ], + "description": "MySQL schema parser", + "support": { + "issues": "https://github.com/iamcal/SQLParser/issues", + "source": "https://github.com/iamcal/SQLParser/tree/v0.7" + }, + "time": "2026-01-28T22:20:33+00:00" + }, + { + "name": "jean85/pretty-package-versions", + "version": "2.1.1", + "source": { + "type": "git", + "url": "https://github.com/Jean85/pretty-package-versions.git", + "reference": "4d7aa5dab42e2a76d99559706022885de0e18e1a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Jean85/pretty-package-versions/zipball/4d7aa5dab42e2a76d99559706022885de0e18e1a", + "reference": "4d7aa5dab42e2a76d99559706022885de0e18e1a", "shasum": "" }, "require": { @@ -7025,8 +8939,9 @@ "require-dev": { "friendsofphp/php-cs-fixer": "^3.2", "jean85/composer-provided-replaced-stub-package": "^1.0", - "phpstan/phpstan": "^1.4", + "phpstan/phpstan": "^2.0", "phpunit/phpunit": "^7.5|^8.5|^9.6", + "rector/rector": "^2.0", "vimeo/psalm": "^4.3 || ^5.0" }, "type": "library", @@ -7059,43 +8974,273 @@ ], "support": { "issues": "https://github.com/Jean85/pretty-package-versions/issues", - "source": "https://github.com/Jean85/pretty-package-versions/tree/2.1.0" + "source": "https://github.com/Jean85/pretty-package-versions/tree/2.1.1" }, - "time": "2024-11-18T16:19:46+00:00" + "time": "2025-03-19T14:43:43+00:00" }, { - "name": "laravel/pail", - "version": "v1.2.2", + "name": "larastan/larastan", + "version": "v3.9.3", "source": { "type": "git", - "url": "https://github.com/laravel/pail.git", - "reference": "f31f4980f52be17c4667f3eafe034e6826787db2" + "url": "https://github.com/larastan/larastan.git", + "reference": "64a52bcc5347c89fdf131cb59f96ebfbc8d1ad65" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/pail/zipball/f31f4980f52be17c4667f3eafe034e6826787db2", - "reference": "f31f4980f52be17c4667f3eafe034e6826787db2", + "url": "https://api.github.com/repos/larastan/larastan/zipball/64a52bcc5347c89fdf131cb59f96ebfbc8d1ad65", + "reference": "64a52bcc5347c89fdf131cb59f96ebfbc8d1ad65", + "shasum": "" + }, + "require": { + "ext-json": "*", + "iamcal/sql-parser": "^0.7.0", + "illuminate/console": "^11.44.2 || ^12.4.1 || ^13", + "illuminate/container": "^11.44.2 || ^12.4.1 || ^13", + "illuminate/contracts": "^11.44.2 || ^12.4.1 || ^13", + "illuminate/database": "^11.44.2 || ^12.4.1 || ^13", + "illuminate/http": "^11.44.2 || ^12.4.1 || ^13", + "illuminate/pipeline": "^11.44.2 || ^12.4.1 || ^13", + "illuminate/support": "^11.44.2 || ^12.4.1 || ^13", + "php": "^8.2", + "phpstan/phpstan": "^2.1.32" + }, + "require-dev": { + "doctrine/coding-standard": "^13", + "laravel/framework": "^11.44.2 || ^12.7.2 || ^13", + "mockery/mockery": "^1.6.12", + "nikic/php-parser": "^5.4", + "orchestra/canvas": "^v9.2.2 || ^10.0.1 || ^11", + "orchestra/testbench-core": "^9.12.0 || ^10.1 || ^11", + "phpstan/phpstan-deprecation-rules": "^2.0.1", + "phpunit/phpunit": "^10.5.35 || ^11.5.15 || ^12.5.8" + }, + "suggest": { + "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": { + "phpstan": { + "includes": [ + "extension.neon" + ] + }, + "branch-alias": { + "dev-master": "3.0-dev" + } + }, + "autoload": { + "psr-4": { + "Larastan\\Larastan\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Can Vural", + "email": "can9119@gmail.com" + } + ], + "description": "Larastan - Discover bugs in your code without running it. A phpstan/phpstan extension for Laravel", + "keywords": [ + "PHPStan", + "code analyse", + "code analysis", + "larastan", + "laravel", + "package", + "php", + "static analysis" + ], + "support": { + "issues": "https://github.com/larastan/larastan/issues", + "source": "https://github.com/larastan/larastan/tree/v3.9.3" + }, + "funding": [ + { + "url": "https://github.com/canvural", + "type": "github" + } + ], + "time": "2026-02-20T12:07:12+00:00" + }, + { + "name": "laravel/boost", + "version": "v2.3.1", + "source": { + "type": "git", + "url": "https://github.com/laravel/boost.git", + "reference": "ba0a9e6497398b6ce8243f5517b67d6761509150" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/boost/zipball/ba0a9e6497398b6ce8243f5517b67d6761509150", + "reference": "ba0a9e6497398b6ce8243f5517b67d6761509150", + "shasum": "" + }, + "require": { + "guzzlehttp/guzzle": "^7.9", + "illuminate/console": "^11.45.3|^12.41.1|^13.0", + "illuminate/contracts": "^11.45.3|^12.41.1|^13.0", + "illuminate/routing": "^11.45.3|^12.41.1|^13.0", + "illuminate/support": "^11.45.3|^12.41.1|^13.0", + "laravel/mcp": "^0.5.1|^0.6.0", + "laravel/prompts": "^0.3.10", + "laravel/roster": "^0.5.0", + "php": "^8.2" + }, + "require-dev": { + "laravel/pint": "^1.27.0", + "mockery/mockery": "^1.6.12", + "orchestra/testbench": "^9.15.0|^10.6|^11.0", + "pestphp/pest": "^2.36.0|^3.8.4|^4.1.5", + "phpstan/phpstan": "^2.1.27", + "rector/rector": "^2.1" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Laravel\\Boost\\BoostServiceProvider" + ] + }, + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Laravel\\Boost\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "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", + "dev", + "laravel" + ], + "support": { + "issues": "https://github.com/laravel/boost/issues", + "source": "https://github.com/laravel/boost" + }, + "time": "2026-03-12T09:06:47+00:00" + }, + { + "name": "laravel/mcp", + "version": "v0.6.2", + "source": { + "type": "git", + "url": "https://github.com/laravel/mcp.git", + "reference": "f696e44735b95ff275392eab8ce5a3b4b42a2223" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/mcp/zipball/f696e44735b95ff275392eab8ce5a3b4b42a2223", + "reference": "f696e44735b95ff275392eab8ce5a3b4b42a2223", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-mbstring": "*", + "illuminate/console": "^11.45.3|^12.41.1|^13.0", + "illuminate/container": "^11.45.3|^12.41.1|^13.0", + "illuminate/contracts": "^11.45.3|^12.41.1|^13.0", + "illuminate/http": "^11.45.3|^12.41.1|^13.0", + "illuminate/json-schema": "^12.41.1|^13.0", + "illuminate/routing": "^11.45.3|^12.41.1|^13.0", + "illuminate/support": "^11.45.3|^12.41.1|^13.0", + "illuminate/validation": "^11.45.3|^12.41.1|^13.0", + "php": "^8.2" + }, + "require-dev": { + "laravel/pint": "^1.20", + "orchestra/testbench": "^9.15|^10.8|^11.0", + "pestphp/pest": "^3.8.5|^4.3.2", + "phpstan/phpstan": "^2.1.27", + "rector/rector": "^2.2.4" + }, + "type": "library", + "extra": { + "laravel": { + "aliases": { + "Mcp": "Laravel\\Mcp\\Server\\Facades\\Mcp" + }, + "providers": [ + "Laravel\\Mcp\\Server\\McpServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "Laravel\\Mcp\\": "src/", + "Laravel\\Mcp\\Server\\": "src/Server/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "Rapidly build MCP servers for your Laravel applications.", + "homepage": "https://github.com/laravel/mcp", + "keywords": [ + "laravel", + "mcp" + ], + "support": { + "issues": "https://github.com/laravel/mcp/issues", + "source": "https://github.com/laravel/mcp" + }, + "time": "2026-03-10T20:00:23+00:00" + }, + { + "name": "laravel/pail", + "version": "v1.2.6", + "source": { + "type": "git", + "url": "https://github.com/laravel/pail.git", + "reference": "aa71a01c309e7f66bc2ec4fb1a59291b82eb4abf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/pail/zipball/aa71a01c309e7f66bc2ec4fb1a59291b82eb4abf", + "reference": "aa71a01c309e7f66bc2ec4fb1a59291b82eb4abf", "shasum": "" }, "require": { "ext-mbstring": "*", - "illuminate/console": "^10.24|^11.0|^12.0", - "illuminate/contracts": "^10.24|^11.0|^12.0", - "illuminate/log": "^10.24|^11.0|^12.0", - "illuminate/process": "^10.24|^11.0|^12.0", - "illuminate/support": "^10.24|^11.0|^12.0", + "illuminate/console": "^10.24|^11.0|^12.0|^13.0", + "illuminate/contracts": "^10.24|^11.0|^12.0|^13.0", + "illuminate/log": "^10.24|^11.0|^12.0|^13.0", + "illuminate/process": "^10.24|^11.0|^12.0|^13.0", + "illuminate/support": "^10.24|^11.0|^12.0|^13.0", "nunomaduro/termwind": "^1.15|^2.0", "php": "^8.2", - "symfony/console": "^6.0|^7.0" + "symfony/console": "^6.0|^7.0|^8.0" }, "require-dev": { - "laravel/framework": "^10.24|^11.0|^12.0", + "laravel/framework": "^10.24|^11.0|^12.0|^13.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", - "phpstan/phpstan": "^1.10", - "symfony/var-dumper": "^6.3|^7.0" + "orchestra/testbench-core": "^8.13|^9.17|^10.8|^11.0", + "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|^8.0", + "symfony/yaml": "^6.3|^7.0|^8.0" }, "type": "library", "extra": { @@ -7130,6 +9275,7 @@ "description": "Easily delve into your Laravel application's log files directly from the command line.", "homepage": "https://github.com/laravel/pail", "keywords": [ + "dev", "laravel", "logs", "php", @@ -7139,20 +9285,20 @@ "issues": "https://github.com/laravel/pail/issues", "source": "https://github.com/laravel/pail" }, - "time": "2025-01-28T15:15:15+00:00" + "time": "2026-02-09T13:44:54+00:00" }, { "name": "laravel/pint", - "version": "v1.21.0", + "version": "v1.29.0", "source": { "type": "git", "url": "https://github.com/laravel/pint.git", - "reference": "531fa0871fbde719c51b12afa3a443b8f4e4b425" + "reference": "bdec963f53172c5e36330f3a400604c69bf02d39" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/pint/zipball/531fa0871fbde719c51b12afa3a443b8f4e4b425", - "reference": "531fa0871fbde719c51b12afa3a443b8f4e4b425", + "url": "https://api.github.com/repos/laravel/pint/zipball/bdec963f53172c5e36330f3a400604c69bf02d39", + "reference": "bdec963f53172c5e36330f3a400604c69bf02d39", "shasum": "" }, "require": { @@ -7163,13 +9309,14 @@ "php": "^8.2.0" }, "require-dev": { - "friendsofphp/php-cs-fixer": "^3.68.5", - "illuminate/view": "^11.42.0", - "larastan/larastan": "^3.0.4", - "laravel-zero/framework": "^11.36.1", + "friendsofphp/php-cs-fixer": "^3.94.2", + "illuminate/view": "^12.54.1", + "larastan/larastan": "^3.9.3", + "laravel-zero/framework": "^12.0.5", "mockery/mockery": "^1.6.12", - "nunomaduro/termwind": "^2.3", - "pestphp/pest": "^2.36.0" + "nunomaduro/termwind": "^2.4.0", + "pestphp/pest": "^3.8.6", + "shipfastlabs/agent-detector": "^1.1.0" }, "bin": [ "builds/pint" @@ -7195,6 +9342,7 @@ "description": "An opinionated code formatter for PHP.", "homepage": "https://laravel.com", "keywords": [ + "dev", "format", "formatter", "lint", @@ -7205,33 +9353,94 @@ "issues": "https://github.com/laravel/pint/issues", "source": "https://github.com/laravel/pint" }, - "time": "2025-02-18T03:18:57+00:00" + "time": "2026-03-12T15:51:39+00:00" }, { - "name": "laravel/sail", - "version": "v1.41.0", + "name": "laravel/roster", + "version": "v0.5.1", "source": { "type": "git", - "url": "https://github.com/laravel/sail.git", - "reference": "fe1a4ada0abb5e4bd99eb4e4b0d87906c00cdeec" + "url": "https://github.com/laravel/roster.git", + "reference": "5089de7615f72f78e831590ff9d0435fed0102bb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/sail/zipball/fe1a4ada0abb5e4bd99eb4e4b0d87906c00cdeec", - "reference": "fe1a4ada0abb5e4bd99eb4e4b0d87906c00cdeec", + "url": "https://api.github.com/repos/laravel/roster/zipball/5089de7615f72f78e831590ff9d0435fed0102bb", + "reference": "5089de7615f72f78e831590ff9d0435fed0102bb", "shasum": "" }, "require": { - "illuminate/console": "^9.52.16|^10.0|^11.0|^12.0", - "illuminate/contracts": "^9.52.16|^10.0|^11.0|^12.0", - "illuminate/support": "^9.52.16|^10.0|^11.0|^12.0", - "php": "^8.0", - "symfony/console": "^6.0|^7.0", - "symfony/yaml": "^6.0|^7.0" + "illuminate/console": "^11.0|^12.0|^13.0", + "illuminate/contracts": "^11.0|^12.0|^13.0", + "illuminate/routing": "^11.0|^12.0|^13.0", + "illuminate/support": "^11.0|^12.0|^13.0", + "php": "^8.2", + "symfony/yaml": "^7.2|^8.0" }, "require-dev": { - "orchestra/testbench": "^7.0|^8.0|^9.0|^10.0", - "phpstan/phpstan": "^1.10" + "laravel/pint": "^1.14", + "mockery/mockery": "^1.6", + "orchestra/testbench": "^9.0|^10.0|^11.0", + "pestphp/pest": "^3.0|^4.1", + "phpstan/phpstan": "^2.0" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Laravel\\Roster\\RosterServiceProvider" + ] + }, + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Laravel\\Roster\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Detect packages & approaches in use within a Laravel project", + "homepage": "https://github.com/laravel/roster", + "keywords": [ + "dev", + "laravel" + ], + "support": { + "issues": "https://github.com/laravel/roster/issues", + "source": "https://github.com/laravel/roster" + }, + "time": "2026-03-05T07:58:43+00:00" + }, + { + "name": "laravel/sail", + "version": "v1.53.0", + "source": { + "type": "git", + "url": "https://github.com/laravel/sail.git", + "reference": "e340eaa2bea9b99192570c48ed837155dbf24fbb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/sail/zipball/e340eaa2bea9b99192570c48ed837155dbf24fbb", + "reference": "e340eaa2bea9b99192570c48ed837155dbf24fbb", + "shasum": "" + }, + "require": { + "illuminate/console": "^9.52.16|^10.0|^11.0|^12.0|^13.0", + "illuminate/contracts": "^9.52.16|^10.0|^11.0|^12.0|^13.0", + "illuminate/support": "^9.52.16|^10.0|^11.0|^12.0|^13.0", + "php": "^8.0", + "symfony/console": "^6.0|^7.0|^8.0", + "symfony/yaml": "^6.0|^7.0|^8.0" + }, + "require-dev": { + "orchestra/testbench": "^7.0|^8.0|^9.0|^10.0|^11.0", + "phpstan/phpstan": "^2.0" }, "bin": [ "bin/sail" @@ -7268,7 +9477,7 @@ "issues": "https://github.com/laravel/sail/issues", "source": "https://github.com/laravel/sail" }, - "time": "2025-01-24T15:45:36+00:00" + "time": "2026-02-06T12:16:02+00:00" }, { "name": "mockery/mockery", @@ -7355,16 +9564,16 @@ }, { "name": "myclabs/deep-copy", - "version": "1.13.0", + "version": "1.13.4", "source": { "type": "git", "url": "https://github.com/myclabs/DeepCopy.git", - "reference": "024473a478be9df5fdaca2c793f2232fe788e414" + "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/024473a478be9df5fdaca2c793f2232fe788e414", - "reference": "024473a478be9df5fdaca2c793f2232fe788e414", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/07d290f0c47959fd5eed98c95ee5602db07e0b6a", + "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a", "shasum": "" }, "require": { @@ -7403,7 +9612,7 @@ ], "support": { "issues": "https://github.com/myclabs/DeepCopy/issues", - "source": "https://github.com/myclabs/DeepCopy/tree/1.13.0" + "source": "https://github.com/myclabs/DeepCopy/tree/1.13.4" }, "funding": [ { @@ -7411,42 +9620,40 @@ "type": "tidelift" } ], - "time": "2025-02-12T12:17:51+00:00" + "time": "2025-08-01T08:46:24+00:00" }, { "name": "nunomaduro/collision", - "version": "v8.6.1", + "version": "v8.9.1", "source": { "type": "git", "url": "https://github.com/nunomaduro/collision.git", - "reference": "86f003c132143d5a2ab214e19933946409e0cae7" + "reference": "a1ed3fa530fd60bc515f9303e8520fcb7d4bd935" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nunomaduro/collision/zipball/86f003c132143d5a2ab214e19933946409e0cae7", - "reference": "86f003c132143d5a2ab214e19933946409e0cae7", + "url": "https://api.github.com/repos/nunomaduro/collision/zipball/a1ed3fa530fd60bc515f9303e8520fcb7d4bd935", + "reference": "a1ed3fa530fd60bc515f9303e8520fcb7d4bd935", "shasum": "" }, "require": { - "filp/whoops": "^2.16.0", - "nunomaduro/termwind": "^2.3.0", + "filp/whoops": "^2.18.4", + "nunomaduro/termwind": "^2.4.0", "php": "^8.2.0", - "symfony/console": "^7.2.1" + "symfony/console": "^7.4.4 || ^8.0.4" }, "conflict": { - "laravel/framework": "<11.39.1 || >=13.0.0", - "phpunit/phpunit": "<11.5.3 || >=12.0.0" + "laravel/framework": "<11.48.0 || >=14.0.0", + "phpunit/phpunit": "<11.5.50 || >=14.0.0" }, "require-dev": { - "larastan/larastan": "^2.9.12", - "laravel/framework": "^11.39.1", - "laravel/pint": "^1.20.0", - "laravel/sail": "^1.40.0", - "laravel/sanctum": "^4.0.7", - "laravel/tinker": "^2.10.0", - "orchestra/testbench-core": "^9.9.2", - "pestphp/pest": "^3.7.3", - "sebastian/environment": "^6.1.0 || ^7.2.0" + "brianium/paratest": "^7.8.5", + "larastan/larastan": "^3.9.2", + "laravel/framework": "^11.48.0 || ^12.52.0", + "laravel/pint": "^1.27.1", + "orchestra/testbench-core": "^9.12.0 || ^10.9.0", + "pestphp/pest": "^3.8.5 || ^4.4.1 || ^5.0.0", + "sebastian/environment": "^7.2.1 || ^8.0.3 || ^9.0.0" }, "type": "library", "extra": { @@ -7509,42 +9716,45 @@ "type": "patreon" } ], - "time": "2025-01-23T13:41:43+00:00" + "time": "2026-02-17T17:33:08+00:00" }, { "name": "pestphp/pest", - "version": "v3.7.4", + "version": "v4.4.2", "source": { "type": "git", "url": "https://github.com/pestphp/pest.git", - "reference": "4a987d3d5c4e3ba36c76fecbf56113baac2d1b2b" + "reference": "5d42e8fe3ae1d9fdf7c9f73ee88138fd30265701" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/pestphp/pest/zipball/4a987d3d5c4e3ba36c76fecbf56113baac2d1b2b", - "reference": "4a987d3d5c4e3ba36c76fecbf56113baac2d1b2b", + "url": "https://api.github.com/repos/pestphp/pest/zipball/5d42e8fe3ae1d9fdf7c9f73ee88138fd30265701", + "reference": "5d42e8fe3ae1d9fdf7c9f73ee88138fd30265701", "shasum": "" }, "require": { - "brianium/paratest": "^7.7.0", - "nunomaduro/collision": "^8.6.1", - "nunomaduro/termwind": "^2.3.0", - "pestphp/pest-plugin": "^3.0.0", - "pestphp/pest-plugin-arch": "^3.0.0", - "pestphp/pest-plugin-mutate": "^3.0.5", - "php": "^8.2.0", - "phpunit/phpunit": "^11.5.3" + "brianium/paratest": "^7.19.0", + "nunomaduro/collision": "^8.9.1", + "nunomaduro/termwind": "^2.4.0", + "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.12", + "symfony/process": "^7.4.5|^8.0.5" }, "conflict": { - "filp/whoops": "<2.16.0", - "phpunit/phpunit": ">11.5.3", - "sebastian/exporter": "<6.0.0", + "filp/whoops": "<2.18.3", + "phpunit/phpunit": ">12.5.12", + "sebastian/exporter": "<7.0.0", "webmozart/assert": "<1.11.0" }, "require-dev": { - "pestphp/pest-dev-tools": "^3.3.0", - "pestphp/pest-plugin-type-coverage": "^3.2.3", - "symfony/process": "^7.2.0" + "pestphp/pest-dev-tools": "^4.1.0", + "pestphp/pest-plugin-browser": "^4.3.0", + "pestphp/pest-plugin-type-coverage": "^4.0.3", + "psy/psysh": "^0.12.21" }, "bin": [ "bin/pest" @@ -7570,6 +9780,7 @@ "Pest\\Plugins\\Snapshot", "Pest\\Plugins\\Verbose", "Pest\\Plugins\\Version", + "Pest\\Plugins\\Shard", "Pest\\Plugins\\Parallel" ] }, @@ -7609,7 +9820,7 @@ ], "support": { "issues": "https://github.com/pestphp/pest/issues", - "source": "https://github.com/pestphp/pest/tree/v3.7.4" + "source": "https://github.com/pestphp/pest/tree/v4.4.2" }, "funding": [ { @@ -7621,34 +9832,34 @@ "type": "github" } ], - "time": "2025-01-23T14:03:29+00:00" + "time": "2026-03-10T21:09:12+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": { @@ -7675,7 +9886,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": [ { @@ -7691,30 +9902,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.0.0", + "version": "v4.0.0", "source": { "type": "git", "url": "https://github.com/pestphp/pest-plugin-arch.git", - "reference": "0a27e55a270cfe73d8cb70551b91002ee2cb64b0" + "reference": "25bb17e37920ccc35cbbcda3b00d596aadf3e58d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/pestphp/pest-plugin-arch/zipball/0a27e55a270cfe73d8cb70551b91002ee2cb64b0", - "reference": "0a27e55a270cfe73d8cb70551b91002ee2cb64b0", + "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.0.0", - "pestphp/pest-dev-tools": "^3.0.0" + "pestphp/pest": "^4.0.0", + "pestphp/pest-dev-tools": "^4.0.0" }, "type": "library", "extra": { @@ -7749,7 +9960,7 @@ "unit" ], "support": { - "source": "https://github.com/pestphp/pest-plugin-arch/tree/v3.0.0" + "source": "https://github.com/pestphp/pest-plugin-arch/tree/v4.0.0" }, "funding": [ { @@ -7761,30 +9972,30 @@ "type": "github" } ], - "time": "2024-09-08T23:23:55+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": { @@ -7814,7 +10025,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": [ { @@ -7830,31 +10041,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.1.0", + "version": "v4.1.0", "source": { "type": "git", "url": "https://github.com/pestphp/pest-plugin-laravel.git", - "reference": "1c4e994476375c72aa7aebaaa97aa98f5d5378cd" + "reference": "3057a36669ff11416cc0dc2b521b3aec58c488d0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/pestphp/pest-plugin-laravel/zipball/1c4e994476375c72aa7aebaaa97aa98f5d5378cd", - "reference": "1c4e994476375c72aa7aebaaa97aa98f5d5378cd", + "url": "https://api.github.com/repos/pestphp/pest-plugin-laravel/zipball/3057a36669ff11416cc0dc2b521b3aec58c488d0", + "reference": "3057a36669ff11416cc0dc2b521b3aec58c488d0", "shasum": "" }, "require": { - "laravel/framework": "^11.39.1|^12.0.0", - "pestphp/pest": "^3.7.4", - "php": "^8.2.0" + "laravel/framework": "^11.45.2|^12.52.0|^13.0", + "pestphp/pest": "^4.4.1", + "php": "^8.3.0" }, "require-dev": { - "laravel/dusk": "^8.2.13|dev-develop", - "orchestra/testbench": "^9.9.0|^10.0.0", - "pestphp/pest-dev-tools": "^3.3.0" + "laravel/dusk": "^8.3.6", + "orchestra/testbench": "^9.13.0|^10.9.0|^11.0", + "pestphp/pest-dev-tools": "^4.1.0" }, "type": "library", "extra": { @@ -7892,7 +10103,7 @@ "unit" ], "support": { - "source": "https://github.com/pestphp/pest-plugin-laravel/tree/v3.1.0" + "source": "https://github.com/pestphp/pest-plugin-laravel/tree/v4.1.0" }, "funding": [ { @@ -7904,32 +10115,32 @@ "type": "github" } ], - "time": "2025-01-24T13:22:39+00:00" + "time": "2026-02-21T00:29:45+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": { @@ -7942,6 +10153,10 @@ "MIT" ], "authors": [ + { + "name": "Nuno Maduro", + "email": "enunomaduro@gmail.com" + }, { "name": "Sandro Gehri", "email": "sandrogehri@gmail.com" @@ -7960,7 +10175,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": [ { @@ -7976,7 +10191,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", @@ -8096,71 +10367,18 @@ }, "time": "2022-02-21T01:04:05+00:00" }, - { - "name": "phpdocumentor/reflection-common", - "version": "2.2.0", - "source": { - "type": "git", - "url": "https://github.com/phpDocumentor/ReflectionCommon.git", - "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/1d01c49d4ed62f25aa84a747ad35d5a16924662b", - "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b", - "shasum": "" - }, - "require": { - "php": "^7.2 || ^8.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-2.x": "2.x-dev" - } - }, - "autoload": { - "psr-4": { - "phpDocumentor\\Reflection\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Jaap van Otterdijk", - "email": "opensource@ijaap.nl" - } - ], - "description": "Common reflection classes used by phpdocumentor to reflect the code structure", - "homepage": "http://www.phpdoc.org", - "keywords": [ - "FQSEN", - "phpDocumentor", - "phpdoc", - "reflection", - "static analysis" - ], - "support": { - "issues": "https://github.com/phpDocumentor/ReflectionCommon/issues", - "source": "https://github.com/phpDocumentor/ReflectionCommon/tree/2.x" - }, - "time": "2020-06-27T09:03:43+00:00" - }, { "name": "phpdocumentor/reflection-docblock", - "version": "5.6.1", + "version": "5.6.6", "source": { "type": "git", "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", - "reference": "e5e784149a09bd69d9a5e3b01c5cbd2e2bd653d8" + "reference": "5cee1d3dfc2d2aa6599834520911d246f656bcb8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/e5e784149a09bd69d9a5e3b01c5cbd2e2bd653d8", - "reference": "e5e784149a09bd69d9a5e3b01c5cbd2e2bd653d8", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/5cee1d3dfc2d2aa6599834520911d246f656bcb8", + "reference": "5cee1d3dfc2d2aa6599834520911d246f656bcb8", "shasum": "" }, "require": { @@ -8170,7 +10388,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", @@ -8209,146 +10427,93 @@ "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.1" + "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/5.6.6" }, - "time": "2024-12-07T09:39:29+00:00" + "time": "2025-12-22T21:13:58+00:00" }, { - "name": "phpdocumentor/type-resolver", - "version": "1.10.0", - "source": { - "type": "git", - "url": "https://github.com/phpDocumentor/TypeResolver.git", - "reference": "679e3ce485b99e84c775d28e2e96fade9a7fb50a" - }, + "name": "phpstan/phpstan", + "version": "2.1.40", "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/679e3ce485b99e84c775d28e2e96fade9a7fb50a", - "reference": "679e3ce485b99e84c775d28e2e96fade9a7fb50a", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/9b2c7aeb83a75d8680ea5e7c9b7fca88052b766b", + "reference": "9b2c7aeb83a75d8680ea5e7c9b7fca88052b766b", "shasum": "" }, "require": { - "doctrine/deprecations": "^1.0", - "php": "^7.3 || ^8.0", - "phpdocumentor/reflection-common": "^2.0", - "phpstan/phpdoc-parser": "^1.18|^2.0" + "php": "^7.4|^8.0" }, - "require-dev": { - "ext-tokenizer": "*", - "phpbench/phpbench": "^1.2", - "phpstan/extension-installer": "^1.1", - "phpstan/phpstan": "^1.8", - "phpstan/phpstan-phpunit": "^1.1", - "phpunit/phpunit": "^9.5", - "rector/rector": "^0.13.9", - "vimeo/psalm": "^4.25" + "conflict": { + "phpstan/phpstan-shim": "*" }, + "bin": [ + "phpstan", + "phpstan.phar" + ], "type": "library", - "extra": { - "branch-alias": { - "dev-1.x": "1.x-dev" - } - }, "autoload": { - "psr-4": { - "phpDocumentor\\Reflection\\": "src" - } + "files": [ + "bootstrap.php" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" ], - "authors": [ + "description": "PHPStan - PHP Static Analysis Tool", + "keywords": [ + "dev", + "static analysis" + ], + "support": { + "docs": "https://phpstan.org/user-guide/getting-started", + "forum": "https://github.com/phpstan/phpstan/discussions", + "issues": "https://github.com/phpstan/phpstan/issues", + "security": "https://github.com/phpstan/phpstan/security/policy", + "source": "https://github.com/phpstan/phpstan-src" + }, + "funding": [ { - "name": "Mike van Riel", - "email": "me@mikevanriel.com" + "url": "https://github.com/ondrejmirtes", + "type": "github" + }, + { + "url": "https://github.com/phpstan", + "type": "github" } ], - "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" - }, - "time": "2024-11-09T15:12:26+00:00" - }, - { - "name": "phpstan/phpdoc-parser", - "version": "2.1.0", - "source": { - "type": "git", - "url": "https://github.com/phpstan/phpdoc-parser.git", - "reference": "9b30d6fd026b2c132b3985ce6b23bec09ab3aa68" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/9b30d6fd026b2c132b3985ce6b23bec09ab3aa68", - "reference": "9b30d6fd026b2c132b3985ce6b23bec09ab3aa68", - "shasum": "" - }, - "require": { - "php": "^7.4 || ^8.0" - }, - "require-dev": { - "doctrine/annotations": "^2.0", - "nikic/php-parser": "^5.3.0", - "php-parallel-lint/php-parallel-lint": "^1.2", - "phpstan/extension-installer": "^1.0", - "phpstan/phpstan": "^2.0", - "phpstan/phpstan-phpunit": "^2.0", - "phpstan/phpstan-strict-rules": "^2.0", - "phpunit/phpunit": "^9.6", - "symfony/process": "^5.2" - }, - "type": "library", - "autoload": { - "psr-4": { - "PHPStan\\PhpDocParser\\": [ - "src/" - ] - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "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.1.0" - }, - "time": "2025-02-19T13:28:12+00:00" + "time": "2026-02-23T15:04:35+00:00" }, { "name": "phpunit/php-code-coverage", - "version": "11.0.9", + "version": "12.5.3", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "14d63fbcca18457e49c6f8bebaa91a87e8e188d7" + "reference": "b015312f28dd75b75d3422ca37dff2cd1a565e8d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/14d63fbcca18457e49c6f8bebaa91a87e8e188d7", - "reference": "14d63fbcca18457e49c6f8bebaa91a87e8e188d7", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/b015312f28dd75b75d3422ca37dff2cd1a565e8d", + "reference": "b015312f28dd75b75d3422ca37dff2cd1a565e8d", "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", @@ -8357,7 +10522,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "11.0.x-dev" + "dev-main": "12.5.x-dev" } }, "autoload": { @@ -8386,40 +10551,52 @@ "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/11.0.9" + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/12.5.3" }, "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/phpunit/php-code-coverage", + "type": "tidelift" } ], - "time": "2025-02-25T13:26:39+00:00" + "time": "2026-02-06T06:01:44+00:00" }, { "name": "phpunit/php-file-iterator", - "version": "5.1.0", + "version": "6.0.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-file-iterator.git", - "reference": "118cfaaa8bc5aef3287bf315b6060b1174754af6" + "reference": "3d1cd096ef6bea4bf2762ba586e35dbd317cbfd5" }, "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/3d1cd096ef6bea4bf2762ba586e35dbd317cbfd5", + "reference": "3d1cd096ef6bea4bf2762ba586e35dbd317cbfd5", "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": { @@ -8447,36 +10624,48 @@ "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.1" }, "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/phpunit/php-file-iterator", + "type": "tidelift" } ], - "time": "2024-08-27T05:02:59+00:00" + "time": "2026-02-02T14:04:18+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": "*" @@ -8484,7 +10673,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "5.0-dev" + "dev-main": "6.0-dev" } }, "autoload": { @@ -8511,7 +10700,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": [ { @@ -8519,32 +10708,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": { @@ -8571,7 +10760,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": [ { @@ -8579,32 +10768,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": { @@ -8631,7 +10820,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": [ { @@ -8639,20 +10828,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.3", + "version": "12.5.12", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "30e319e578a7b5da3543073e30002bf82042f701" + "reference": "418e06b3b46b0d54bad749ff4907fc7dfb530199" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/30e319e578a7b5da3543073e30002bf82042f701", - "reference": "30e319e578a7b5da3543073e30002bf82042f701", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/418e06b3b46b0d54bad749ff4907fc7dfb530199", + "reference": "418e06b3b46b0d54bad749ff4907fc7dfb530199", "shasum": "" }, "require": { @@ -8662,37 +10851,34 @@ "ext-mbstring": "*", "ext-xml": "*", "ext-xmlwriter": "*", - "myclabs/deep-copy": "^1.12.1", + "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.8", - "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.2", - "sebastian/comparator": "^6.3.0", - "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.0", - "sebastian/version": "^5.0.2", + "php": ">=8.3", + "phpunit/php-code-coverage": "^12.5.3", + "phpunit/php-file-iterator": "^6.0.1", + "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.4", + "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/recursion-context": "^7.0.1", + "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": { @@ -8724,7 +10910,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.3" + "source": "https://github.com/sebastianbergmann/phpunit/tree/12.5.12" }, "funding": [ { @@ -8735,37 +10921,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-01-13T09:36:00+00:00" + "time": "2026-02-16T08:34:36+00:00" }, { - "name": "sebastian/cli-parser", - "version": "3.0.2", + "name": "rector/rector", + "version": "2.3.8", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/cli-parser.git", - "reference": "15c5dd40dc4f38794d383bb95465193f5e0ae180" + "url": "https://github.com/rectorphp/rector.git", + "reference": "bbd37aedd8df749916cffa2a947cfc4714d1ba2c" }, "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/bbd37aedd8df749916cffa2a947cfc4714d1ba2c", + "reference": "bbd37aedd8df749916cffa2a947cfc4714d1ba2c", "shasum": "" }, "require": { - "php": ">=8.2" + "php": "^7.4|^8.0", + "phpstan/phpstan": "^2.1.38" + }, + "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.8" + }, + "funding": [ + { + "url": "https://github.com/tomasvotruba", + "type": "github" + } + ], + "time": "2026-02-22T09:45:50+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": { @@ -8789,152 +11043,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.2", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/code-unit.git", - "reference": "ee88b0cdbe74cf8dd3b54940ff17643c0d6543ca" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/ee88b0cdbe74cf8dd3b54940ff17643c0d6543ca", - "reference": "ee88b0cdbe74cf8dd3b54940ff17643c0d6543ca", - "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.2" - }, - "funding": [ + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2024-12-12T09:59:06+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.1", + "version": "7.1.4", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/comparator.git", - "reference": "24b8fbc2c8e201bb1308e7b05148d6ab393b6959" + "reference": "6a7de5df2e094f9a80b40a522391a7e6022df5f6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/24b8fbc2c8e201bb1308e7b05148d6ab393b6959", - "reference": "24b8fbc2c8e201bb1308e7b05148d6ab393b6959", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/6a7de5df2e094f9a80b40a522391a7e6022df5f6", + "reference": "6a7de5df2e094f9a80b40a522391a7e6022df5f6", "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" @@ -8942,7 +11095,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "6.3-dev" + "dev-main": "7.1-dev" } }, "autoload": { @@ -8982,41 +11135,53 @@ "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.1" + "source": "https://github.com/sebastianbergmann/comparator/tree/7.1.4" }, "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/comparator", + "type": "tidelift" } ], - "time": "2025-03-07T06:57:01+00:00" + "time": "2026-01-24T09:28:48+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": { @@ -9040,7 +11205,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": [ { @@ -9048,33 +11213,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": { @@ -9107,7 +11272,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": [ { @@ -9115,27 +11280,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.0", + "version": "8.0.3", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/environment.git", - "reference": "855f3ae0ab316bbafe1ba4e16e9f3c078d24a0c5" + "reference": "24a711b5c916efc6d6e62aa65aa2ec98fef77f68" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/855f3ae0ab316bbafe1ba4e16e9f3c078d24a0c5", - "reference": "855f3ae0ab316bbafe1ba4e16e9f3c078d24a0c5", + "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.0" + "phpunit/phpunit": "^12.0" }, "suggest": { "ext-posix": "*" @@ -9143,7 +11308,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "7.2-dev" + "dev-main": "8.0-dev" } }, "autoload": { @@ -9171,42 +11336,54 @@ "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.0" + "source": "https://github.com/sebastianbergmann/environment/tree/8.0.3" }, "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/environment", + "type": "tidelift" } ], - "time": "2024-07-03T04:54:44+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": { @@ -9249,43 +11426,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": { @@ -9311,41 +11500,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": { @@ -9369,7 +11570,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": [ { @@ -9377,34 +11578,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": { @@ -9427,7 +11628,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": [ { @@ -9435,32 +11636,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": { @@ -9483,7 +11684,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": [ { @@ -9491,32 +11692,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.2", + "version": "7.0.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/recursion-context.git", - "reference": "694d156164372abbd149a4b85ccda2e4670c0e16" + "reference": "0b01998a7d5b1f122911a66bebcb8d46f0c82d8c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/694d156164372abbd149a4b85ccda2e4670c0e16", - "reference": "694d156164372abbd149a4b85ccda2e4670c0e16", + "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.0" + "phpunit/phpunit": "^12.0" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "6.0-dev" + "dev-main": "7.0-dev" } }, "autoload": { @@ -9547,40 +11748,52 @@ "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.2" + "source": "https://github.com/sebastianbergmann/recursion-context/tree/7.0.1" }, "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/recursion-context", + "type": "tidelift" } ], - "time": "2024-07-03T05:10:34+00:00" + "time": "2025-08-13T04:44:59+00:00" }, { "name": "sebastian/type", - "version": "5.1.0", + "version": "6.0.3", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/type.git", - "reference": "461b9c5da241511a2a0e8f240814fb23ce5c0aac" + "reference": "e549163b9760b8f71f191651d22acf32d56d6d4d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/461b9c5da241511a2a0e8f240814fb23ce5c0aac", - "reference": "461b9c5da241511a2a0e8f240814fb23ce5c0aac", + "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": { @@ -9604,37 +11817,49 @@ "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.0" + "source": "https://github.com/sebastianbergmann/type/tree/6.0.3" }, "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/type", + "type": "tidelift" } ], - "time": "2024-09-17T13:12:04+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": { @@ -9658,7 +11883,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": [ { @@ -9666,7 +11891,7 @@ "type": "github" } ], - "time": "2024-10-09T05:16:32+00:00" + "time": "2025-02-07T05:00:38+00:00" }, { "name": "staabm/side-effects-detector", @@ -9720,98 +11945,26 @@ ], "time": "2024-10-20T05:08:20+00:00" }, - { - "name": "symfony/yaml", - "version": "v7.2.3", - "source": { - "type": "git", - "url": "https://github.com/symfony/yaml.git", - "reference": "ac238f173df0c9c1120f862d0f599e17535a87ec" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/ac238f173df0c9c1120f862d0f599e17535a87ec", - "reference": "ac238f173df0c9c1120f862d0f599e17535a87ec", - "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.2.3" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2025-01-07T12:55:42+00:00" - }, { "name": "ta-tikoma/phpunit-architecture-test", - "version": "0.8.4", + "version": "0.8.7", "source": { "type": "git", "url": "https://github.com/ta-tikoma/phpunit-architecture-test.git", - "reference": "89f0dea1cb0f0d5744d3ec1764a286af5e006636" + "reference": "1248f3f506ca9641d4f68cebcd538fa489754db8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/ta-tikoma/phpunit-architecture-test/zipball/89f0dea1cb0f0d5744d3ec1764a286af5e006636", - "reference": "89f0dea1cb0f0d5744d3ec1764a286af5e006636", + "url": "https://api.github.com/repos/ta-tikoma/phpunit-architecture-test/zipball/1248f3f506ca9641d4f68cebcd538fa489754db8", + "reference": "1248f3f506ca9641d4f68cebcd538fa489754db8", "shasum": "" }, "require": { "nikic/php-parser": "^4.18.0 || ^5.0.0", "php": "^8.1.0", - "phpdocumentor/reflection-docblock": "^5.3.0", - "phpunit/phpunit": "^10.5.5 || ^11.0.0", - "symfony/finder": "^6.4.0 || ^7.0.0" + "phpdocumentor/reflection-docblock": "^5.3.0 || ^6.0.0", + "phpunit/phpunit": "^10.5.5 || ^11.0.0 || ^12.0.0 || ^13.0.0", + "symfony/finder": "^6.4.0 || ^7.0.0 || ^8.0.0" }, "require-dev": { "laravel/pint": "^1.13.7", @@ -9847,29 +12000,29 @@ ], "support": { "issues": "https://github.com/ta-tikoma/phpunit-architecture-test/issues", - "source": "https://github.com/ta-tikoma/phpunit-architecture-test/tree/0.8.4" + "source": "https://github.com/ta-tikoma/phpunit-architecture-test/tree/0.8.7" }, - "time": "2024-01-05T14:10:56+00:00" + "time": "2026-02-17T17:25:14+00:00" }, { "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": { @@ -9891,7 +12044,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": [ { @@ -9899,7 +12052,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.6", + "source": { + "type": "git", + "url": "https://github.com/webmozarts/assert.git", + "reference": "ff31ad6efc62e66e518fbab1cde3453d389bcdc8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/webmozarts/assert/zipball/ff31ad6efc62e66e518fbab1cde3453d389bcdc8", + "reference": "ff31ad6efc62e66e518fbab1cde3453d389bcdc8", + "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.6" + }, + "time": "2026-02-27T10:28:38+00:00" } ], "aliases": [], @@ -9908,9 +12123,11 @@ "prefer-stable": true, "prefer-lowest": false, "platform": { - "php": "^8.2", - "ext-imagick": "*" + "php": "^8.4", + "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 92fe477..176342e 100644 --- a/config/app.php +++ b/config/app.php @@ -13,7 +13,7 @@ return [ | */ - 'name' => env('APP_NAME', 'Laravel'), + 'name' => env('APP_NAME', 'LaraPaper'), /* |-------------------------------------------------------------------------- @@ -127,7 +127,34 @@ return [ 'enabled' => env('REGISTRATION_ENABLED', true), ], + 'pixel_logo_enabled' => env('PIXELLOGO_ENABLED', true), + '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', true), + 'puppeteer_window_size_strategy' => env('PUPPETEER_WINDOW_SIZE_STRATEGY', 'v2'), + 'notifications' => [ + 'battery_low' => [ + 'warn_at_percent' => env('NOTIFICATION_BATTERYLOW_WARNATPERCENT', 20), + ], + ], + + /* + |-------------------------------------------------------------------------- + | Application Version + |-------------------------------------------------------------------------- + | + | This value is the version of your application, which will be used when + | displaying the version number in the UI. This is set during the Docker + | build process from the release tag. + | + */ + + 'version' => env('APP_VERSION', null), + + 'catalog_url' => env('CATALOG_URL', 'https://raw.githubusercontent.com/bnussbau/trmnl-recipe-catalog/refs/heads/main/catalog.yaml'), + + 'github_repo' => env('GITHUB_REPO', 'usetrmnl/larapaper'), ]; diff --git a/config/auth.php b/config/auth.php index 0ba5d5d..7d1eb0d 100644 --- a/config/auth.php +++ b/config/auth.php @@ -104,7 +104,7 @@ return [ | Password Confirmation Timeout |-------------------------------------------------------------------------- | - | Here you may define the amount of seconds before a password confirmation + | Here you may define the number of seconds before a password confirmation | window expires and users are asked to re-enter their password via the | confirmation screen. By default, the timeout lasts for three hours. | diff --git a/config/cache.php b/config/cache.php index 925f7d2..b32aead 100644 --- a/config/cache.php +++ b/config/cache.php @@ -27,7 +27,8 @@ return [ | same cache driver to group types of items stored in your caches. | | Supported drivers: "array", "database", "file", "memcached", - | "redis", "dynamodb", "octane", "null" + | "redis", "dynamodb", "octane", + | "failover", "null" | */ @@ -90,6 +91,14 @@ return [ 'driver' => 'octane', ], + 'failover' => [ + 'driver' => 'failover', + 'stores' => [ + 'database', + 'array', + ], + ], + ], /* @@ -103,6 +112,6 @@ return [ | */ - 'prefix' => env('CACHE_PREFIX', Str::slug(env('APP_NAME', 'laravel'), '_').'_cache_'), + 'prefix' => env('CACHE_PREFIX', Str::slug((string) env('APP_NAME', 'laravel')).'-cache-'), ]; diff --git a/config/database.php b/config/database.php index 8910562..4da9dc6 100644 --- a/config/database.php +++ b/config/database.php @@ -40,6 +40,7 @@ return [ 'busy_timeout' => null, 'journal_mode' => null, 'synchronous' => null, + 'transaction_mode' => 'DEFERRED', ], 'mysql' => [ @@ -58,7 +59,7 @@ return [ 'strict' => true, 'engine' => null, 'options' => extension_loaded('pdo_mysql') ? array_filter([ - PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'), + (PHP_VERSION_ID >= 80500 ? Pdo\Mysql::ATTR_SSL_CA : PDO::MYSQL_ATTR_SSL_CA) => env('MYSQL_ATTR_SSL_CA'), ]) : [], ], @@ -78,7 +79,7 @@ return [ 'strict' => true, 'engine' => null, 'options' => extension_loaded('pdo_mysql') ? array_filter([ - PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'), + (PHP_VERSION_ID >= 80500 ? Pdo\Mysql::ATTR_SSL_CA : PDO::MYSQL_ATTR_SSL_CA) => env('MYSQL_ATTR_SSL_CA'), ]) : [], ], @@ -94,7 +95,7 @@ return [ 'prefix' => '', 'prefix_indexes' => true, 'search_path' => 'public', - 'sslmode' => 'prefer', + 'sslmode' => env('DB_SSLMODE', 'prefer'), ], 'sqlsrv' => [ @@ -147,7 +148,7 @@ return [ 'options' => [ 'cluster' => env('REDIS_CLUSTER', 'redis'), - 'prefix' => env('REDIS_PREFIX', Str::slug(env('APP_NAME', 'laravel'), '_').'_database_'), + 'prefix' => env('REDIS_PREFIX', Str::slug((string) env('APP_NAME', 'laravel')).'-database-'), 'persistent' => env('REDIS_PERSISTENT', false), ], @@ -158,6 +159,10 @@ return [ 'password' => env('REDIS_PASSWORD'), 'port' => env('REDIS_PORT', '6379'), 'database' => env('REDIS_DB', '0'), + 'max_retries' => env('REDIS_MAX_RETRIES', 3), + 'backoff_algorithm' => env('REDIS_BACKOFF_ALGORITHM', 'decorrelated_jitter'), + 'backoff_base' => env('REDIS_BACKOFF_BASE', 100), + 'backoff_cap' => env('REDIS_BACKOFF_CAP', 1000), ], 'cache' => [ @@ -167,6 +172,10 @@ return [ 'password' => env('REDIS_PASSWORD'), 'port' => env('REDIS_PORT', '6379'), 'database' => env('REDIS_CACHE_DB', '1'), + 'max_retries' => env('REDIS_MAX_RETRIES', 3), + 'backoff_algorithm' => env('REDIS_BACKOFF_ALGORITHM', 'decorrelated_jitter'), + 'backoff_base' => env('REDIS_BACKOFF_BASE', 100), + 'backoff_cap' => env('REDIS_BACKOFF_CAP', 1000), ], ], diff --git a/config/filesystems.php b/config/filesystems.php index b564035..ccaf2a9 100644 --- a/config/filesystems.php +++ b/config/filesystems.php @@ -35,14 +35,16 @@ return [ 'root' => storage_path('app/private'), 'serve' => true, 'throw' => false, + 'report' => false, ], 'public' => [ 'driver' => 'local', 'root' => storage_path('app/public'), - 'url' => env('APP_URL').'/storage', + 'url' => mb_rtrim(env('APP_URL'), '/').'/storage', 'visibility' => 'public', 'throw' => false, + 'report' => false, ], 's3' => [ @@ -55,6 +57,7 @@ return [ 'endpoint' => env('AWS_ENDPOINT'), 'use_path_style_endpoint' => env('AWS_USE_PATH_STYLE_ENDPOINT', false), 'throw' => false, + 'report' => false, ], ], diff --git a/config/fortify.php b/config/fortify.php new file mode 100644 index 0000000..ed7b0c0 --- /dev/null +++ b/config/fortify.php @@ -0,0 +1,159 @@ + 'web', + + /* + |-------------------------------------------------------------------------- + | Fortify Password Broker + |-------------------------------------------------------------------------- + | + | Here you may specify which password broker Fortify can use when a user + | is resetting their password. This configured value should match one + | of your password brokers setup in your "auth" configuration file. + | + */ + + 'passwords' => 'users', + + /* + |-------------------------------------------------------------------------- + | Username / Email + |-------------------------------------------------------------------------- + | + | This value defines which model attribute should be considered as your + | application's "username" field. Typically, this might be the email + | address of the users but you are free to change this value here. + | + | Out of the box, Fortify expects forgot password and reset password + | requests to have a field named 'email'. If the application uses + | another name for the field you may define it below as needed. + | + */ + + 'username' => 'email', + + 'email' => 'email', + + /* + |-------------------------------------------------------------------------- + | Lowercase Usernames + |-------------------------------------------------------------------------- + | + | This value defines whether usernames should be lowercased before saving + | them in the database, as some database system string fields are case + | sensitive. You may disable this for your application if necessary. + | + */ + + 'lowercase_usernames' => true, + + /* + |-------------------------------------------------------------------------- + | Home Path + |-------------------------------------------------------------------------- + | + | Here you may configure the path where users will get redirected during + | authentication or password reset when the operations are successful + | and the user is authenticated. You are free to change this value. + | + */ + + 'home' => '/dashboard', + + /* + |-------------------------------------------------------------------------- + | Fortify Routes Prefix / Subdomain + |-------------------------------------------------------------------------- + | + | Here you may specify which prefix Fortify will assign to all the routes + | that it registers with the application. If necessary, you may change + | subdomain under which all of the Fortify routes will be available. + | + */ + + 'prefix' => '', + + 'domain' => null, + + /* + |-------------------------------------------------------------------------- + | Fortify Routes Middleware + |-------------------------------------------------------------------------- + | + | Here you may specify which middleware Fortify will assign to the routes + | that it registers with the application. If necessary, you may change + | these middleware but typically this provided default is preferred. + | + */ + + 'middleware' => ['web'], + + /* + |-------------------------------------------------------------------------- + | Rate Limiting + |-------------------------------------------------------------------------- + | + | By default, Fortify will throttle logins to five requests per minute for + | every email and IP address combination. However, if you would like to + | specify a custom rate limiter to call then you may specify it here. + | + */ + + 'limiters' => [ + 'login' => 'login', + 'two-factor' => 'two-factor', + ], + + /* + |-------------------------------------------------------------------------- + | Register View Routes + |-------------------------------------------------------------------------- + | + | Here you may specify if the routes returning views should be disabled as + | you may not need them when building your own application. This may be + | especially true if you're writing a custom single-page application. + | + */ + + 'views' => true, + + /* + |-------------------------------------------------------------------------- + | Features + |-------------------------------------------------------------------------- + | + | Some of the Fortify features are optional. You may disable the features + | by removing them from this array. You're free to only remove some of + | these features or you can even remove all of these if you need to. + | + */ + + 'features' => [ + config('app.registration.enabled') && Features::registration(), + Features::resetPasswords(), + Features::emailVerification(), + // Features::updateProfileInformation(), + // Features::updatePasswords(), + Features::twoFactorAuthentication([ + 'confirm' => true, + 'confirmPassword' => true, + // 'window' => 0, + ]), + ], + +]; diff --git a/config/livewire.php b/config/livewire.php new file mode 100644 index 0000000..4c68f45 --- /dev/null +++ b/config/livewire.php @@ -0,0 +1,277 @@ + [ + resource_path('views/components'), + resource_path('views/livewire'), + ], + + /* + |--------------------------------------------------------------------------- + | Component Namespaces + |--------------------------------------------------------------------------- + | + | This value sets default namespaces that will be used to resolve view-based + | components like single-file and multi-file components. These folders'll + | also be referenced when creating new components via the make command. + | + */ + + 'component_namespaces' => [ + 'layouts' => resource_path('views/layouts'), + 'pages' => resource_path('views/pages'), + ], + + /* + |--------------------------------------------------------------------------- + | Page Layout + |--------------------------------------------------------------------------- + | The view that will be used as the layout when rendering a single component as + | an entire page via `Route::livewire('/post/create', 'pages::create-post')`. + | In this case, the content of pages::create-post will render into $slot. + | + */ + + 'component_layout' => 'layouts::app', + + /* + |--------------------------------------------------------------------------- + | Lazy Loading Placeholder + |--------------------------------------------------------------------------- + | Livewire allows you to lazy load components that would otherwise slow down + | the initial page load. Every component can have a custom placeholder or + | you can define the default placeholder view for all components below. + | + */ + + 'component_placeholder' => null, // Example: 'placeholders::skeleton' + + /* + |--------------------------------------------------------------------------- + | Make Command + |--------------------------------------------------------------------------- + | This value determines the default configuration for the artisan make command + | You can configure the component type (sfc, mfc, class) and whether to use + | the high-voltage (⚑) emoji as a prefix in the sfc|mfc component names. + | + */ + + 'make_command' => [ + 'type' => 'sfc', // Options: 'sfc', 'mfc', 'class' + 'emoji' => false, // Options: true, false + ], + + /* + |--------------------------------------------------------------------------- + | Class Namespace + |--------------------------------------------------------------------------- + | + | This value sets the root class namespace for Livewire component classes in + | your application. This value will change where component auto-discovery + | finds components. It's also referenced by the file creation commands. + | + */ + + 'class_namespace' => 'App\\Livewire', + + /* + |--------------------------------------------------------------------------- + | Class Path + |--------------------------------------------------------------------------- + | + | This value is used to specify the path where Livewire component class files + | are created when running creation commands like `artisan make:livewire`. + | This path is customizable to match your projects directory structure. + | + */ + + 'class_path' => app_path('Livewire'), + + /* + |--------------------------------------------------------------------------- + | View Path + |--------------------------------------------------------------------------- + | + | This value is used to specify where Livewire component Blade templates are + | stored when running file creation commands like `artisan make:livewire`. + | It is also used if you choose to omit a component's render() method. + | + */ + + 'view_path' => resource_path('views/livewire'), + + /* + |--------------------------------------------------------------------------- + | Temporary File Uploads + |--------------------------------------------------------------------------- + | + | Livewire handles file uploads by storing uploads in a temporary directory + | before the file is stored permanently. All file uploads are directed to + | a global endpoint for temporary storage. You may configure this below: + | + */ + + 'temporary_file_upload' => [ + 'disk' => env('LIVEWIRE_TEMPORARY_FILE_UPLOAD_DISK'), // Example: 'local', 's3' | Default: 'default' + 'rules' => null, // Example: ['file', 'mimes:png,jpg'] | Default: ['required', 'file', 'max:12288'] (12MB) + 'directory' => null, // Example: 'tmp' | Default: 'livewire-tmp' + 'middleware' => null, // Example: 'throttle:5,1' | Default: 'throttle:60,1' + 'preview_mimes' => [ // Supported file types for temporary pre-signed file URLs... + 'png', 'gif', 'bmp', 'svg', 'wav', 'mp4', + 'mov', 'avi', 'wmv', 'mp3', 'm4a', + 'jpg', 'jpeg', 'mpga', 'webp', 'wma', + ], + 'max_upload_time' => 5, // Max duration (in minutes) before an upload is invalidated... + 'cleanup' => true, // Should cleanup temporary uploads older than 24 hrs... + ], + + /* + |--------------------------------------------------------------------------- + | Render On Redirect + |--------------------------------------------------------------------------- + | + | This value determines if Livewire will run a component's `render()` method + | after a redirect has been triggered using something like `redirect(...)` + | Setting this to true will render the view once more before redirecting + | + */ + + 'render_on_redirect' => false, + + /* + |--------------------------------------------------------------------------- + | Eloquent Model Binding + |--------------------------------------------------------------------------- + | + | Previous versions of Livewire supported binding directly to eloquent model + | properties using wire:model by default. However, this behavior has been + | deemed too "magical" and has therefore been put under a feature flag. + | + */ + + 'legacy_model_binding' => false, + + /* + |--------------------------------------------------------------------------- + | Auto-inject Frontend Assets + |--------------------------------------------------------------------------- + | + | By default, Livewire automatically injects its JavaScript and CSS into the + | and of pages containing Livewire components. By disabling + | this behavior, you need to use @livewireStyles and @livewireScripts. + | + */ + + 'inject_assets' => true, + + /* + |--------------------------------------------------------------------------- + | Navigate (SPA mode) + |--------------------------------------------------------------------------- + | + | By adding `wire:navigate` to links in your Livewire application, Livewire + | will prevent the default link handling and instead request those pages + | via AJAX, creating an SPA-like effect. Configure this behavior here. + | + */ + + 'navigate' => [ + 'show_progress_bar' => true, + 'progress_bar_color' => '#E05B45', + ], + + /* + |--------------------------------------------------------------------------- + | HTML Morph Markers + |--------------------------------------------------------------------------- + | + | Livewire intelligently "morphs" existing HTML into the newly rendered HTML + | after each update. To make this process more reliable, Livewire injects + | "markers" into the rendered Blade surrounding @if, @class & @foreach. + | + */ + + 'inject_morph_markers' => true, + + /* + |--------------------------------------------------------------------------- + | Smart Wire Keys + |--------------------------------------------------------------------------- + | + | Livewire uses loops and keys used within loops to generate smart keys that + | are applied to nested components that don't have them. This makes using + | nested components more reliable by ensuring that they all have keys. + | + */ + + 'smart_wire_keys' => true, + + /* + |--------------------------------------------------------------------------- + | Pagination Theme + |--------------------------------------------------------------------------- + | + | When enabling Livewire's pagination feature by using the `WithPagination` + | trait, Livewire will use Tailwind templates to render pagination views + | on the page. If you want Bootstrap CSS, you can specify: "bootstrap" + | + */ + + 'pagination_theme' => 'tailwind', + + /* + |--------------------------------------------------------------------------- + | Release Token + |--------------------------------------------------------------------------- + | + | This token is stored client-side and sent along with each request to check + | a users session to see if a new release has invalidated it. If there is + | a mismatch it will throw an error and prompt for a browser refresh. + | + */ + + 'release_token' => 'a', + + /* + |--------------------------------------------------------------------------- + | CSP Safe + |--------------------------------------------------------------------------- + | + | This config is used to determine if Livewire will use the CSP-safe version + | of Alpine in its bundle. This is useful for applications that are using + | strict Content Security Policy (CSP) to protect against XSS attacks. + | + */ + + 'csp_safe' => false, + + /* + |--------------------------------------------------------------------------- + | Payload Guards + |--------------------------------------------------------------------------- + | + | These settings protect against malicious or oversized payloads that could + | cause denial of service. The default values should feel reasonable for + | most web applications. Each can be set to null to disable the limit. + | + */ + + 'payload' => [ + 'max_size' => 1024 * 1024, // 1MB - maximum request payload size in bytes + 'max_nesting_depth' => 10, // Maximum depth of dot-notation property paths + 'max_calls' => 50, // Maximum method calls per request + 'max_components' => 20, // Maximum components per batch request + ], +]; diff --git a/config/logging.php b/config/logging.php index 47b1d08..9f6e543 100644 --- a/config/logging.php +++ b/config/logging.php @@ -54,7 +54,7 @@ return [ 'stack' => [ 'driver' => 'stack', - 'channels' => explode(',', env('LOG_STACK', 'single')), + 'channels' => explode(',', (string) env('LOG_STACK', 'single')), 'ignore_exceptions' => false, ], @@ -98,10 +98,10 @@ return [ 'driver' => 'monolog', 'level' => env('LOG_LEVEL', 'debug'), 'handler' => StreamHandler::class, - 'formatter' => env('LOG_STDOUT_FORMATTER'), - 'with' => [ + 'handler_with' => [ 'stream' => 'php://stdout', ], + 'formatter' => env('LOG_STDOUT_FORMATTER'), 'processors' => [PsrLogMessageProcessor::class], ], @@ -109,10 +109,10 @@ return [ 'driver' => 'monolog', 'level' => env('LOG_LEVEL', 'debug'), 'handler' => StreamHandler::class, - 'formatter' => env('LOG_STDERR_FORMATTER'), - 'with' => [ + 'handler_with' => [ 'stream' => 'php://stderr', ], + 'formatter' => env('LOG_STDERR_FORMATTER'), 'processors' => [PsrLogMessageProcessor::class], ], diff --git a/config/mail.php b/config/mail.php index 756305b..522b284 100644 --- a/config/mail.php +++ b/config/mail.php @@ -46,7 +46,7 @@ return [ 'username' => env('MAIL_USERNAME'), 'password' => env('MAIL_PASSWORD'), 'timeout' => null, - 'local_domain' => env('MAIL_EHLO_DOMAIN', parse_url(env('APP_URL', 'http://localhost'), PHP_URL_HOST)), + 'local_domain' => env('MAIL_EHLO_DOMAIN', parse_url((string) env('APP_URL', 'http://localhost'), PHP_URL_HOST)), ], 'ses' => [ @@ -85,6 +85,7 @@ return [ 'smtp', 'log', ], + 'retry_after' => 60, ], 'roundrobin' => [ @@ -93,6 +94,7 @@ return [ 'ses', 'postmark', ], + 'retry_after' => 60, ], ], diff --git a/config/queue.php b/config/queue.php index 116bd8d..79c2c0a 100644 --- a/config/queue.php +++ b/config/queue.php @@ -24,7 +24,8 @@ return [ | used by your application. An example configuration is provided for | each backend supported by Laravel. You're also free to add more. | - | Drivers: "sync", "database", "beanstalkd", "sqs", "redis", "null" + | Drivers: "sync", "database", "beanstalkd", "sqs", "redis", + | "deferred", "background", "failover", "null" | */ @@ -72,6 +73,22 @@ return [ 'after_commit' => false, ], + 'deferred' => [ + 'driver' => 'deferred', + ], + + 'background' => [ + 'driver' => 'background', + ], + + 'failover' => [ + 'driver' => 'failover', + 'connections' => [ + 'database', + 'deferred', + ], + ], + ], /* diff --git a/config/services.php b/config/services.php index 3df6254..4598eb4 100644 --- a/config/services.php +++ b/config/services.php @@ -15,7 +15,11 @@ return [ */ 'postmark' => [ - 'token' => env('POSTMARK_TOKEN'), + 'key' => env('POSTMARK_API_KEY'), + ], + + 'resend' => [ + 'key' => env('RESEND_API_KEY'), ], 'ses' => [ @@ -24,10 +28,6 @@ return [ 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), ], - 'resend' => [ - 'key' => env('RESEND_KEY'), - ], - 'slack' => [ 'notifications' => [ 'bot_user_oauth_token' => env('SLACK_BOT_USER_OAUTH_TOKEN'), @@ -36,10 +36,33 @@ return [ ], 'trmnl' => [ + 'base_url' => 'https://trmnl.com', 'proxy_base_url' => env('TRMNL_PROXY_BASE_URL', 'https://trmnl.app'), 'proxy_refresh_minutes' => env('TRMNL_PROXY_REFRESH_MINUTES', 15), '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' => [ + 'notifications' => [ + 'url' => env('WEBHOOK_NOTIFICATION_URL', null), + 'topic' => env('WEBHOOK_NOTIFICATION_TOPIC', 'null'), + ], + ], + + 'oidc' => [ + 'enabled' => env('OIDC_ENABLED', false), + // OIDC_ENDPOINT can be either: + // - Base URL: https://your-provider.com (will append /.well-known/openid-configuration) + // - Full well-known URL: https://your-provider.com/.well-known/openid-configuration + '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', + 'scopes' => explode(',', env('OIDC_SCOPES', 'openid,profile,email')), ], ]; diff --git a/config/session.php b/config/session.php index f0b6541..5b541b7 100644 --- a/config/session.php +++ b/config/session.php @@ -13,8 +13,8 @@ return [ | incoming requests. Laravel supports a variety of storage options to | persist session data. Database storage is a great default choice. | - | Supported: "file", "cookie", "database", "apc", - | "memcached", "redis", "dynamodb", "array" + | Supported: "file", "cookie", "database", "memcached", + | "redis", "dynamodb", "array" | */ @@ -32,7 +32,7 @@ return [ | */ - 'lifetime' => env('SESSION_LIFETIME', 120), + 'lifetime' => (int) env('SESSION_LIFETIME', 120), 'expire_on_close' => env('SESSION_EXPIRE_ON_CLOSE', false), @@ -97,7 +97,7 @@ return [ | define the cache store which should be used to store the session data | between requests. This must match one of your defined cache stores. | - | Affects: "apc", "dynamodb", "memcached", "redis" + | Affects: "dynamodb", "memcached", "redis" | */ @@ -129,7 +129,7 @@ return [ 'cookie' => env( 'SESSION_COOKIE', - Str::slug(env('APP_NAME', 'laravel'), '_').'_session' + Str::slug((string) env('APP_NAME', 'laravel')).'-session' ), /* @@ -152,7 +152,7 @@ return [ | | This value determines the domain and subdomains the session cookie is | available to. By default, the cookie will be available to the root - | domain and all subdomains. Typically, this shouldn't be changed. + | domain without subdomains. Typically, this shouldn't be changed. | */ diff --git a/config/settings.php b/config/settings.php new file mode 100644 index 0000000..1065503 --- /dev/null +++ b/config/settings.php @@ -0,0 +1,94 @@ + [ + + ], + + /* + * The path where the settings classes will be created. + */ + 'setting_class_path' => app_path('Settings'), + + /* + * In these directories settings migrations will be stored and ran when migrating. A settings + * migration created via the make:settings-migration command will be stored in the first path or + * a custom defined path when running the command. + */ + 'migrations_paths' => [ + database_path('settings'), + ], + + /* + * When no repository was set for a settings class the following repository + * will be used for loading and saving settings. + */ + 'default_repository' => 'database', + + /* + * Settings will be stored and loaded from these repositories. + */ + 'repositories' => [ + 'database' => [ + 'type' => Spatie\LaravelSettings\SettingsRepositories\DatabaseSettingsRepository::class, + 'model' => null, + 'table' => null, + 'connection' => null, + ], + 'redis' => [ + 'type' => Spatie\LaravelSettings\SettingsRepositories\RedisSettingsRepository::class, + 'connection' => null, + 'prefix' => null, + ], + ], + + /* + * The encoder and decoder will determine how settings are stored and + * retrieved in the database. By default, `json_encode` and `json_decode` + * are used. + */ + 'encoder' => null, + 'decoder' => null, + + /* + * The contents of settings classes can be cached through your application, + * settings will be stored within a provided Laravel store and can have an + * additional prefix. + */ + 'cache' => [ + 'enabled' => env('SETTINGS_CACHE_ENABLED', false), + 'store' => null, + 'prefix' => null, + 'ttl' => null, + ], + + /* + * These global casts will be automatically used whenever a property within + * your settings class isn't a default PHP type. + */ + 'global_casts' => [ + DateTimeInterface::class => Spatie\LaravelSettings\SettingsCasts\DateTimeInterfaceCast::class, + DateTimeZone::class => Spatie\LaravelSettings\SettingsCasts\DateTimeZoneCast::class, + // Spatie\DataTransferObject\DataTransferObject::class => Spatie\LaravelSettings\SettingsCasts\DtoCast::class, + Spatie\LaravelData\Data::class => Spatie\LaravelSettings\SettingsCasts\DataCast::class, + ], + + /* + * The package will look for settings in these paths and automatically + * register them. + */ + 'auto_discover_settings' => [ + app_path('Settings'), + ], + + /* + * Automatically discovered settings classes can be cached, so they don't + * need to be searched each time the application boots up. + */ + 'discovered_settings_cache_path' => base_path('bootstrap/cache'), +]; diff --git a/config/sidecar-browsershot.php b/config/sidecar-browsershot.php new file mode 100644 index 0000000..217f3ef --- /dev/null +++ b/config/sidecar-browsershot.php @@ -0,0 +1,56 @@ + env('SIDECAR_BROWSERSHOT_MEMORY', 2048), + + /** + * The default ephemeral storage available to SidecarBrowsershot, in megabytes. (Defaults to 512MB) + * + * @see https://hammerstone.dev/sidecar/docs/main/functions/customization#storage + */ + 'storage' => env('SIDECAR_BROWSERSHOT_STORAGE', 512), + + /** + * The default timeout to use for SidecarBrowsershot, in seconds. (Defaults to 300) + * + * @see https://hammerstone.dev/sidecar/docs/main/functions/customization#timeout + */ + 'timeout' => env('SIDECAR_BROWSERSHOT_TIMEOUT', 300), + + /** + * Define the number of warming instances to boot. + * + * @see https://hammerstone.dev/sidecar/docs/main/functions/warming + */ + 'warming' => env('SIDECAR_BROWSERSHOT_WARMING_INSTANCES', 0), + + /** + * AWS Layers to use by the Lambda function. + * Defaults to "shelfio/chrome-aws-lambda-layer" and "sidecar-browsershot-layer" in your respective AWS region. + * + * If you customize this, you must include both "sidecar-browsershot-layer" and "shelfio/chrome-aws-lambda-layer" + * in your list, as the config overrides the default values. + * (See BrowsershotFunction@layers for more details) + * + * @see https://github.com/shelfio/chrome-aws-lambda-layer + * @see https://github.com/stefanzweifel/sidecar-browsershot-layer + */ + 'layers' => [ + // "arn:aws:lambda:us-east-1:821527532446:layer:sidecar-browsershot-layer:2", + // "arn:aws:lambda:us-east-1:764866452798:layer:chrome-aws-lambda:42", + ], + + /** + * Path to local directory containing fonts to be installed in the Lambda function. + * During deployment, BorwsershotLambda will scan this directory for + * any files and will bundle them into the Lambda function. + */ + 'fonts' => resource_path('sidecar-browsershot/fonts'), +]; diff --git a/config/sidecar.php b/config/sidecar.php new file mode 100644 index 0000000..b825e9d --- /dev/null +++ b/config/sidecar.php @@ -0,0 +1,10 @@ + [ + Wnx\SidecarBrowsershot\Functions\BrowsershotFunction::class, + ], +]; 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/DeviceLogFactory.php b/database/factories/DeviceLogFactory.php new file mode 100644 index 0000000..10871d0 --- /dev/null +++ b/database/factories/DeviceLogFactory.php @@ -0,0 +1,24 @@ + ['creation_timestamp' => fake()->dateTimeBetween('-1 month', 'now')->getTimestamp(), 'device_status_stamp' => ['wifi_rssi_level' => -65, 'wifi_status' => 'connected', 'refresh_rate' => 900, 'time_since_last_sleep_start' => 901, 'current_fw_version' => '1.5.5', 'special_function' => 'none', 'battery_voltage' => 4.052, 'wakeup_reason' => 'timer', 'free_heap_size' => 215128, 'max_alloc_size' => 192500], 'log_id' => 17, 'log_message' => 'Error fetching API display: 7, detail: HTTP Client failed with error: connection refused(-1)', 'log_codeline' => 586, 'log_sourcefile' => "src\/bl.cpp", 'additional_info' => ['filename_current' => 'UUID.png', 'filename_new' => null, 'retry_attempt' => 5]], + 'device_timestamp' => fake()->dateTimeBetween('-1 month', 'now'), + 'created_at' => Carbon::now(), + 'updated_at' => Carbon::now(), + 'device_id' => Device::first(), + ]; + } +} diff --git a/database/factories/DeviceModelFactory.php b/database/factories/DeviceModelFactory.php new file mode 100644 index 0000000..ec3f77d --- /dev/null +++ b/database/factories/DeviceModelFactory.php @@ -0,0 +1,38 @@ + + */ +class DeviceModelFactory extends Factory +{ + protected $model = DeviceModel::class; + + /** + * Define the model's default state. + * + * @return array + */ + public function definition(): array + { + return [ + 'name' => $this->faker->unique()->slug(), + 'label' => $this->faker->words(2, true), + 'description' => $this->faker->sentence(), + 'width' => $this->faker->randomElement([800, 1024, 1280, 1920]), + 'height' => $this->faker->randomElement([480, 600, 720, 1080]), + 'colors' => $this->faker->randomElement([2, 16, 256, 65536]), + 'bit_depth' => $this->faker->randomElement([1, 4, 8, 16]), + 'scale_factor' => $this->faker->randomElement([1, 2, 4]), + 'rotation' => $this->faker->randomElement([0, 90, 180, 270]), + 'mime_type' => $this->faker->randomElement(['image/png', 'image/jpeg', 'image/gif']), + 'offset_x' => $this->faker->numberBetween(-100, 100), + 'offset_y' => $this->faker->numberBetween(-100, 100), + 'published_at' => $this->faker->optional()->dateTimeBetween('-1 year', 'now'), + ]; + } +} 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/FirmwareFactory.php b/database/factories/FirmwareFactory.php new file mode 100644 index 0000000..f0b27ee --- /dev/null +++ b/database/factories/FirmwareFactory.php @@ -0,0 +1,24 @@ + $this->faker->word(), + 'url' => $this->faker->url(), + 'latest' => $this->faker->boolean(), + 'storage_location' => $this->faker->word(), + 'created_at' => Carbon::now(), + 'updated_at' => Carbon::now(), + ]; + } +} diff --git a/database/factories/PlaylistItemFactory.php b/database/factories/PlaylistItemFactory.php index 9045e58..a7a1d97 100644 --- a/database/factories/PlaylistItemFactory.php +++ b/database/factories/PlaylistItemFactory.php @@ -17,6 +17,7 @@ class PlaylistItemFactory extends Factory return [ 'playlist_id' => Playlist::factory(), 'plugin_id' => Plugin::factory(), + 'mashup' => null, 'order' => $this->faker->numberBetween(0, 100), 'is_active' => $this->faker->boolean(80), // 80% chance of being active 'last_displayed_at' => null, diff --git a/database/factories/PluginFactory.php b/database/factories/PluginFactory.php index a4aa033..10a1580 100644 --- a/database/factories/PluginFactory.php +++ b/database/factories/PluginFactory.php @@ -22,14 +22,31 @@ class PluginFactory extends Factory 'polling_url' => $this->faker->url(), 'polling_verb' => $this->faker->randomElement(['get', 'post']), 'polling_header' => null, + 'polling_body' => null, 'render_markup' => null, 'render_markup_view' => null, 'detail_view_route' => null, '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/factories/UserFactory.php b/database/factories/UserFactory.php index c5d3f2c..80da5ac 100644 --- a/database/factories/UserFactory.php +++ b/database/factories/UserFactory.php @@ -29,7 +29,9 @@ class UserFactory extends Factory 'email_verified_at' => now(), 'password' => static::$password ??= Hash::make('password'), 'remember_token' => Str::random(10), - 'assign_new_devices' => false, + 'two_factor_secret' => null, + 'two_factor_recovery_codes' => null, + 'two_factor_confirmed_at' => null, ]; } @@ -42,4 +44,16 @@ class UserFactory extends Factory 'email_verified_at' => null, ]); } + + /** + * Indicate that the model has two-factor authentication configured. + */ + public function withTwoFactor(): static + { + return $this->state(fn (array $attributes) => [ + 'two_factor_secret' => encrypt('secret'), + 'two_factor_recovery_codes' => encrypt(json_encode(['recovery-code-1'])), + 'two_factor_confirmed_at' => now(), + ]); + } } diff --git a/database/migrations/2025_03_18_110028_add_refresh_time_to_playlists_table.php b/database/migrations/2025_03_18_110028_add_refresh_time_to_playlists_table.php new file mode 100644 index 0000000..ad0b79d --- /dev/null +++ b/database/migrations/2025_03_18_110028_add_refresh_time_to_playlists_table.php @@ -0,0 +1,28 @@ +integer('refresh_time')->nullable()->after('active_until'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('playlists', function (Blueprint $table) { + $table->dropColumn('refresh_time'); + }); + } +}; diff --git a/database/migrations/2025_04_26_120013_update_plugin_table_correct_recipe_typo.php b/database/migrations/2025_04_26_120013_update_plugin_table_correct_recipe_typo.php new file mode 100644 index 0000000..d6efa6a --- /dev/null +++ b/database/migrations/2025_04_26_120013_update_plugin_table_correct_recipe_typo.php @@ -0,0 +1,47 @@ +where('uuid', $uuid) + ->update([ + 'render_markup_view' => DB::raw("REPLACE(render_markup_view, 'receipts.', 'recipes.')"), + ]); + } + } + + public function down(): void + { + // Revert the typo correction if needed + $pluginUuids = [ + '9e46c6cf-358c-4bfe-8998-436b3a207fec', // Γ–BB Departures + '3b046eda-34e9-4232-b935-c33b989a284b', // Weather + '21464b16-5f5a-4099-a967-f5c915e3da54', // Zen Quotes + '8d472959-400f-46ee-afb2-4a9f1cfd521f', // This Day in History + '4349fdad-a273-450b-aa00-3d32f2de788d', // Home Assistant + ]; + + foreach ($pluginUuids as $uuid) { + DB::table('plugins') + ->where('uuid', $uuid) + ->update([ + 'render_markup_view' => DB::raw("REPLACE(render_markup_view, 'recipes.', 'receipts.')"), + ]); + } + } +}; diff --git a/database/migrations/2025_05_05_151823_add_device_dimensions_to_devices_table.php b/database/migrations/2025_05_05_151823_add_device_dimensions_to_devices_table.php new file mode 100644 index 0000000..00defd1 --- /dev/null +++ b/database/migrations/2025_05_05_151823_add_device_dimensions_to_devices_table.php @@ -0,0 +1,24 @@ +integer('width')->nullable()->default(800)->after('api_key'); + $table->integer('height')->nullable()->default(480)->after('width'); + }); + } + + public function down(): void + { + Schema::table('devices', function (Blueprint $table) { + $table->dropColumn('width'); + $table->dropColumn('height'); + }); + } +}; diff --git a/database/migrations/2025_05_05_174926_add_mirror_device_id_to_devices_table.php b/database/migrations/2025_05_05_174926_add_mirror_device_id_to_devices_table.php new file mode 100644 index 0000000..f19ea47 --- /dev/null +++ b/database/migrations/2025_05_05_174926_add_mirror_device_id_to_devices_table.php @@ -0,0 +1,29 @@ +foreignId('mirror_device_id')->nullable()->constrained('devices')->nullOnDelete(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('devices', function (Blueprint $table) { + $table->dropForeign(['mirror_device_id']); + $table->dropColumn('mirror_device_id'); + }); + } +}; diff --git a/database/migrations/2025_05_08_225241_add_assign_new_device_id_to_users_table.php b/database/migrations/2025_05_08_225241_add_assign_new_device_id_to_users_table.php new file mode 100644 index 0000000..242b368 --- /dev/null +++ b/database/migrations/2025_05_08_225241_add_assign_new_device_id_to_users_table.php @@ -0,0 +1,29 @@ +foreignId('assign_new_device_id')->nullable()->constrained('devices')->nullOnDelete(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('users', function (Blueprint $table) { + $table->dropForeign(['assign_new_device_id']); + $table->dropColumn('assign_new_device_id'); + }); + } +}; diff --git a/database/migrations/2025_05_10_182724_add_plugin_cache.php b/database/migrations/2025_05_10_182724_add_plugin_cache.php new file mode 100644 index 0000000..a24f436 --- /dev/null +++ b/database/migrations/2025_05_10_182724_add_plugin_cache.php @@ -0,0 +1,28 @@ +string('current_image')->nullable()->after('data_payload_updated_at'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('plugins', function (Blueprint $table) { + $table->dropColumn('current_image'); + }); + } +}; diff --git a/database/migrations/2025_05_10_202133_add_rotate_to_devices_table.php b/database/migrations/2025_05_10_202133_add_rotate_to_devices_table.php new file mode 100644 index 0000000..e439b1b --- /dev/null +++ b/database/migrations/2025_05_10_202133_add_rotate_to_devices_table.php @@ -0,0 +1,22 @@ +integer('rotate')->nullable()->default(0)->after('width'); + }); + } + + public function down(): void + { + Schema::table('devices', function (Blueprint $table) { + $table->dropColumn('rotate'); + }); + } +}; diff --git a/database/migrations/2025_05_13_154942_add_image_format_to_devices_table.php b/database/migrations/2025_05_13_154942_add_image_format_to_devices_table.php new file mode 100644 index 0000000..41dc98c --- /dev/null +++ b/database/migrations/2025_05_13_154942_add_image_format_to_devices_table.php @@ -0,0 +1,28 @@ +string('image_format')->default('auto')->after('rotate'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('devices', function (Blueprint $table) { + $table->dropColumn('image_format'); + }); + } +}; diff --git a/database/migrations/2025_05_28_232528_create_firmware_table.php b/database/migrations/2025_05_28_232528_create_firmware_table.php new file mode 100644 index 0000000..e238629 --- /dev/null +++ b/database/migrations/2025_05_28_232528_create_firmware_table.php @@ -0,0 +1,25 @@ +id(); + $table->string('version_tag'); + $table->string('url')->nullable(); + $table->boolean('latest')->default(false); + $table->string('storage_location')->nullable(); + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists('firmware'); + } +}; diff --git a/database/migrations/2025_05_29_010428_add_update_firmware_id_to_devices_table.php b/database/migrations/2025_05_29_010428_add_update_firmware_id_to_devices_table.php new file mode 100644 index 0000000..fc5b99b --- /dev/null +++ b/database/migrations/2025_05_29_010428_add_update_firmware_id_to_devices_table.php @@ -0,0 +1,23 @@ +foreignId('update_firmware_id')->nullable()->constrained('firmware')->nullOnDelete(); + }); + } + + public function down(): void + { + Schema::table('devices', function (Blueprint $table) { + $table->dropForeign(['update_firmware_id']); + $table->dropColumn('update_firmware_id'); + }); + } +}; diff --git a/database/migrations/2025_06_01_195732_create_device_logs_table.php b/database/migrations/2025_06_01_195732_create_device_logs_table.php new file mode 100644 index 0000000..ef89f3e --- /dev/null +++ b/database/migrations/2025_06_01_195732_create_device_logs_table.php @@ -0,0 +1,25 @@ +id(); + $table->foreignIdFor(Device::class)->constrained('devices')->cascadeOnDelete(); + $table->timestamp('device_timestamp'); + $table->json('log_entry'); + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists('device_logs'); + } +}; diff --git a/database/migrations/2025_06_03_141055_add_last_refresh_at_to_devices_table.php b/database/migrations/2025_06_03_141055_add_last_refresh_at_to_devices_table.php new file mode 100644 index 0000000..51b1882 --- /dev/null +++ b/database/migrations/2025_06_03_141055_add_last_refresh_at_to_devices_table.php @@ -0,0 +1,28 @@ +timestamp('last_refreshed_at')->nullable(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('devices', function (Blueprint $table) { + $table->dropColumn('last_refreshed_at'); + }); + } +}; diff --git a/database/migrations/2025_06_10_211056_add_mashup_to_playlist_items_table.php b/database/migrations/2025_06_10_211056_add_mashup_to_playlist_items_table.php new file mode 100644 index 0000000..a8a61d5 --- /dev/null +++ b/database/migrations/2025_06_10_211056_add_mashup_to_playlist_items_table.php @@ -0,0 +1,22 @@ +json('mashup')->nullable(); + }); + } + + public function down(): void + { + Schema::table('playlist_items', function (Blueprint $table) { + $table->dropColumn('mashup'); + }); + } +}; 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_06_18_105902_add_battery_notification_sent_to_devices_table.php b/database/migrations/2025_06_18_105902_add_battery_notification_sent_to_devices_table.php new file mode 100644 index 0000000..ffe23bb --- /dev/null +++ b/database/migrations/2025_06_18_105902_add_battery_notification_sent_to_devices_table.php @@ -0,0 +1,28 @@ +boolean('battery_notification_sent')->default(false); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('devices', function (Blueprint $table) { + $table->dropColumn('battery_notification_sent'); + }); + } +}; diff --git a/database/migrations/2025_06_20_163742_allow_long_polling_url.php b/database/migrations/2025_06_20_163742_allow_long_polling_url.php new file mode 100644 index 0000000..867837f --- /dev/null +++ b/database/migrations/2025_06_20_163742_allow_long_polling_url.php @@ -0,0 +1,29 @@ +string('polling_url', 1024)->nullable()->change(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('plugins', function (Blueprint $table) { + // old default string length value in Illuminate + $table->string('polling_url', 255)->nullable()->change(); + }); + } +}; diff --git a/database/migrations/2025_07_02_161953_add_polling_body_to_plugins_table.php b/database/migrations/2025_07_02_161953_add_polling_body_to_plugins_table.php new file mode 100644 index 0000000..c1fbc94 --- /dev/null +++ b/database/migrations/2025_07_02_161953_add_polling_body_to_plugins_table.php @@ -0,0 +1,28 @@ +text('polling_body')->nullable()->after('polling_header'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('plugins', function (Blueprint $table) { + $table->dropColumn('polling_body'); + }); + } +}; diff --git a/database/migrations/2025_07_02_231414_add_markup_language_to_plugins_table.php b/database/migrations/2025_07_02_231414_add_markup_language_to_plugins_table.php new file mode 100644 index 0000000..09405be --- /dev/null +++ b/database/migrations/2025_07_02_231414_add_markup_language_to_plugins_table.php @@ -0,0 +1,28 @@ +string('markup_language')->nullable()->after('render_markup'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('plugins', function (Blueprint $table) { + $table->dropColumn('markup_language'); + }); + } +}; diff --git a/database/migrations/2025_07_08_191003_add_sleep_mode_and_special_function_to_devices_table.php b/database/migrations/2025_07_08_191003_add_sleep_mode_and_special_function_to_devices_table.php new file mode 100644 index 0000000..adc74e7 --- /dev/null +++ b/database/migrations/2025_07_08_191003_add_sleep_mode_and_special_function_to_devices_table.php @@ -0,0 +1,31 @@ +boolean('sleep_mode_enabled')->default(false); + $table->time('sleep_mode_from')->nullable(); + $table->time('sleep_mode_to')->nullable(); + $table->string('special_function')->nullable(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('devices', function (Blueprint $table) { + $table->dropColumn(['sleep_mode_enabled', 'sleep_mode_from', 'sleep_mode_to', 'special_function']); + }); + } +}; diff --git a/database/migrations/2025_07_10_164606_add_pause_until_to_devices_table.php b/database/migrations/2025_07_10_164606_add_pause_until_to_devices_table.php new file mode 100644 index 0000000..69181df --- /dev/null +++ b/database/migrations/2025_07_10_164606_add_pause_until_to_devices_table.php @@ -0,0 +1,22 @@ +dateTime('pause_until')->nullable()->after('last_refreshed_at'); + }); + } + + public function down(): void + { + Schema::table('devices', function (Blueprint $table) { + $table->dropColumn('pause_until'); + }); + } +}; 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 new file mode 100644 index 0000000..7ec1374 --- /dev/null +++ b/database/migrations/2025_08_04_064514_add_oidc_sub_to_users_table.php @@ -0,0 +1,29 @@ +string('oidc_sub')->nullable()->unique()->after('email'); + }); + } + + /** + * Reverse the migrations. + */ + 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_07_111635_create_device_models_table.php b/database/migrations/2025_08_07_111635_create_device_models_table.php new file mode 100644 index 0000000..338ca98 --- /dev/null +++ b/database/migrations/2025_08_07_111635_create_device_models_table.php @@ -0,0 +1,41 @@ +id(); + $table->string('name')->unique(); + $table->string('label'); + $table->text('description'); + $table->unsignedInteger('width'); + $table->unsignedInteger('height'); + $table->unsignedInteger('colors'); + $table->unsignedInteger('bit_depth'); + $table->float('scale_factor'); + $table->integer('rotation'); + $table->string('mime_type'); + $table->integer('offset_x')->default(0); + $table->integer('offset_y')->default(0); + $table->timestamp('published_at')->nullable(); + $table->string('source')->nullable(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('device_models'); + } +}; diff --git a/database/migrations/2025_08_07_131843_add_device_model_id_to_devices_table.php b/database/migrations/2025_08_07_131843_add_device_model_id_to_devices_table.php new file mode 100644 index 0000000..727c545 --- /dev/null +++ b/database/migrations/2025_08_07_131843_add_device_model_id_to_devices_table.php @@ -0,0 +1,29 @@ +foreignId('device_model_id')->nullable()->constrained('device_models')->nullOnDelete(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('devices', function (Blueprint $table) { + $table->dropForeign(['device_model_id']); + $table->dropColumn('device_model_id'); + }); + } +}; diff --git a/database/migrations/2025_08_16_135740_seed_device_models.php b/database/migrations/2025_08_16_135740_seed_device_models.php new file mode 100644 index 0000000..355227f --- /dev/null +++ b/database/migrations/2025_08_16_135740_seed_device_models.php @@ -0,0 +1,285 @@ + 'og_png', + 'label' => 'TRMNL OG (1-bit)', + 'description' => 'TRMNL OG (1-bit)', + 'width' => 800, + 'height' => 480, + 'colors' => 2, + 'bit_depth' => 1, + 'scale_factor' => 1, + 'rotation' => 0, + 'mime_type' => 'image/png', + 'offset_x' => 0, + 'offset_y' => 0, + 'published_at' => '2024-01-01 00:00:00', + 'source' => 'api', + 'created_at' => now(), + 'updated_at' => now(), + ], + [ + 'name' => 'og_plus', + 'label' => 'TRMNL OG (2-bit)', + 'description' => 'TRMNL OG (2-bit)', + 'width' => 800, + 'height' => 480, + 'colors' => 4, + 'bit_depth' => 2, + 'scale_factor' => 1, + 'rotation' => 0, + 'mime_type' => 'image/png', + 'offset_x' => 0, + 'offset_y' => 0, + 'published_at' => '2024-01-01 00:00:00', + 'source' => 'api', + 'created_at' => now(), + 'updated_at' => now(), + ], + [ + 'name' => 'amazon_kindle_2024', + 'label' => 'Amazon Kindle 2024', + 'description' => 'Amazon Kindle 2024', + 'width' => 1400, + 'height' => 840, + 'colors' => 256, + 'bit_depth' => 8, + 'scale_factor' => 2.414, + 'rotation' => 90, + 'mime_type' => 'image/png', + 'offset_x' => 75, + 'offset_y' => 25, + 'published_at' => '2024-01-01 00:00:00', + 'source' => 'api', + 'created_at' => now(), + 'updated_at' => now(), + ], + [ + 'name' => 'amazon_kindle_paperwhite_6th_gen', + 'label' => 'Amazon Kindle PW 6th Gen', + 'description' => 'Amazon Kindle PW 6th Gen', + 'width' => 1024, + 'height' => 768, + 'colors' => 256, + 'bit_depth' => 8, + 'scale_factor' => 1, + 'rotation' => 90, + 'mime_type' => 'image/png', + 'offset_x' => 0, + 'offset_y' => 0, + 'published_at' => '2024-01-01 00:00:00', + 'source' => 'api', + 'created_at' => now(), + 'updated_at' => now(), + ], + [ + 'name' => 'amazon_kindle_paperwhite_7th_gen', + 'label' => 'Amazon Kindle PW 7th Gen', + 'description' => 'Amazon Kindle PW 7th Gen', + 'width' => 1448, + 'height' => 1072, + 'colors' => 256, + 'bit_depth' => 8, + 'scale_factor' => 1, + 'rotation' => 90, + 'mime_type' => 'image/png', + 'offset_x' => 0, + 'offset_y' => 0, + 'published_at' => '2024-01-01 00:00:00', + 'source' => 'api', + 'created_at' => now(), + 'updated_at' => now(), + ], + [ + 'name' => 'inkplate_10', + 'label' => 'Inkplate 10', + 'description' => 'Inkplate 10', + 'width' => 1200, + 'height' => 820, + 'colors' => 8, + 'bit_depth' => 3, + 'scale_factor' => 1, + 'rotation' => 0, + 'mime_type' => 'image/png', + 'offset_x' => 0, + 'offset_y' => 0, + 'published_at' => '2024-01-01 00:00:00', + 'source' => 'api', + 'created_at' => now(), + 'updated_at' => now(), + ], + [ + 'name' => 'amazon_kindle_7', + 'label' => 'Amazon Kindle 7', + 'description' => 'Amazon Kindle 7', + 'width' => 800, + 'height' => 600, + 'colors' => 256, + 'bit_depth' => 8, + 'scale_factor' => 1, + 'rotation' => 90, + 'mime_type' => 'image/png', + 'offset_x' => 0, + 'offset_y' => 0, + 'published_at' => '2024-01-01 00:00:00', + 'source' => 'api', + 'created_at' => now(), + 'updated_at' => now(), + ], + [ + 'name' => 'inky_impression_7_3', + 'label' => 'Inky Impression 7.3', + 'description' => 'Inky Impression 7.3', + 'width' => 800, + 'height' => 480, + 'colors' => 2, + 'bit_depth' => 1, + 'scale_factor' => 1, + 'rotation' => 0, + 'mime_type' => 'image/png', + 'offset_x' => 0, + 'offset_y' => 0, + 'published_at' => '2024-01-01 00:00:00', + 'source' => 'api', + 'created_at' => now(), + 'updated_at' => now(), + ], + [ + 'name' => 'kobo_libra_2', + 'label' => 'Kobo Libra 2', + 'description' => 'Kobo Libra 2', + 'width' => 1680, + 'height' => 1264, + 'colors' => 256, + 'bit_depth' => 8, + 'scale_factor' => 1, + 'rotation' => 90, + 'mime_type' => 'image/png', + 'offset_x' => 0, + 'offset_y' => 0, + 'published_at' => '2024-01-01 00:00:00', + 'source' => 'api', + 'created_at' => now(), + 'updated_at' => now(), + ], + [ + 'name' => 'amazon_kindle_oasis_2', + 'label' => 'Amazon Kindle Oasis 2', + 'description' => 'Amazon Kindle Oasis 2', + 'width' => 1680, + 'height' => 1264, + 'colors' => 256, + 'bit_depth' => 8, + 'scale_factor' => 1, + 'rotation' => 90, + 'mime_type' => 'image/png', + 'offset_x' => 0, + 'offset_y' => 0, + 'published_at' => '2024-01-01 00:00:00', + 'source' => 'api', + 'created_at' => now(), + 'updated_at' => now(), + ], + [ + 'name' => 'kobo_aura_one', + 'label' => 'Kobo Aura One', + 'description' => 'Kobo Aura One', + 'width' => 1872, + 'height' => 1404, + 'colors' => 256, + 'bit_depth' => 8, + 'scale_factor' => 1, + 'rotation' => 90, + 'mime_type' => 'image/png', + 'offset_x' => 0, + 'offset_y' => 0, + 'published_at' => '2024-01-01 00:00:00', + 'source' => 'api', + 'created_at' => now(), + 'updated_at' => now(), + ], + [ + 'name' => 'kobo_aura_hd', + 'label' => 'Kobo Aura HD', + 'description' => 'Kobo Aura HD', + 'width' => 1440, + 'height' => 1080, + 'colors' => 16, + 'bit_depth' => 4, + 'scale_factor' => 1, + 'rotation' => 90, + 'mime_type' => 'image/png', + 'offset_x' => 0, + 'offset_y' => 0, + 'published_at' => '2024-01-01 00:00:00', + 'source' => 'api', + 'created_at' => now(), + 'updated_at' => now(), + ], + [ + 'name' => 'inky_impression_13_3', + 'label' => 'Inky Impression 13.3', + 'description' => 'Inky Impression 13.3', + 'width' => 1600, + 'height' => 1200, + 'colors' => 2, + 'bit_depth' => 1, + 'scale_factor' => 1, + 'rotation' => 0, + 'mime_type' => 'image/png', + 'offset_x' => 0, + 'offset_y' => 0, + 'published_at' => '2024-01-01 00:00:00', + 'source' => 'api', + 'created_at' => now(), + 'updated_at' => now(), + ], + ]; + + // Upsert by unique 'name' to avoid duplicates and keep data fresh + DeviceModel::query()->upsert( + $deviceModels, + ['name'], + [ + 'label', 'description', 'width', 'height', 'colors', 'bit_depth', 'scale_factor', + 'rotation', 'mime_type', 'offset_x', 'offset_y', 'published_at', 'source', + 'created_at', 'updated_at', + ] + ); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + $names = [ + 'og_png', + 'amazon_kindle_2024', + 'amazon_kindle_paperwhite_6th_gen', + 'amazon_kindle_paperwhite_7th_gen', + 'inkplate_10', + 'amazon_kindle_7', + 'inky_impression_7_3', + 'kobo_libra_2', + 'amazon_kindle_oasis_2', + 'og_plus', + 'kobo_aura_one', + 'kobo_aura_hd', + 'inky_impression_13_3', + ]; + + DeviceModel::query()->whereIn('name', $names)->delete(); + } +}; 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/migrations/2026_01_15_075243_add_two_factor_columns_to_users_table.php b/database/migrations/2026_01_15_075243_add_two_factor_columns_to_users_table.php new file mode 100644 index 0000000..187d974 --- /dev/null +++ b/database/migrations/2026_01_15_075243_add_two_factor_columns_to_users_table.php @@ -0,0 +1,34 @@ +text('two_factor_secret')->after('password')->nullable(); + $table->text('two_factor_recovery_codes')->after('two_factor_secret')->nullable(); + $table->timestamp('two_factor_confirmed_at')->after('two_factor_recovery_codes')->nullable(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('users', function (Blueprint $table) { + $table->dropColumn([ + 'two_factor_secret', + 'two_factor_recovery_codes', + 'two_factor_confirmed_at', + ]); + }); + } +}; diff --git a/database/migrations/2026_01_16_083707_create_settings_table.php b/database/migrations/2026_01_16_083707_create_settings_table.php new file mode 100644 index 0000000..9b14b86 --- /dev/null +++ b/database/migrations/2026_01_16_083707_create_settings_table.php @@ -0,0 +1,24 @@ +id(); + + $table->string('group'); + $table->string('name'); + $table->boolean('locked')->default(false); + $table->json('payload'); + + $table->timestamps(); + + $table->unique(['group', 'name']); + }); + } +}; diff --git a/database/migrations/2026_01_28_143142_add_layout_markup_columns_to_plugins_table.php b/database/migrations/2026_01_28_143142_add_layout_markup_columns_to_plugins_table.php new file mode 100644 index 0000000..e56751c --- /dev/null +++ b/database/migrations/2026_01_28_143142_add_layout_markup_columns_to_plugins_table.php @@ -0,0 +1,38 @@ +text('render_markup_half_horizontal')->nullable()->after('render_markup'); + $table->text('render_markup_half_vertical')->nullable()->after('render_markup_half_horizontal'); + $table->text('render_markup_quadrant')->nullable()->after('render_markup_half_vertical'); + $table->text('render_markup_shared')->nullable()->after('render_markup_quadrant'); + $table->text('transform_code')->nullable()->after('render_markup_shared'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('plugins', function (Blueprint $table) { + $table->dropColumn([ + 'render_markup_half_horizontal', + 'render_markup_half_vertical', + 'render_markup_quadrant', + 'render_markup_shared', + 'transform_code', + ]); + }); + } +}; diff --git a/database/migrations/2026_02_01_121714_add_maximum_compatibility_to_devices_table.php b/database/migrations/2026_02_01_121714_add_maximum_compatibility_to_devices_table.php new file mode 100644 index 0000000..bd0d54b --- /dev/null +++ b/database/migrations/2026_02_01_121714_add_maximum_compatibility_to_devices_table.php @@ -0,0 +1,22 @@ +boolean('maximum_compatibility')->default(false); + }); + } + + public function down(): void + { + Schema::table('devices', function (Blueprint $table): void { + $table->dropColumn('maximum_compatibility'); + }); + } +}; diff --git a/database/migrations/2026_02_17_153908_add_css_device_and_css_variables_to_device_models_table.php b/database/migrations/2026_02_17_153908_add_css_device_and_css_variables_to_device_models_table.php new file mode 100644 index 0000000..cd1b7db --- /dev/null +++ b/database/migrations/2026_02_17_153908_add_css_device_and_css_variables_to_device_models_table.php @@ -0,0 +1,29 @@ +string('css_name')->nullable()->after('kind'); + $table->json('css_variables')->nullable()->after('css_name'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('device_models', function (Blueprint $table) { + $table->dropColumn(['css_name', 'css_variables']); + }); + } +}; diff --git a/database/migrations/2026_02_17_221924_set_css_name_and_css_variables_for_seeded_device_models.php b/database/migrations/2026_02_17_221924_set_css_name_and_css_variables_for_seeded_device_models.php new file mode 100644 index 0000000..728fe4f --- /dev/null +++ b/database/migrations/2026_02_17_221924_set_css_name_and_css_variables_for_seeded_device_models.php @@ -0,0 +1,160 @@ +}> + */ + private const SEEDED_CSS = [ + 'og_png' => [ + 'css_name' => 'og_png', + 'css_variables' => [ + '--screen-w' => '800px', + '--screen-h' => '480px', + '--ui-scale' => '1.0', + '--gap-scale' => '1.0', + ], + ], + 'og_plus' => [ + 'css_name' => 'ogv2', + 'css_variables' => [ + '--screen-w' => '800px', + '--screen-h' => '480px', + '--ui-scale' => '1.0', + '--gap-scale' => '1.0', + ], + ], + 'amazon_kindle_2024' => [ + 'css_name' => 'amazon_kindle_2024', + 'css_variables' => [ + '--screen-w' => '800px', + '--screen-h' => '480px', + '--ui-scale' => '0.8', + '--gap-scale' => '1.0', + ], + ], + 'amazon_kindle_paperwhite_6th_gen' => [ + 'css_name' => 'amazon_kindle_paperwhite_6th_gen', + 'css_variables' => [ + '--screen-w' => '800px', + '--screen-h' => '600px', + '--ui-scale' => '1.0', + '--gap-scale' => '1.0', + ], + ], + 'amazon_kindle_paperwhite_7th_gen' => [ + 'css_name' => 'amazon_kindle_paperwhite_7th_gen', + 'css_variables' => [ + '--screen-w' => '905px', + '--screen-h' => '670px', + '--ui-scale' => '1.0', + '--gap-scale' => '1.0', + ], + ], + 'inkplate_10' => [ + 'css_name' => 'inkplate_10', + 'css_variables' => [ + '--screen-w' => '800px', + '--screen-h' => '547px', + '--ui-scale' => '1.0', + '--gap-scale' => '1.0', + ], + ], + 'amazon_kindle_7' => [ + 'css_name' => 'amazon_kindle_7', + 'css_variables' => [ + '--screen-w' => '800px', + '--screen-h' => '600px', + '--ui-scale' => '1.0', + '--gap-scale' => '1.0', + ], + ], + 'inky_impression_7_3' => [ + 'css_name' => 'inky_impression_7_3', + 'css_variables' => [ + '--screen-w' => '800px', + '--screen-h' => '480px', + '--ui-scale' => '1.0', + '--gap-scale' => '1.0', + ], + ], + 'kobo_libra_2' => [ + 'css_name' => 'kobo_libra_2', + 'css_variables' => [ + '--screen-w' => '800px', + '--screen-h' => '602px', + '--ui-scale' => '1.0', + '--gap-scale' => '1.0', + ], + ], + 'amazon_kindle_oasis_2' => [ + 'css_name' => 'amazon_kindle_oasis_2', + 'css_variables' => [ + '--screen-w' => '800px', + '--screen-h' => '602px', + '--ui-scale' => '1.0', + '--gap-scale' => '1.0', + ], + ], + 'kobo_aura_one' => [ + 'css_name' => 'kobo_aura_one', + 'css_variables' => [ + '--screen-w' => '1040px', + '--screen-h' => '780px', + '--ui-scale' => '1.0', + '--gap-scale' => '1.0', + ], + ], + 'kobo_aura_hd' => [ + 'css_name' => 'kobo_aura_hd', + 'css_variables' => [ + '--screen-w' => '800px', + '--screen-h' => '600px', + '--ui-scale' => '1.0', + '--gap-scale' => '1.0', + ], + ], + 'inky_impression_13_3' => [ + 'css_name' => 'inky_impression_13_3', + 'css_variables' => [ + '--screen-w' => '800px', + '--screen-h' => '600px', + '--ui-scale' => '1.0', + '--gap-scale' => '1.0', + ], + ], + ]; + + /** + * Run the migrations. + */ + public function up(): void + { + foreach (self::SEEDED_CSS as $name => $payload) { + DeviceModel::query() + ->where('name', $name) + ->update([ + 'css_name' => $payload['css_name'], + 'css_variables' => $payload['css_variables'], + ]); + } + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + DeviceModel::query() + ->whereIn('name', array_keys(self::SEEDED_CSS)) + ->update([ + 'css_name' => null, + 'css_variables' => null, + ]); + } +}; diff --git a/database/migrations/2026_02_27_153837_add_current_image_metadata_to_plugins_table.php b/database/migrations/2026_02_27_153837_add_current_image_metadata_to_plugins_table.php new file mode 100644 index 0000000..d212fe7 --- /dev/null +++ b/database/migrations/2026_02_27_153837_add_current_image_metadata_to_plugins_table.php @@ -0,0 +1,28 @@ +json('current_image_metadata')->nullable()->after('current_image'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('plugins', function (Blueprint $table) { + $table->dropColumn('current_image_metadata'); + }); + } +}; diff --git a/database/seeders/DatabaseSeeder.php b/database/seeders/DatabaseSeeder.php index a68d2f9..5811f4c 100644 --- a/database/seeders/DatabaseSeeder.php +++ b/database/seeders/DatabaseSeeder.php @@ -3,6 +3,7 @@ namespace Database\Seeders; use App\Models\Device; +use App\Models\Playlist; use App\Models\Plugin; use App\Models\User; use Illuminate\Database\Seeder; @@ -23,9 +24,29 @@ class DatabaseSeeder extends Seeder 'password' => bcrypt('admin@example.com'), ]); + $device = Device::factory()->create([ + 'mac_address' => '00:00:00:00:00:00', + 'api_key' => 'test-api-key', + 'proxy_cloud' => false, + ]); + + Playlist::factory()->create([ + 'device_id' => $device->id, + 'name' => 'Default', + 'is_active' => true, + 'active_from' => null, + 'active_until' => null, + 'weekdays' => null + ]); + // Device::factory(5)->create(); // Plugin::factory(3)->create(); + + $this->call([ + ExampleRecipesSeeder::class, + // MashupPocSeeder::class, + ]); } } } diff --git a/database/seeders/ExampleRecipesSeeder.php b/database/seeders/ExampleRecipesSeeder.php new file mode 100644 index 0000000..890eed9 --- /dev/null +++ b/database/seeders/ExampleRecipesSeeder.php @@ -0,0 +1,185 @@ + '9e46c6cf-358c-4bfe-8998-436b3a207fec'], + [ + 'name' => 'Γ–BB Departures', + 'user_id' => $user_id, + 'data_payload' => null, + 'data_stale_minutes' => 15, + 'data_strategy' => 'polling', + 'polling_url' => 'https://dbf.finalrewind.org/Wien%20Hbf.json?detailed=1&version=3&limit=8&admode=dep&hafas=%C3%96BB&platforms=1%2C2', + 'polling_verb' => 'get', + 'polling_header' => null, + 'render_markup' => null, + 'render_markup_view' => 'recipes.train', + 'detail_view_route' => null, + 'icon_url' => null, + 'flux_icon_name' => 'train-front', + ] + ); + + Plugin::updateOrCreate( + ['uuid' => '3b046eda-34e9-4232-b935-c33b989a284b'], + [ + 'name' => 'Weather', + 'user_id' => $user_id, + 'data_payload' => null, + 'data_stale_minutes' => 60, + 'data_strategy' => 'polling', + 'polling_url' => 'https://api.met.no/weatherapi/locationforecast/2.0/compact?lat=48.2083&lon=16.3731', + 'polling_verb' => 'get', + 'polling_header' => null, + 'render_markup' => null, + 'render_markup_view' => 'recipes.weather', + 'detail_view_route' => null, + 'icon_url' => null, + 'flux_icon_name' => 'sun', + ] + ); + + Plugin::updateOrCreate( + ['uuid' => '21464b16-5f5a-4099-a967-f5c915e3da54'], + [ + 'name' => 'Zen Quotes', + 'user_id' => $user_id, + 'data_payload' => null, + 'data_stale_minutes' => 720, + 'data_strategy' => 'polling', + 'polling_url' => 'https://zenquotes.io/api/today', + 'polling_verb' => 'get', + 'polling_header' => null, + 'render_markup' => null, + 'render_markup_view' => 'recipes.zen', + 'detail_view_route' => null, + 'icon_url' => null, + 'flux_icon_name' => 'chat-bubble-bottom-center', + ] + ); + + Plugin::updateOrCreate( + ['uuid' => '8d472959-400f-46ee-afb2-4a9f1cfd521f'], + [ + 'name' => 'This Day in History', + 'user_id' => $user_id, + 'data_payload' => null, + 'data_stale_minutes' => 720, + 'data_strategy' => 'polling', + 'polling_url' => 'https://raw.githubusercontent.com/jvivona/tidbyt-data/refs/heads/main/thisdayinhistwikipedia/thisdayinhist.json', + 'polling_verb' => 'get', + 'polling_header' => null, + 'render_markup' => null, + 'render_markup_view' => 'recipes.day-in-history', + 'detail_view_route' => null, + 'icon_url' => null, + 'flux_icon_name' => 'calendar', + ] + ); + + Plugin::updateOrCreate( + ['uuid' => '4349fdad-a273-450b-aa00-3d32f2de788d'], + [ + 'name' => 'Home Assistant', + 'user_id' => $user_id, + 'data_payload' => null, + 'data_stale_minutes' => 30, + 'data_strategy' => 'polling', + 'polling_url' => 'http://raspberrypi.local:8123/api/states', + 'polling_verb' => 'get', + 'polling_header' => 'Authorization: Bearer YOUR_API_KEY', + 'render_markup' => null, + 'render_markup_view' => 'recipes.home-assistant', + 'detail_view_route' => null, + 'icon_url' => null, + 'flux_icon_name' => 'thermometer', + ] + ); + + Plugin::updateOrCreate( + ['uuid' => 'be5f7e1f-3ad8-4d66-93b2-36f7d6dcbd80'], + [ + 'name' => 'Sunrise/Sunset', + 'user_id' => $user_id, + 'data_payload' => null, + 'data_stale_minutes' => 720, + 'data_strategy' => 'polling', + 'polling_url' => 'https://suntracker.me/?lat=48.2083&lon=16.3731', + 'polling_verb' => 'get', + 'polling_header' => null, + 'render_markup' => null, + 'render_markup_view' => 'recipes.sunrise-sunset', + 'detail_view_route' => null, + 'icon_url' => null, + 'flux_icon_name' => 'sunrise', + ] + ); + + Plugin::updateOrCreate( + ['uuid' => '82d3ee14-d578-4969-bda5-2bbf825435fe'], + [ + 'name' => 'Pollen Forecast', + 'user_id' => $user_id, + 'data_payload' => null, + 'data_stale_minutes' => 720, + 'data_strategy' => 'polling', + 'polling_url' => 'https://air-quality-api.open-meteo.com/v1/air-quality?latitude=48.2083&longitude=16.3731&hourly=alder_pollen,birch_pollen,grass_pollen,mugwort_pollen,ragweed_pollen¤t=alder_pollen,birch_pollen,grass_pollen,mugwort_pollen,ragweed_pollen&timezone=Europe%2FVienna&forecast_days=2', + 'polling_verb' => 'get', + 'polling_header' => null, + 'render_markup' => null, + 'render_markup_view' => 'recipes.pollen-forecast-eu', + 'detail_view_route' => null, + 'icon_url' => null, + '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/database/seeders/MashupPocSeeder.php b/database/seeders/MashupPocSeeder.php new file mode 100644 index 0000000..35060f8 --- /dev/null +++ b/database/seeders/MashupPocSeeder.php @@ -0,0 +1,50 @@ + 1, + 'name' => 'Mashup Test Playlist', + 'is_active' => true, + ]); + + // Create a playlist item with 1Tx1B layout using the new JSON structure + PlaylistItem::createMashup( + playlist: $playlist, + layout: '1Tx1B', + pluginIds: [2, 3], // Top and bottom plugins + name: 'Mashup 1Tx1B', + order: 1 + ); + + // Create another playlist item with 2x2 layout + PlaylistItem::createMashup( + playlist: $playlist, + layout: '1Lx1R', + pluginIds: [2, 6], // All four quadrants + name: 'Mashup Quadrant', + order: 2 + ); + + // Create a single plugin item (no mashup) + PlaylistItem::create([ + 'playlist_id' => $playlist->id, + 'plugin_id' => 1, + 'mashup' => null, + 'is_active' => true, + 'order' => 3, + ]); + } +} diff --git a/database/settings/2026_01_16_102043_create_update_settings.php b/database/settings/2026_01_16_102043_create_update_settings.php new file mode 100644 index 0000000..02be86d --- /dev/null +++ b/database/settings/2026_01_16_102043_create_update_settings.php @@ -0,0 +1,11 @@ +migrator->add('update.prereleases', false); + } +}; diff --git a/database/storage/.gitkeep b/database/storage/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/docker-compose.yml b/docker-compose.yml index d5fa69e..5978037 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -4,12 +4,18 @@ services: context: . dockerfile: Dockerfile ports: - - "4567:80" + - "4567:8080" environment: #- APP_KEY= + - PHP_OPCACHE_ENABLE=1 - TRMNL_PROXY_REFRESH_MINUTES=15 - # volumes: - # - ./database/database.sqlite:/var/www/html/database/database.sqlite - # - ./storage:/var/www/html/storage + - DB_DATABASE=database/storage/database.sqlite + volumes: + - database:/var/www/html/database/storage + - storage:/var/www/html/storage/app/public/images/generated restart: unless-stopped #platform: "linux/arm64/v8" +volumes: + database: + storage: + diff --git a/docker/nginx.conf b/docker/nginx.conf deleted file mode 100644 index 5f42b71..0000000 --- a/docker/nginx.conf +++ /dev/null @@ -1,17 +0,0 @@ -server { - listen 80; - server_name _; - root /var/www/html/public; - index index.php; - - location / { - try_files $uri $uri/ /index.php?$query_string; - } - - location ~ \.php$ { - fastcgi_pass 127.0.0.1:9000; - fastcgi_index index.php; - fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; - include fastcgi_params; - } -} \ No newline at end of file diff --git a/docker/php.ini b/docker/php.ini deleted file mode 100644 index 1b12c3e..0000000 --- a/docker/php.ini +++ /dev/null @@ -1,14 +0,0 @@ -[PHP] -memory_limit = 256M -max_execution_time = 60 -upload_max_filesize = 50M -post_max_size = 50M - -[opcache] -opcache.enable=1 -opcache.memory_consumption=128 -opcache.interned_strings_buffer=8 -opcache.max_accelerated_files=4000 -opcache.revalidate_freq=60 -opcache.fast_shutdown=1 -opcache.enable_cli=1 \ No newline at end of file diff --git a/docker/prod/docker-compose.yml b/docker/prod/docker-compose.yml new file mode 100644 index 0000000..eaa48fb --- /dev/null +++ b/docker/prod/docker-compose.yml @@ -0,0 +1,19 @@ +services: + app: + image: ghcr.io/usetrmnl/larapaper:latest + ports: + - "4567:8080" + environment: + # Generate the APP_KEY with `echo "base64:$(openssl rand -base64 32)"` + #- APP_KEY= + - PHP_OPCACHE_ENABLE=1 + - TRMNL_PROXY_REFRESH_MINUTES=15 + - DB_DATABASE=database/storage/database.sqlite + volumes: + - database:/var/www/html/database/storage + - storage:/var/www/html/storage/app/public/images/generated + restart: unless-stopped +volumes: + database: + storage: + diff --git a/docker/supervisord.conf b/docker/supervisord.conf deleted file mode 100644 index 07352b4..0000000 --- a/docker/supervisord.conf +++ /dev/null @@ -1,56 +0,0 @@ -[supervisord] -nodaemon=true -user=root -logfile=/var/log/supervisor/supervisord.log -pidfile=/var/run/supervisord.pid - -[program:php-fpm] -command=php-fpm -stdout_logfile=/dev/stdout -stdout_logfile_maxbytes=0 -stderr_logfile=/dev/stderr -stderr_logfile_maxbytes=0 - -[program:nginx] -command=nginx -g 'daemon off;' -stdout_logfile=/dev/stdout -stdout_logfile_maxbytes=0 -stderr_logfile=/dev/stderr -stderr_logfile_maxbytes=0 - -[program:laravel-queue] -command=php /var/www/html/artisan queue:work -autostart=true -autorestart=true -stdout_logfile=/dev/stdout -stdout_logfile_maxbytes=0 -stderr_logfile=/dev/stderr -stderr_logfile_maxbytes=0 -stopwaitsecs=3600 - -[program:laravel-scheduler] -command=php /var/www/html/artisan schedule:work -autostart=true -autorestart=true -stdout_logfile=/dev/stdout -stdout_logfile_maxbytes=0 -stderr_logfile=/dev/stderr -stderr_logfile_maxbytes=0 - -[program:laravel-setup] -command=/bin/sh -c "php /var/www/html/artisan storage:link >> /tmp/storage-link.done" -autostart=true -autorestart=false -startsecs=0 -exitcodes=0 -stdout_logfile=/dev/stdout -stderr_logfile=/dev/stderr - -[program:laravel-db-migrate] -command=/bin/sh -c "php /var/www/html/artisan migrate --force >> /tmp/migrate.done" -autostart=true -autorestart=false -startsecs=0 -exitcodes=0 -stdout_logfile=/dev/stdout -stderr_logfile=/dev/stderr diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md new file mode 100644 index 0000000..e2246bc --- /dev/null +++ b/docs/DEVELOPMENT.md @@ -0,0 +1,92 @@ +## Local Development + +#### Requirements + +* PHP >= 8.2 +* ext-imagick +* puppeteer [see Browsershot docs](https://spatie.be/docs/browsershot/v4/requirements) + +#### Clone the repository + +```bash +git clone git@github.com:usetrmnl/larapaper.git +``` + +#### Copy environment file + +```bash +cp .env.example .env +``` + +#### Install dependencies and build frontend + +```bash +composer install +npm i +npm run build +``` + +#### Generate application key + +```bash +php artisan key:generate +``` + +#### Run migrations + +```bash +php artisan migrate --seed +``` + +#### Link storage to expose public assets + +```bash +php artisan storage:link +``` + +#### Run the server + +To expose the built-in server to the local network, you can run the following command: + +```bash +php artisan serve --host=0.0.0.0 --port 4567 +``` + +--- + +## Docker +Use the provided Dockerfile, or docker-compose file to run the server in a container. + +#### .devcontainer + +Open this repository in Visual Studio Code with the Dev Containers extension installed. This will automatically build the devcontainer and start the server. + +Copy the .env.example.local to .env: + +```bash +cp .env.example.local .env +``` + +Run migrations and seed the database: + +```bash +php artisan migrate --seed +``` + +Link storage to expose public assets: + +```bash +php artisan storage:link +``` + +Server is ready. Visit tab "Ports" in VSCode and visit the "Forwarded Address" in your browser. + +Login with user / password `admin@example.com` / `admin@example.com` + +##### After Pull: Install Packages and Build Frontend + +```bash +composer install +npm i +npm run build +``` diff --git a/lang/de/custom_plugins.php b/lang/de/custom_plugins.php new file mode 100644 index 0000000..3fd8785 --- /dev/null +++ b/lang/de/custom_plugins.php @@ -0,0 +1,7 @@ + 'heute', + 'tomorrow' => 'morgen', + 'yesterday' => 'gestern', +]; diff --git a/package-lock.json b/package-lock.json index 9a5760f..cbcac8c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,18 +1,33 @@ { - "name": "laravel", + "name": "laravel-trmnl-server", "lockfileVersion": 3, "requires": true, "packages": { "": { + "name": "laravel-trmnl-server", "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/lang-yaml": "^6.1.2", + "@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.37.0", "tailwindcss": "^4.0.7", - "vite": "^6.0" + "vite": "^7.0.4" }, "optionalDependencies": { "@rollup/rollup-linux-x64-gnu": "4.9.5", @@ -21,32 +36,211 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.26.2", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", - "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", "license": "MIT", "dependencies": { - "@babel/helper-validator-identifier": "^7.25.9", + "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", - "picocolors": "^1.0.0" + "picocolors": "^1.1.1" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", - "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", + "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.2", + "resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.10.2.tgz", + "integrity": "sha512-vvX1fsih9HledO1c9zdotZYUZnE4xV0m6i3m25s5DIfXofuprk6cRcLUZvSk3CASUbwjQX21tOGbkY2BH8TpnQ==", + "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.1", + "resolved": "https://registry.npmjs.org/@codemirror/lang-liquid/-/lang-liquid-6.3.1.tgz", + "integrity": "sha512-S/jE/D7iij2Pu70AC65ME6AYWxOOcX20cSJvaPgY5w7m2sfxsArAcUAuUgm/CZCVmqoi9KiOlS7gj/gyLipABw==", + "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/lang-yaml": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/@codemirror/lang-yaml/-/lang-yaml-6.1.2.tgz", + "integrity": "sha512-dxrfG8w5Ce/QbT7YID7mWZFKhdhsaTNOYjOkSIMt1qmC4VQnXSDSYVHHHn8k6kJUfIhtLo8t1JJgltlxWdsITw==", + "license": "MIT", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.2.0", + "@lezer/lr": "^1.0.0", + "@lezer/yaml": "^1.0.0" + } + }, + "node_modules/@codemirror/language": { + "version": "6.12.1", + "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.12.1.tgz", + "integrity": "sha512-Fa6xkSiuGKc8XC8Cn96T+TQHYj4ZZ7RdFmXA3i9xe/3hLHfwPZdM+dqfX0Cp0zQklBKhVD8Yzc8LS45rkqcwpQ==", + "license": "MIT", + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.23.0", + "@lezer/common": "^1.5.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0", + "style-mod": "^4.0.0" + } + }, + "node_modules/@codemirror/lint": { + "version": "6.9.4", + "resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.9.4.tgz", + "integrity": "sha512-ABc9vJ8DEmvOWuH26P3i8FpMWPQkduD9Rvba5iwb6O3hxASgclm3T3krGo8NASXkHCidz6b++LWlzWIUfEPSWw==", + "license": "MIT", + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.35.0", + "crelt": "^1.0.5" + } + }, + "node_modules/@codemirror/search": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.6.0.tgz", + "integrity": "sha512-koFuNXcDvyyotWcgOnZGmY7LZqEOXZaaxD/j6n18TCLx2/9HieZJ5H6hs1g8FiRxBD0DNfs0nXn17g872RmYdw==", + "license": "MIT", + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.37.0", + "crelt": "^1.0.5" + } + }, + "node_modules/@codemirror/state": { + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.4.tgz", + "integrity": "sha512-8y7xqG/hpB53l25CIoit9/ngxdfoG+fx+V3SHBrinnhOtLvKHRyAJJuHzkWrR4YXXLX8eXBsejgAAxHUOdW1yw==", + "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.39.15", + "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.39.15.tgz", + "integrity": "sha512-aCWjgweIIXLBHh7bY6cACvXuyrZ0xGafjQ2VInjp4RM4gMfscK5uESiNdrH0pE+e1lZr2B4ONGsjchl2KsKZzg==", + "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.0", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.0.tgz", - "integrity": "sha512-O7vun9Sf8DFjH2UtqK8Ku3LkquL9SZL8OLY1T5NZkA34+wG3OQF7cl4Ql8vdNzM6fzBbYfLaiRLIOZ+2FOCgBQ==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz", + "integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==", "cpu": [ "ppc64" ], @@ -60,9 +254,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.0.tgz", - "integrity": "sha512-PTyWCYYiU0+1eJKmw21lWtC+d08JDZPQ5g+kFyxP0V+es6VPPSUhM6zk8iImp2jbV6GwjX4pap0JFbUQN65X1g==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz", + "integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==", "cpu": [ "arm" ], @@ -76,9 +270,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.0.tgz", - "integrity": "sha512-grvv8WncGjDSyUBjN9yHXNt+cq0snxXbDxy5pJtzMKGmmpPxeAmAhWxXI+01lU5rwZomDgD3kJwulEnhTRUd6g==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz", + "integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==", "cpu": [ "arm64" ], @@ -92,9 +286,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.0.tgz", - "integrity": "sha512-m/ix7SfKG5buCnxasr52+LI78SQ+wgdENi9CqyCXwjVR2X4Jkz+BpC3le3AoBPYTC9NHklwngVXvbJ9/Akhrfg==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz", + "integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==", "cpu": [ "x64" ], @@ -108,9 +302,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.0.tgz", - "integrity": "sha512-mVwdUb5SRkPayVadIOI78K7aAnPamoeFR2bT5nszFUZ9P8UpK4ratOdYbZZXYSqPKMHfS1wdHCJk1P1EZpRdvw==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz", + "integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==", "cpu": [ "arm64" ], @@ -124,9 +318,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.0.tgz", - "integrity": "sha512-DgDaYsPWFTS4S3nWpFcMn/33ZZwAAeAFKNHNa1QN0rI4pUjgqf0f7ONmXf6d22tqTY+H9FNdgeaAa+YIFUn2Rg==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz", + "integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==", "cpu": [ "x64" ], @@ -140,9 +334,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.0.tgz", - "integrity": "sha512-VN4ocxy6dxefN1MepBx/iD1dH5K8qNtNe227I0mnTRjry8tj5MRk4zprLEdG8WPyAPb93/e4pSgi1SoHdgOa4w==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz", + "integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==", "cpu": [ "arm64" ], @@ -156,9 +350,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.0.tgz", - "integrity": "sha512-mrSgt7lCh07FY+hDD1TxiTyIHyttn6vnjesnPoVDNmDfOmggTLXRv8Id5fNZey1gl/V2dyVK1VXXqVsQIiAk+A==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz", + "integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==", "cpu": [ "x64" ], @@ -172,9 +366,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.0.tgz", - "integrity": "sha512-vkB3IYj2IDo3g9xX7HqhPYxVkNQe8qTK55fraQyTzTX/fxaDtXiEnavv9geOsonh2Fd2RMB+i5cbhu2zMNWJwg==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz", + "integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==", "cpu": [ "arm" ], @@ -188,9 +382,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.0.tgz", - "integrity": "sha512-9QAQjTWNDM/Vk2bgBl17yWuZxZNQIF0OUUuPZRKoDtqF2k4EtYbpyiG5/Dk7nqeK6kIJWPYldkOcBqjXjrUlmg==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz", + "integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==", "cpu": [ "arm64" ], @@ -204,9 +398,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.0.tgz", - "integrity": "sha512-43ET5bHbphBegyeqLb7I1eYn2P/JYGNmzzdidq/w0T8E2SsYL1U6un2NFROFRg1JZLTzdCoRomg8Rvf9M6W6Gg==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz", + "integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==", "cpu": [ "ia32" ], @@ -220,9 +414,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.0.tgz", - "integrity": "sha512-fC95c/xyNFueMhClxJmeRIj2yrSMdDfmqJnyOY4ZqsALkDrrKJfIg5NTMSzVBr5YW1jf+l7/cndBfP3MSDpoHw==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz", + "integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==", "cpu": [ "loong64" ], @@ -236,9 +430,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.0.tgz", - "integrity": "sha512-nkAMFju7KDW73T1DdH7glcyIptm95a7Le8irTQNO/qtkoyypZAnjchQgooFUDQhNAy4iu08N79W4T4pMBwhPwQ==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz", + "integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==", "cpu": [ "mips64el" ], @@ -252,9 +446,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.0.tgz", - "integrity": "sha512-NhyOejdhRGS8Iwv+KKR2zTq2PpysF9XqY+Zk77vQHqNbo/PwZCzB5/h7VGuREZm1fixhs4Q/qWRSi5zmAiO4Fw==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz", + "integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==", "cpu": [ "ppc64" ], @@ -268,9 +462,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.0.tgz", - "integrity": "sha512-5S/rbP5OY+GHLC5qXp1y/Mx//e92L1YDqkiBbO9TQOvuFXM+iDqUNG5XopAnXoRH3FjIUDkeGcY1cgNvnXp/kA==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz", + "integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==", "cpu": [ "riscv64" ], @@ -284,9 +478,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.0.tgz", - "integrity": "sha512-XM2BFsEBz0Fw37V0zU4CXfcfuACMrppsMFKdYY2WuTS3yi8O1nFOhil/xhKTmE1nPmVyvQJjJivgDT+xh8pXJA==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz", + "integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==", "cpu": [ "s390x" ], @@ -300,9 +494,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.0.tgz", - "integrity": "sha512-9yl91rHw/cpwMCNytUDxwj2XjFpxML0y9HAOH9pNVQDpQrBxHy01Dx+vaMu0N1CKa/RzBD2hB4u//nfc+Sd3Cw==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz", + "integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==", "cpu": [ "x64" ], @@ -316,9 +510,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.0.tgz", - "integrity": "sha512-RuG4PSMPFfrkH6UwCAqBzauBWTygTvb1nxWasEJooGSJ/NwRw7b2HOwyRTQIU97Hq37l3npXoZGYMy3b3xYvPw==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz", + "integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==", "cpu": [ "arm64" ], @@ -332,9 +526,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.0.tgz", - "integrity": "sha512-jl+qisSB5jk01N5f7sPCsBENCOlPiS/xptD5yxOx2oqQfyourJwIKLRA2yqWdifj3owQZCL2sn6o08dBzZGQzA==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz", + "integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==", "cpu": [ "x64" ], @@ -348,9 +542,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.0.tgz", - "integrity": "sha512-21sUNbq2r84YE+SJDfaQRvdgznTD8Xc0oc3p3iW/a1EVWeNj/SdUCbm5U0itZPQYRuRTW20fPMWMpcrciH2EJw==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz", + "integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==", "cpu": [ "arm64" ], @@ -364,9 +558,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.0.tgz", - "integrity": "sha512-2gwwriSMPcCFRlPlKx3zLQhfN/2WjJ2NSlg5TKLQOJdV0mSxIcYNTMhk3H3ulL/cak+Xj0lY1Ym9ysDV1igceg==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz", + "integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==", "cpu": [ "x64" ], @@ -379,10 +573,26 @@ "node": ">=18" } }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz", + "integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@esbuild/sunos-x64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.0.tgz", - "integrity": "sha512-bxI7ThgLzPrPz484/S9jLlvUAHYMzy6I0XiU1ZMeAEOBcS0VePBFxh1JjTQt3Xiat5b6Oh4x7UC7IwKQKIJRIg==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz", + "integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==", "cpu": [ "x64" ], @@ -396,9 +606,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.0.tgz", - "integrity": "sha512-ZUAc2YK6JW89xTbXvftxdnYy3m4iHIkDtK3CLce8wg8M2L+YZhIvO1DKpxrd0Yr59AeNNkTiic9YLf6FTtXWMw==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz", + "integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==", "cpu": [ "arm64" ], @@ -412,9 +622,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.0.tgz", - "integrity": "sha512-eSNxISBu8XweVEWG31/JzjkIGbGIJN/TrRoiSVZwZ6pkC6VX4Im/WV2cz559/TXLcYbcrDN8JtKgd9DJVIo8GA==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz", + "integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==", "cpu": [ "ia32" ], @@ -428,9 +638,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.0.tgz", - "integrity": "sha512-ZENoHJBxA20C2zFzh6AI4fT6RraMzjYw4xKWemRTRmRVtN9c5DcH9r/f2ihEkMjOW5eGgrwCslG/+Y/3bL+DHQ==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz", + "integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==", "cpu": [ "x64" ], @@ -443,18 +653,160 @@ "node": ">=18" } }, + "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.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@lezer/common": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.5.1.tgz", + "integrity": "sha512-6YRVG9vBkaY7p1IVxL4s44n5nUnaNnGM2/AckNgYOnxTG2kWh1vR8BMxPseWPjRNpb5VtXnMpeYAEAADoRV1Iw==", + "license": "MIT" + }, + "node_modules/@lezer/css": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@lezer/css/-/css-1.3.1.tgz", + "integrity": "sha512-PYAKeUVBo3HFThruRyp/iK91SwiZJnzXh8QzkQlwijB5y+N5iB28+iLk78o2zmKqqV0uolNhCwFqB8LA7b0Svg==", + "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.13", + "resolved": "https://registry.npmjs.org/@lezer/html/-/html-1.3.13.tgz", + "integrity": "sha512-oI7n6NJml729m7pjm9lvLvmXbdoMoi2f+1pwSDJkl9d68zGr7a9Btz8NdHTGQZtW2DA25ybeuv/SyDb9D5tseg==", + "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.8", + "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.8.tgz", + "integrity": "sha512-bPWa0Pgx69ylNlMlPvBPryqeLYQjyJjqPx+Aupm5zydLIF3NE+6MMLT8Yi23Bd9cif9VS00aUebn+6fDIGBcDA==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.0.0" + } + }, + "node_modules/@lezer/yaml": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@lezer/yaml/-/yaml-1.0.4.tgz", + "integrity": "sha512-2lrrHqxalACEbxIbsjhqGpSW8kWpUKuY6RHgnSAFZa6qK62wvnPxA8hGOwOoDbwHcOFs5M4o27mjGu+P7TvBmw==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.4.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.7.1", - "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.7.1.tgz", - "integrity": "sha512-MK7rtm8JjaxPN7Mf1JdZIZKPD2Z+W7osvrC1vjpvfOX1K0awDIHYbNi89f7eotp7eMUn2shWnt03HwVbriXtKQ==", + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.12.0.tgz", + "integrity": "sha512-Xuq42yxcQJ54ti8ZHNzF5snFvtpgXzNToJ1bXUGQRaiO8t+B6UM8sTUJfvV+AJnqtkJU/7hdy6nbKyA12aHtRw==", "license": "Apache-2.0", "dependencies": { - "debug": "^4.4.0", + "debug": "^4.4.3", "extract-zip": "^2.0.1", "progress": "^2.0.3", "proxy-agent": "^6.5.0", - "semver": "^7.7.0", - "tar-fs": "^3.0.8", + "semver": "^7.7.3", + "tar-fs": "^3.1.1", "yargs": "^17.7.2" }, "bin": { @@ -465,9 +817,9 @@ } }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.34.9", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.34.9.tgz", - "integrity": "sha512-qZdlImWXur0CFakn2BJ2znJOdqYZKiedEPEVNTBrpfPjc/YuTGcaYZcdmNFTkUj3DU0ZM/AElcM8Ybww3xVLzA==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz", + "integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==", "cpu": [ "arm" ], @@ -478,9 +830,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.34.9", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.34.9.tgz", - "integrity": "sha512-4KW7P53h6HtJf5Y608T1ISKvNIYLWRKMvfnG0c44M6In4DQVU58HZFEVhWINDZKp7FZps98G3gxwC1sb0wXUUg==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz", + "integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==", "cpu": [ "arm64" ], @@ -491,9 +843,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.34.9", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.34.9.tgz", - "integrity": "sha512-0CY3/K54slrzLDjOA7TOjN1NuLKERBgk9nY5V34mhmuu673YNb+7ghaDUs6N0ujXR7fz5XaS5Aa6d2TNxZd0OQ==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz", + "integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==", "cpu": [ "arm64" ], @@ -504,9 +856,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.34.9", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.34.9.tgz", - "integrity": "sha512-eOojSEAi/acnsJVYRxnMkPFqcxSMFfrw7r2iD9Q32SGkb/Q9FpUY1UlAu1DH9T7j++gZ0lHjnm4OyH2vCI7l7Q==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz", + "integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==", "cpu": [ "x64" ], @@ -517,9 +869,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.34.9", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.34.9.tgz", - "integrity": "sha512-2lzjQPJbN5UnHm7bHIUKFMulGTQwdvOkouJDpPysJS+QFBGDJqcfh+CxxtG23Ik/9tEvnebQiylYoazFMAgrYw==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz", + "integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==", "cpu": [ "arm64" ], @@ -530,9 +882,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.34.9", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.34.9.tgz", - "integrity": "sha512-SLl0hi2Ah2H7xQYd6Qaiu01kFPzQ+hqvdYSoOtHYg/zCIFs6t8sV95kaoqjzjFwuYQLtOI0RZre/Ke0nPaQV+g==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz", + "integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==", "cpu": [ "x64" ], @@ -543,9 +895,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.34.9", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.34.9.tgz", - "integrity": "sha512-88I+D3TeKItrw+Y/2ud4Tw0+3CxQ2kLgu3QvrogZ0OfkmX/DEppehus7L3TS2Q4lpB+hYyxhkQiYPJ6Mf5/dPg==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz", + "integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==", "cpu": [ "arm" ], @@ -556,9 +908,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.34.9", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.34.9.tgz", - "integrity": "sha512-3qyfWljSFHi9zH0KgtEPG4cBXHDFhwD8kwg6xLfHQ0IWuH9crp005GfoUUh/6w9/FWGBwEHg3lxK1iHRN1MFlA==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz", + "integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==", "cpu": [ "arm" ], @@ -569,9 +921,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.34.9", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.34.9.tgz", - "integrity": "sha512-6TZjPHjKZUQKmVKMUowF3ewHxctrRR09eYyvT5eFv8w/fXarEra83A2mHTVJLA5xU91aCNOUnM+DWFMSbQ0Nxw==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz", + "integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==", "cpu": [ "arm64" ], @@ -582,9 +934,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.34.9", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.34.9.tgz", - "integrity": "sha512-LD2fytxZJZ6xzOKnMbIpgzFOuIKlxVOpiMAXawsAZ2mHBPEYOnLRK5TTEsID6z4eM23DuO88X0Tq1mErHMVq0A==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz", + "integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==", "cpu": [ "arm64" ], @@ -594,10 +946,10 @@ "linux" ] }, - "node_modules/@rollup/rollup-linux-loongarch64-gnu": { - "version": "4.34.9", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.34.9.tgz", - "integrity": "sha512-dRAgTfDsn0TE0HI6cmo13hemKpVHOEyeciGtvlBTkpx/F65kTvShtY/EVyZEIfxFkV5JJTuQ9tP5HGBS0hfxIg==", + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz", + "integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==", "cpu": [ "loong64" ], @@ -607,10 +959,36 @@ "linux" ] }, - "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.34.9", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.34.9.tgz", - "integrity": "sha512-PHcNOAEhkoMSQtMf+rJofwisZqaU8iQ8EaSps58f5HYll9EAY5BSErCZ8qBDMVbq88h4UxaNPlbrKqfWP8RfJA==", + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz", + "integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz", + "integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz", + "integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==", "cpu": [ "ppc64" ], @@ -621,9 +999,22 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.34.9", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.34.9.tgz", - "integrity": "sha512-Z2i0Uy5G96KBYKjeQFKbbsB54xFOL5/y1P5wNBsbXB8yE+At3oh0DVMjQVzCJRJSfReiB2tX8T6HUFZ2k8iaKg==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz", + "integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz", + "integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==", "cpu": [ "riscv64" ], @@ -634,9 +1025,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.34.9", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.34.9.tgz", - "integrity": "sha512-U+5SwTMoeYXoDzJX5dhDTxRltSrIax8KWwfaaYcynuJw8mT33W7oOgz0a+AaXtGuvhzTr2tVKh5UO8GVANTxyQ==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz", + "integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==", "cpu": [ "s390x" ], @@ -660,9 +1051,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.34.9", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.34.9.tgz", - "integrity": "sha512-cYRpV4650z2I3/s6+5/LONkjIz8MBeqrk+vPXV10ORBnshpn8S32bPqQ2Utv39jCiDcO2eJTuSlPXpnvmaIgRA==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz", + "integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==", "cpu": [ "x64" ], @@ -672,10 +1063,36 @@ "linux" ] }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz", + "integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz", + "integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.34.9", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.34.9.tgz", - "integrity": "sha512-z4mQK9dAN6byRA/vsSgQiPeuO63wdiDxZ9yg9iyX2QTzKuQM7T4xlBoeUP/J8uiFkqxkcWndWi+W7bXdPbt27Q==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz", + "integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==", "cpu": [ "arm64" ], @@ -686,9 +1103,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.34.9", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.34.9.tgz", - "integrity": "sha512-KB48mPtaoHy1AwDNkAJfHXvHp24H0ryZog28spEs0V48l3H1fr4i37tiyHsgKZJnCmvxsbATdZGBpbmxTE3a9w==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz", + "integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==", "cpu": [ "ia32" ], @@ -698,10 +1115,23 @@ "win32" ] }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz", + "integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.34.9", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.34.9.tgz", - "integrity": "sha512-AyleYRPU7+rgkMWbEh71fQlrzRfeP6SyMnRf9XX4fCdDPAJumdSBqYEcWPMzVQ4ScAl7E4oFfK0GUVn77xSwbw==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz", + "integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==", "cpu": [ "x64" ], @@ -712,42 +1142,47 @@ ] }, "node_modules/@tailwindcss/node": { - "version": "4.0.9", - "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.0.9.tgz", - "integrity": "sha512-tOJvdI7XfJbARYhxX+0RArAhmuDcczTC46DGCEziqxzzbIaPnfYaIyRT31n4u8lROrsO7Q6u/K9bmQHL2uL1bQ==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.2.0.tgz", + "integrity": "sha512-Yv+fn/o2OmL5fh/Ir62VXItdShnUxfpkMA4Y7jdeC8O81WPB8Kf6TT6GSHvnqgSwDzlB5iT7kDpeXxLsUS0T6Q==", "license": "MIT", "dependencies": { - "enhanced-resolve": "^5.18.1", - "jiti": "^2.4.2", - "tailwindcss": "4.0.9" + "@jridgewell/remapping": "^2.3.5", + "enhanced-resolve": "^5.19.0", + "jiti": "^2.6.1", + "lightningcss": "1.31.1", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.2.0" } }, "node_modules/@tailwindcss/oxide": { - "version": "4.0.9", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.0.9.tgz", - "integrity": "sha512-eLizHmXFqHswJONwfqi/WZjtmWZpIalpvMlNhTM99/bkHtUs6IqgI1XQ0/W5eO2HiRQcIlXUogI2ycvKhVLNcA==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.2.0.tgz", + "integrity": "sha512-AZqQzADaj742oqn2xjl5JbIOzZB/DGCYF/7bpvhA8KvjUj9HJkag6bBuwZvH1ps6dfgxNHyuJVlzSr2VpMgdTQ==", "license": "MIT", "engines": { - "node": ">= 10" + "node": ">= 20" }, "optionalDependencies": { - "@tailwindcss/oxide-android-arm64": "4.0.9", - "@tailwindcss/oxide-darwin-arm64": "4.0.9", - "@tailwindcss/oxide-darwin-x64": "4.0.9", - "@tailwindcss/oxide-freebsd-x64": "4.0.9", - "@tailwindcss/oxide-linux-arm-gnueabihf": "4.0.9", - "@tailwindcss/oxide-linux-arm64-gnu": "4.0.9", - "@tailwindcss/oxide-linux-arm64-musl": "4.0.9", - "@tailwindcss/oxide-linux-x64-gnu": "4.0.9", - "@tailwindcss/oxide-linux-x64-musl": "4.0.9", - "@tailwindcss/oxide-win32-arm64-msvc": "4.0.9", - "@tailwindcss/oxide-win32-x64-msvc": "4.0.9" + "@tailwindcss/oxide-android-arm64": "4.2.0", + "@tailwindcss/oxide-darwin-arm64": "4.2.0", + "@tailwindcss/oxide-darwin-x64": "4.2.0", + "@tailwindcss/oxide-freebsd-x64": "4.2.0", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.0", + "@tailwindcss/oxide-linux-arm64-gnu": "4.2.0", + "@tailwindcss/oxide-linux-arm64-musl": "4.2.0", + "@tailwindcss/oxide-linux-x64-gnu": "4.2.0", + "@tailwindcss/oxide-linux-x64-musl": "4.2.0", + "@tailwindcss/oxide-wasm32-wasi": "4.2.0", + "@tailwindcss/oxide-win32-arm64-msvc": "4.2.0", + "@tailwindcss/oxide-win32-x64-msvc": "4.2.0" } }, "node_modules/@tailwindcss/oxide-android-arm64": { - "version": "4.0.9", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.0.9.tgz", - "integrity": "sha512-YBgy6+2flE/8dbtrdotVInhMVIxnHJPbAwa7U1gX4l2ThUIaPUp18LjB9wEH8wAGMBZUb//SzLtdXXNBHPUl6Q==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.2.0.tgz", + "integrity": "sha512-F0QkHAVaW/JNBWl4CEKWdZ9PMb0khw5DCELAOnu+RtjAfx5Zgw+gqCHFvqg3AirU1IAd181fwOtJQ5I8Yx5wtw==", "cpu": [ "arm64" ], @@ -757,13 +1192,13 @@ "android" ], "engines": { - "node": ">= 10" + "node": ">= 20" } }, "node_modules/@tailwindcss/oxide-darwin-arm64": { - "version": "4.0.9", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.0.9.tgz", - "integrity": "sha512-pWdl4J2dIHXALgy2jVkwKBmtEb73kqIfMpYmcgESr7oPQ+lbcQ4+tlPeVXaSAmang+vglAfFpXQCOvs/aGSqlw==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.2.0.tgz", + "integrity": "sha512-I0QylkXsBsJMZ4nkUNSR04p6+UptjcwhcVo3Zu828ikiEqHjVmQL9RuQ6uT/cVIiKpvtVA25msu/eRV97JeNSA==", "cpu": [ "arm64" ], @@ -773,13 +1208,13 @@ "darwin" ], "engines": { - "node": ">= 10" + "node": ">= 20" } }, "node_modules/@tailwindcss/oxide-darwin-x64": { - "version": "4.0.9", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.0.9.tgz", - "integrity": "sha512-4Dq3lKp0/C7vrRSkNPtBGVebEyWt9QPPlQctxJ0H3MDyiQYvzVYf8jKow7h5QkWNe8hbatEqljMj/Y0M+ERYJg==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.2.0.tgz", + "integrity": "sha512-6TmQIn4p09PBrmnkvbYQ0wbZhLtbaksCDx7Y7R3FYYx0yxNA7xg5KP7dowmQ3d2JVdabIHvs3Hx4K3d5uCf8xg==", "cpu": [ "x64" ], @@ -789,13 +1224,13 @@ "darwin" ], "engines": { - "node": ">= 10" + "node": ">= 20" } }, "node_modules/@tailwindcss/oxide-freebsd-x64": { - "version": "4.0.9", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.0.9.tgz", - "integrity": "sha512-k7U1RwRODta8x0uealtVt3RoWAWqA+D5FAOsvVGpYoI6ObgmnzqWW6pnVwz70tL8UZ/QXjeMyiICXyjzB6OGtQ==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.2.0.tgz", + "integrity": "sha512-qBudxDvAa2QwGlq9y7VIzhTvp2mLJ6nD/G8/tI70DCDoneaUeLWBJaPcbfzqRIWraj+o969aDQKvKW9dvkUizw==", "cpu": [ "x64" ], @@ -805,13 +1240,13 @@ "freebsd" ], "engines": { - "node": ">= 10" + "node": ">= 20" } }, "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { - "version": "4.0.9", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.0.9.tgz", - "integrity": "sha512-NDDjVweHz2zo4j+oS8y3KwKL5wGCZoXGA9ruJM982uVJLdsF8/1AeKvUwKRlMBpxHt1EdWJSAh8a0Mfhl28GlQ==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.2.0.tgz", + "integrity": "sha512-7XKkitpy5NIjFZNUQPeUyNJNJn1CJeV7rmMR+exHfTuOsg8rxIO9eNV5TSEnqRcaOK77zQpsyUkBWmPy8FgdSg==", "cpu": [ "arm" ], @@ -821,13 +1256,13 @@ "linux" ], "engines": { - "node": ">= 10" + "node": ">= 20" } }, "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { - "version": "4.0.9", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.0.9.tgz", - "integrity": "sha512-jk90UZ0jzJl3Dy1BhuFfRZ2KP9wVKMXPjmCtY4U6fF2LvrjP5gWFJj5VHzfzHonJexjrGe1lMzgtjriuZkxagg==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.2.0.tgz", + "integrity": "sha512-Mff5a5Q3WoQR01pGU1gr29hHM1N93xYrKkGXfPw/aRtK4bOc331Ho4Tgfsm5WDGvpevqMpdlkCojT3qlCQbCpA==", "cpu": [ "arm64" ], @@ -837,13 +1272,13 @@ "linux" ], "engines": { - "node": ">= 10" + "node": ">= 20" } }, "node_modules/@tailwindcss/oxide-linux-arm64-musl": { - "version": "4.0.9", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.0.9.tgz", - "integrity": "sha512-3eMjyTC6HBxh9nRgOHzrc96PYh1/jWOwHZ3Kk0JN0Kl25BJ80Lj9HEvvwVDNTgPg154LdICwuFLuhfgH9DULmg==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.2.0.tgz", + "integrity": "sha512-XKcSStleEVnbH6W/9DHzZv1YhjE4eSS6zOu2eRtYAIh7aV4o3vIBs+t/B15xlqoxt6ef/0uiqJVB6hkHjWD/0A==", "cpu": [ "arm64" ], @@ -853,13 +1288,13 @@ "linux" ], "engines": { - "node": ">= 10" + "node": ">= 20" } }, "node_modules/@tailwindcss/oxide-linux-x64-gnu": { - "version": "4.0.9", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.0.9.tgz", - "integrity": "sha512-v0D8WqI/c3WpWH1kq/HP0J899ATLdGZmENa2/emmNjubT0sWtEke9W9+wXeEoACuGAhF9i3PO5MeyditpDCiWQ==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.2.0.tgz", + "integrity": "sha512-/hlXCBqn9K6fi7eAM0RsobHwJYa5V/xzWspVTzxnX+Ft9v6n+30Pz8+RxCn7sQL/vRHHLS30iQPrHQunu6/vJA==", "cpu": [ "x64" ], @@ -869,13 +1304,13 @@ "linux" ], "engines": { - "node": ">= 10" + "node": ">= 20" } }, "node_modules/@tailwindcss/oxide-linux-x64-musl": { - "version": "4.0.9", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.0.9.tgz", - "integrity": "sha512-Kvp0TCkfeXyeehqLJr7otsc4hd/BUPfcIGrQiwsTVCfaMfjQZCG7DjI+9/QqPZha8YapLA9UoIcUILRYO7NE1Q==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.2.0.tgz", + "integrity": "sha512-lKUaygq4G7sWkhQbfdRRBkaq4LY39IriqBQ+Gk6l5nKq6Ay2M2ZZb1tlIyRNgZKS8cbErTwuYSor0IIULC0SHw==", "cpu": [ "x64" ], @@ -885,13 +1320,42 @@ "linux" ], "engines": { - "node": ">= 10" + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.2.0.tgz", + "integrity": "sha512-xuDjhAsFdUuFP5W9Ze4k/o4AskUtI8bcAGU4puTYprr89QaYFmhYOPfP+d1pH+k9ets6RoE23BXZM1X1jJqoyw==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.8.1", + "@emnapi/runtime": "^1.8.1", + "@emnapi/wasi-threads": "^1.1.0", + "@napi-rs/wasm-runtime": "^1.1.1", + "@tybys/wasm-util": "^0.10.1", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14.0.0" } }, "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { - "version": "4.0.9", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.0.9.tgz", - "integrity": "sha512-m3+60T/7YvWekajNq/eexjhV8z10rswcz4BC9bioJ7YaN+7K8W2AmLmG0B79H14m6UHE571qB0XsPus4n0QVgQ==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.2.0.tgz", + "integrity": "sha512-2UU/15y1sWDEDNJXxEIrfWKC2Yb4YgIW5Xz2fKFqGzFWfoMHWFlfa1EJlGO2Xzjkq/tvSarh9ZTjvbxqWvLLXA==", "cpu": [ "arm64" ], @@ -901,13 +1365,13 @@ "win32" ], "engines": { - "node": ">= 10" + "node": ">= 20" } }, "node_modules/@tailwindcss/oxide-win32-x64-msvc": { - "version": "4.0.9", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.0.9.tgz", - "integrity": "sha512-dpc05mSlqkwVNOUjGu/ZXd5U1XNch1kHFJ4/cHkZFvaW1RzbHmRt24gvM8/HC6IirMxNarzVw4IXVtvrOoZtxA==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.2.0.tgz", + "integrity": "sha512-CrFadmFoc+z76EV6LPG1jx6XceDsaCG3lFhyLNo/bV9ByPrE+FnBPckXQVP4XRkN76h3Fjt/a+5Er/oA/nCBvQ==", "cpu": [ "x64" ], @@ -917,22 +1381,21 @@ "win32" ], "engines": { - "node": ">= 10" + "node": ">= 20" } }, "node_modules/@tailwindcss/vite": { - "version": "4.0.9", - "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.0.9.tgz", - "integrity": "sha512-BIKJO+hwdIsN7V6I7SziMZIVHWWMsV/uCQKYEbeiGRDRld+TkqyRRl9+dQ0MCXbhcVr+D9T/qX2E84kT7V281g==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.2.0.tgz", + "integrity": "sha512-da9mFCaHpoOgtQiWtDGIikTrSpUFBtIZCG3jy/u2BGV+l/X1/pbxzmIUxNt6JWm19N3WtGi4KlJdSH/Si83WOA==", "license": "MIT", "dependencies": { - "@tailwindcss/node": "4.0.9", - "@tailwindcss/oxide": "4.0.9", - "lightningcss": "^1.29.1", - "tailwindcss": "4.0.9" + "@tailwindcss/node": "4.2.0", + "@tailwindcss/oxide": "4.2.0", + "tailwindcss": "4.2.0" }, "peerDependencies": { - "vite": "^5.2.0 || ^6" + "vite": "^5.2.0 || ^6 || ^7" } }, "node_modules/@tootallnate/quickjs-emscripten": { @@ -942,19 +1405,19 @@ "license": "MIT" }, "node_modules/@types/estree": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", - "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", "license": "MIT" }, "node_modules/@types/node": { - "version": "22.13.9", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.9.tgz", - "integrity": "sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw==", + "version": "25.3.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.3.0.tgz", + "integrity": "sha512-4K3bqJpXpqfg2XKGK9bpDTc6xO/xoUP/RBWS7AtRMug6zZFaRekiLzjVtAoZMquxoAbzBvy5nxQ7veS5eYzf8A==", "license": "MIT", "optional": true, "dependencies": { - "undici-types": "~6.20.0" + "undici-types": "~7.18.0" } }, "node_modules/@types/yauzl": { @@ -968,9 +1431,9 @@ } }, "node_modules/agent-base": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz", - "integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==", + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", "license": "MIT", "engines": { "node": ">= 14" @@ -1025,9 +1488,9 @@ "license": "MIT" }, "node_modules/autoprefixer": { - "version": "10.4.20", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.20.tgz", - "integrity": "sha512-XY25y5xSv/wEoqzDyXXME4AFfkZI0P23z6Fs3YgymDnKJkCGOnkL0iTxCa85UTqaSgfcqyf3UA6+c7wUvx/16g==", + "version": "10.4.24", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.24.tgz", + "integrity": "sha512-uHZg7N9ULTVbutaIsDRoUkoS8/h3bdsmVJYZ5l3wv8Cp/6UIIoRDm90hZ+BwxUj/hGBEzLxdHNSKuFpn8WOyZw==", "funding": [ { "type": "opencollective", @@ -1044,11 +1507,10 @@ ], "license": "MIT", "dependencies": { - "browserslist": "^4.23.3", - "caniuse-lite": "^1.0.30001646", - "fraction.js": "^4.3.7", - "normalize-range": "^0.1.2", - "picocolors": "^1.0.1", + "browserslist": "^4.28.1", + "caniuse-lite": "^1.0.30001766", + "fraction.js": "^5.3.4", + "picocolors": "^1.1.1", "postcss-value-parser": "^4.2.0" }, "bin": { @@ -1062,48 +1524,73 @@ } }, "node_modules/axios": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.8.2.tgz", - "integrity": "sha512-ls4GYBm5aig9vWx8AWDSGLpnpDQRtWAfrjU+EuytuODrFBkqesN2RkOQCBzrA1RQNHw1SmRMSDDDSwzNAYQ6Rg==", + "version": "1.13.5", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.5.tgz", + "integrity": "sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q==", "license": "MIT", "dependencies": { - "follow-redirects": "^1.15.6", - "form-data": "^4.0.0", + "follow-redirects": "^1.15.11", + "form-data": "^4.0.5", "proxy-from-env": "^1.1.0" } }, "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.8.0", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.8.0.tgz", + "integrity": "sha512-qRuSmNSkGQaHwNbM7J78Wwy+ghLEYF1zNrSeMxj4Kgw6y33O3mXcQ6Ie9fRvfU/YnxWkOchPXbaLb73TkIsfdg==", + "license": "Apache-2.0", + "peerDependencies": { + "react-native-b4a": "*" + }, + "peerDependenciesMeta": { + "react-native-b4a": { + "optional": true + } + } }, "node_modules/bare-events": { - "version": "2.5.4", - "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.5.4.tgz", - "integrity": "sha512-+gFfDkR8pj4/TrWCGUGWmJIkBwuxPS5F+a5yWjOHQt2hHvNZd5YLzadjmDUtFmMM4y429bnKLa8bYBMHcYdnQA==", + "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.0.1", - "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.0.1.tgz", - "integrity": "sha512-ilQs4fm/l9eMfWY2dY0WCIUplSUp7U0CT1vrqMg1MUdeZl4fypu5UP0XcDBK5WBQPJAKP1b7XEodISmekH/CEg==", + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.5.4.tgz", + "integrity": "sha512-POK4oplfA7P7gqvetNmCs4CNtm9fNsx+IAh7jH7GgU0OJdge2rso0R20TNWVq6VoWcCvsTdlNDaleLHGaKx8CA==", "license": "Apache-2.0", "optional": true, "dependencies": { - "bare-events": "^2.0.0", + "bare-events": "^2.5.4", "bare-path": "^3.0.0", - "bare-stream": "^2.0.0" + "bare-stream": "^2.6.4", + "bare-url": "^2.2.2", + "fast-fifo": "^1.3.2" }, "engines": { - "bare": ">=1.7.0" + "bare": ">=1.16.0" + }, + "peerDependencies": { + "bare-buffer": "*" + }, + "peerDependenciesMeta": { + "bare-buffer": { + "optional": true + } } }, "node_modules/bare-os": { - "version": "3.5.1", - "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.5.1.tgz", - "integrity": "sha512-LvfVNDcWLw2AnIw5f2mWUgumW3I3N/WYGiWeimhQC1Ybt71n2FjlS9GJKeCnFeg1MKZHxzIFmpFnBXDI+sBeFg==", + "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": { @@ -1121,13 +1608,14 @@ } }, "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.8.0", + "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.8.0.tgz", + "integrity": "sha512-reUN0M2sHRqCdG4lUK3Fw8w98eeUIZHL5c3H7Mbhk2yVBL+oofgaIp0ieLfD5QXwPCypBpmEEKU2WZKzbAk8GA==", "license": "Apache-2.0", "optional": true, "dependencies": { - "streamx": "^2.21.0" + "streamx": "^2.21.0", + "teex": "^1.0.1" }, "peerDependencies": { "bare-buffer": "*", @@ -1142,19 +1630,41 @@ } } }, + "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.10.0", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.0.tgz", + "integrity": "sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA==", + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/basic-ftp": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.0.5.tgz", - "integrity": "sha512-4Bcg1P8xhUuqcii/S0Z9wiHIrQVPMermM1any+MX5GeGD7faD3/msQUDGLol9wOcz4/jbg/WJnGqoJF6LiBdtg==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.2.0.tgz", + "integrity": "sha512-VoMINM2rqJwJgfdHq6RiUudKt2BV+FY5ZFezP/ypmwayk68+NzzAQy4XXLlqsGD4MCzq3DrmNFD/uUmBJuGoXw==", "license": "MIT", "engines": { "node": ">=10.0.0" } }, "node_modules/browserslist": { - "version": "4.24.4", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.4.tgz", - "integrity": "sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==", + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", "funding": [ { "type": "opencollective", @@ -1171,10 +1681,11 @@ ], "license": "MIT", "dependencies": { - "caniuse-lite": "^1.0.30001688", - "electron-to-chromium": "^1.5.73", - "node-releases": "^2.0.19", - "update-browserslist-db": "^1.1.1" + "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" @@ -1215,9 +1726,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001702", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001702.tgz", - "integrity": "sha512-LoPe/D7zioC0REI5W73PeR1e1MLCipRGq/VkovJnd6Df+QVqT+vT33OXCp8QUd7kA7RZrHWxb1B36OQKI/0gOA==", + "version": "1.0.30001770", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001770.tgz", + "integrity": "sha512-x/2CLQ1jHENRbHg5PSId2sXq1CIO1CISvwWAj027ltMVG2UNgW+w9oH2+HzgEIRFembL8bUlXtfbBHR1fCg2xw==", "funding": [ { "type": "opencollective", @@ -1263,12 +1774,13 @@ } }, "node_modules/chromium-bidi": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-2.1.2.tgz", - "integrity": "sha512-vtRWBK2uImo5/W2oG6/cDkkHSm+2t6VHgnj+Rcwhb0pP74OoUb4GipyRX/T/y39gYQPhioP0DPShn+A7P6CHNw==", + "version": "13.1.0", + "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-13.1.0.tgz", + "integrity": "sha512-IdGNojX6S04+wgJOALzvkkIyLelhEGqI8xSctwiYJJGSi9T2eBjwAQW2UjBD/mCXv/rUkNlH2+h7jz+58vT74A==", "license": "Apache-2.0", "dependencies": { "mitt": "^3.0.1", + "puppeteer": "^24.36.0", "zod": "^3.24.1" }, "peerDependencies": { @@ -1289,6 +1801,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", @@ -1320,18 +1847,17 @@ } }, "node_modules/concurrently": { - "version": "9.1.2", - "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.1.2.tgz", - "integrity": "sha512-H9MWcoPsYddwbOGM6difjVwVZHl63nwMEwDJG/L7VGtuaJhb12h2caPG2tVPWs7emuYix252iGfqOyrz1GczTQ==", + "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", @@ -1370,6 +1896,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", @@ -1380,9 +1912,9 @@ } }, "node_modules/debug": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", - "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "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" @@ -1420,21 +1952,18 @@ } }, "node_modules/detect-libc": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", - "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==", + "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", - "bin": { - "detect-libc": "bin/detect-libc.js" - }, "engines": { - "node": ">=0.10" + "node": ">=8" } }, "node_modules/devtools-protocol": { - "version": "0.0.1402036", - "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1402036.tgz", - "integrity": "sha512-JwAYQgEvm3yD45CHB+RmF5kMbWtXBaOGwuxa87sZogHcLCv8c/IqnThaoQ1y60d7pXWjSKWQphPEc+1rAScVdg==", + "version": "0.0.1566079", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1566079.tgz", + "integrity": "sha512-MJfAEA1UfVhSs7fbSQOG4czavUp1ajfg6prlAN0+cmfa2zNjaIbvq8VneP7do1WAQQIvgNJWSMeP6UyI90gIlQ==", "license": "BSD-3-Clause" }, "node_modules/dunder-proto": { @@ -1452,9 +1981,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.112", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.112.tgz", - "integrity": "sha512-oen93kVyqSb3l+ziUgzIOlWt/oOuy4zRmpwestMn4rhFWAoFJeFuCVte9F2fASjeZZo7l/Cif9TiyrdW4CwEMA==", + "version": "1.5.302", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.302.tgz", + "integrity": "sha512-sM6HAN2LyK82IyPBpznDRqlTQAtuSaO+ShzFiWTvoMJLHyZ+Y39r8VMfHzwbU8MVBzQ4Wdn85+wlZl2TLGIlwg==", "license": "ISC" }, "node_modules/emoji-regex": { @@ -1464,22 +1993,22 @@ "license": "MIT" }, "node_modules/end-of-stream": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", - "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", "license": "MIT", "dependencies": { "once": "^1.4.0" } }, "node_modules/enhanced-resolve": { - "version": "5.18.1", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.1.tgz", - "integrity": "sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==", + "version": "5.19.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.19.0.tgz", + "integrity": "sha512-phv3E1Xl4tQOShqSte26C7Fl84EwUdZsyOuSSk9qtAGyyQs2s3jJzComh+Abf4g187lUUAvH+H26omrqia2aGg==", "license": "MIT", "dependencies": { "graceful-fs": "^4.2.4", - "tapable": "^2.2.0" + "tapable": "^2.3.0" }, "engines": { "node": ">=10.13.0" @@ -1495,9 +2024,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" @@ -1549,9 +2078,9 @@ } }, "node_modules/esbuild": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.0.tgz", - "integrity": "sha512-BXq5mqc8ltbaN34cDqWuYKyNhX8D/Z0J1xdtdQ8UcIIIyJyz+ZMKUt58tF3SrZ85jcfN/PZYhjR5uDQAYNVbuw==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz", + "integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==", "hasInstallScript": true, "license": "MIT", "bin": { @@ -1561,31 +2090,32 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.0", - "@esbuild/android-arm": "0.25.0", - "@esbuild/android-arm64": "0.25.0", - "@esbuild/android-x64": "0.25.0", - "@esbuild/darwin-arm64": "0.25.0", - "@esbuild/darwin-x64": "0.25.0", - "@esbuild/freebsd-arm64": "0.25.0", - "@esbuild/freebsd-x64": "0.25.0", - "@esbuild/linux-arm": "0.25.0", - "@esbuild/linux-arm64": "0.25.0", - "@esbuild/linux-ia32": "0.25.0", - "@esbuild/linux-loong64": "0.25.0", - "@esbuild/linux-mips64el": "0.25.0", - "@esbuild/linux-ppc64": "0.25.0", - "@esbuild/linux-riscv64": "0.25.0", - "@esbuild/linux-s390x": "0.25.0", - "@esbuild/linux-x64": "0.25.0", - "@esbuild/netbsd-arm64": "0.25.0", - "@esbuild/netbsd-x64": "0.25.0", - "@esbuild/openbsd-arm64": "0.25.0", - "@esbuild/openbsd-x64": "0.25.0", - "@esbuild/sunos-x64": "0.25.0", - "@esbuild/win32-arm64": "0.25.0", - "@esbuild/win32-ia32": "0.25.0", - "@esbuild/win32-x64": "0.25.0" + "@esbuild/aix-ppc64": "0.27.3", + "@esbuild/android-arm": "0.27.3", + "@esbuild/android-arm64": "0.27.3", + "@esbuild/android-x64": "0.27.3", + "@esbuild/darwin-arm64": "0.27.3", + "@esbuild/darwin-x64": "0.27.3", + "@esbuild/freebsd-arm64": "0.27.3", + "@esbuild/freebsd-x64": "0.27.3", + "@esbuild/linux-arm": "0.27.3", + "@esbuild/linux-arm64": "0.27.3", + "@esbuild/linux-ia32": "0.27.3", + "@esbuild/linux-loong64": "0.27.3", + "@esbuild/linux-mips64el": "0.27.3", + "@esbuild/linux-ppc64": "0.27.3", + "@esbuild/linux-riscv64": "0.27.3", + "@esbuild/linux-s390x": "0.27.3", + "@esbuild/linux-x64": "0.27.3", + "@esbuild/netbsd-arm64": "0.27.3", + "@esbuild/netbsd-x64": "0.27.3", + "@esbuild/openbsd-arm64": "0.27.3", + "@esbuild/openbsd-x64": "0.27.3", + "@esbuild/openharmony-arm64": "0.27.3", + "@esbuild/sunos-x64": "0.27.3", + "@esbuild/win32-arm64": "0.27.3", + "@esbuild/win32-ia32": "0.27.3", + "@esbuild/win32-x64": "0.27.3" } }, "node_modules/escalade": { @@ -1649,6 +2179,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", @@ -1684,10 +2223,27 @@ "pend": "~1.2.0" } }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, "node_modules/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", @@ -1705,14 +2261,15 @@ } }, "node_modules/form-data": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz", - "integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==", + "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", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", "mime-types": "^2.1.12" }, "engines": { @@ -1720,15 +2277,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" } }, @@ -1817,9 +2374,9 @@ } }, "node_modules/get-uri": { - "version": "6.0.4", - "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.4.tgz", - "integrity": "sha512-E1b1lFFLvLgak2whF2xDBcOy6NLVGZBqqjJjsIhvopKfWWEi64pLVTWWehV8KlLerZkfNTA95sTe2OdJKm1OzQ==", + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.5.tgz", + "integrity": "sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg==", "license": "MIT", "dependencies": { "basic-ftp": "^5.0.2", @@ -1939,14 +2496,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" } @@ -1967,9 +2520,9 @@ } }, "node_modules/jiti": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.4.2.tgz", - "integrity": "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==", + "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" @@ -1982,9 +2535,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" @@ -1993,12 +2546,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", @@ -2006,9 +2553,9 @@ "license": "MIT" }, "node_modules/laravel-vite-plugin": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/laravel-vite-plugin/-/laravel-vite-plugin-1.2.0.tgz", - "integrity": "sha512-R0pJ+IcTVeqEMoKz/B2Ij57QVq3sFTABiFmb06gAwFdivbOgsUtuhX6N2MGLEArajrS3U5JbberzwOe7uXHMHQ==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/laravel-vite-plugin/-/laravel-vite-plugin-2.1.0.tgz", + "integrity": "sha512-z+ck2BSV6KWtYcoIzk9Y5+p4NEjqM+Y4i8/H+VZRLq0OgNjW2DqyADquwYu5j8qRvaXwzNmfCWl1KrMlV1zpsg==", "license": "MIT", "dependencies": { "picocolors": "^1.0.0", @@ -2018,19 +2565,19 @@ "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.29.1", - "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.29.1.tgz", - "integrity": "sha512-FmGoeD4S05ewj+AkhTY+D+myDvXI6eL27FjHIjoyUkO/uw7WZD1fBVs0QxeYWa7E17CUHJaYX/RUGISCtcrG4Q==", + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.31.1.tgz", + "integrity": "sha512-l51N2r93WmGUye3WuFoN5k10zyvrVs0qfKBhyC5ogUQ6Ew6JUSswh78mbSO+IU3nTWsyOArqPCcShdQSadghBQ==", "license": "MPL-2.0", "dependencies": { - "detect-libc": "^1.0.3" + "detect-libc": "^2.0.3" }, "engines": { "node": ">= 12.0.0" @@ -2040,22 +2587,43 @@ "url": "https://opencollective.com/parcel" }, "optionalDependencies": { - "lightningcss-darwin-arm64": "1.29.1", - "lightningcss-darwin-x64": "1.29.1", - "lightningcss-freebsd-x64": "1.29.1", - "lightningcss-linux-arm-gnueabihf": "1.29.1", - "lightningcss-linux-arm64-gnu": "1.29.1", - "lightningcss-linux-arm64-musl": "1.29.1", - "lightningcss-linux-x64-gnu": "1.29.1", - "lightningcss-linux-x64-musl": "1.29.1", - "lightningcss-win32-arm64-msvc": "1.29.1", - "lightningcss-win32-x64-msvc": "1.29.1" + "lightningcss-android-arm64": "1.31.1", + "lightningcss-darwin-arm64": "1.31.1", + "lightningcss-darwin-x64": "1.31.1", + "lightningcss-freebsd-x64": "1.31.1", + "lightningcss-linux-arm-gnueabihf": "1.31.1", + "lightningcss-linux-arm64-gnu": "1.31.1", + "lightningcss-linux-arm64-musl": "1.31.1", + "lightningcss-linux-x64-gnu": "1.31.1", + "lightningcss-linux-x64-musl": "1.31.1", + "lightningcss-win32-arm64-msvc": "1.31.1", + "lightningcss-win32-x64-msvc": "1.31.1" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.31.1.tgz", + "integrity": "sha512-HXJF3x8w9nQ4jbXRiNppBCqeZPIAfUo8zE/kOEGbW5NZvGc/K7nMxbhIr+YlFlHW5mpbg/YFPdbnCh1wAXCKFg==", + "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.29.1", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.29.1.tgz", - "integrity": "sha512-HtR5XJ5A0lvCqYAoSv2QdZZyoHNttBpa5EP9aNuzBQeKGfbyH5+UipLWvVzpP4Uml5ej4BYs5I9Lco9u1fECqw==", + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.31.1.tgz", + "integrity": "sha512-02uTEqf3vIfNMq3h/z2cJfcOXnQ0GRwQrkmPafhueLb2h7mqEidiCzkE4gBMEH65abHRiQvhdcQ+aP0D0g67sg==", "cpu": [ "arm64" ], @@ -2073,9 +2641,9 @@ } }, "node_modules/lightningcss-darwin-x64": { - "version": "1.29.1", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.29.1.tgz", - "integrity": "sha512-k33G9IzKUpHy/J/3+9MCO4e+PzaFblsgBjSGlpAaFikeBFm8B/CkO3cKU9oI4g+fjS2KlkLM/Bza9K/aw8wsNA==", + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.31.1.tgz", + "integrity": "sha512-1ObhyoCY+tGxtsz1lSx5NXCj3nirk0Y0kB/g8B8DT+sSx4G9djitg9ejFnjb3gJNWo7qXH4DIy2SUHvpoFwfTA==", "cpu": [ "x64" ], @@ -2093,9 +2661,9 @@ } }, "node_modules/lightningcss-freebsd-x64": { - "version": "1.29.1", - "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.29.1.tgz", - "integrity": "sha512-0SUW22fv/8kln2LnIdOCmSuXnxgxVC276W5KLTwoehiO0hxkacBxjHOL5EtHD8BAXg2BvuhsJPmVMasvby3LiQ==", + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.31.1.tgz", + "integrity": "sha512-1RINmQKAItO6ISxYgPwszQE1BrsVU5aB45ho6O42mu96UiZBxEXsuQ7cJW4zs4CEodPUioj/QrXW1r9pLUM74A==", "cpu": [ "x64" ], @@ -2113,9 +2681,9 @@ } }, "node_modules/lightningcss-linux-arm-gnueabihf": { - "version": "1.29.1", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.29.1.tgz", - "integrity": "sha512-sD32pFvlR0kDlqsOZmYqH/68SqUMPNj+0pucGxToXZi4XZgZmqeX/NkxNKCPsswAXU3UeYgDSpGhu05eAufjDg==", + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.31.1.tgz", + "integrity": "sha512-OOCm2//MZJ87CdDK62rZIu+aw9gBv4azMJuA8/KB74wmfS3lnC4yoPHm0uXZ/dvNNHmnZnB8XLAZzObeG0nS1g==", "cpu": [ "arm" ], @@ -2133,9 +2701,9 @@ } }, "node_modules/lightningcss-linux-arm64-gnu": { - "version": "1.29.1", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.29.1.tgz", - "integrity": "sha512-0+vClRIZ6mmJl/dxGuRsE197o1HDEeeRk6nzycSy2GofC2JsY4ifCRnvUWf/CUBQmlrvMzt6SMQNMSEu22csWQ==", + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.31.1.tgz", + "integrity": "sha512-WKyLWztD71rTnou4xAD5kQT+982wvca7E6QoLpoawZ1gP9JM0GJj4Tp5jMUh9B3AitHbRZ2/H3W5xQmdEOUlLg==", "cpu": [ "arm64" ], @@ -2153,9 +2721,9 @@ } }, "node_modules/lightningcss-linux-arm64-musl": { - "version": "1.29.1", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.29.1.tgz", - "integrity": "sha512-UKMFrG4rL/uHNgelBsDwJcBqVpzNJbzsKkbI3Ja5fg00sgQnHw/VrzUTEc4jhZ+AN2BvQYz/tkHu4vt1kLuJyw==", + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.31.1.tgz", + "integrity": "sha512-mVZ7Pg2zIbe3XlNbZJdjs86YViQFoJSpc41CbVmKBPiGmC4YrfeOyz65ms2qpAobVd7WQsbW4PdsSJEMymyIMg==", "cpu": [ "arm64" ], @@ -2173,9 +2741,9 @@ } }, "node_modules/lightningcss-linux-x64-gnu": { - "version": "1.29.1", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.29.1.tgz", - "integrity": "sha512-u1S+xdODy/eEtjADqirA774y3jLcm8RPtYztwReEXoZKdzgsHYPl0s5V52Tst+GKzqjebkULT86XMSxejzfISw==", + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.31.1.tgz", + "integrity": "sha512-xGlFWRMl+0KvUhgySdIaReQdB4FNudfUTARn7q0hh/V67PVGCs3ADFjw+6++kG1RNd0zdGRlEKa+T13/tQjPMA==", "cpu": [ "x64" ], @@ -2193,9 +2761,9 @@ } }, "node_modules/lightningcss-linux-x64-musl": { - "version": "1.29.1", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.29.1.tgz", - "integrity": "sha512-L0Tx0DtaNUTzXv0lbGCLB/c/qEADanHbu4QdcNOXLIe1i8i22rZRpbT3gpWYsCh9aSL9zFujY/WmEXIatWvXbw==", + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.31.1.tgz", + "integrity": "sha512-eowF8PrKHw9LpoZii5tdZwnBcYDxRw2rRCyvAXLi34iyeYfqCQNA9rmUM0ce62NlPhCvof1+9ivRaTY6pSKDaA==", "cpu": [ "x64" ], @@ -2213,9 +2781,9 @@ } }, "node_modules/lightningcss-win32-arm64-msvc": { - "version": "1.29.1", - "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.29.1.tgz", - "integrity": "sha512-QoOVnkIEFfbW4xPi+dpdft/zAKmgLgsRHfJalEPYuJDOWf7cLQzYg0DEh8/sn737FaeMJxHZRc1oBreiwZCjog==", + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.31.1.tgz", + "integrity": "sha512-aJReEbSEQzx1uBlQizAOBSjcmr9dCdL3XuC/6HLXAxmtErsj2ICo5yYggg1qOODQMtnjNQv2UHb9NpOuFtYe4w==", "cpu": [ "arm64" ], @@ -2233,9 +2801,9 @@ } }, "node_modules/lightningcss-win32-x64-msvc": { - "version": "1.29.1", - "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.29.1.tgz", - "integrity": "sha512-NygcbThNBe4JElP+olyTI/doBNGJvLs3bFCRPdvuCcxZCcCZ71B858IHpdm7L1btZex0FvCmM17FK98Y9MRy1Q==", + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.31.1.tgz", + "integrity": "sha512-I9aiFrbd7oYHwlnQDqr1Roz+fTz61oDDJX7n9tYF9FJymH1cIN1DtKw3iYt6b8WZgEjoNwVSncwF4wx/ZedMhw==", "cpu": [ "x64" ], @@ -2258,12 +2826,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", @@ -2273,6 +2835,15 @@ "node": ">=12" } }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -2316,9 +2887,9 @@ "license": "MIT" }, "node_modules/nanoid": { - "version": "3.3.8", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz", - "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==", + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", "funding": [ { "type": "github", @@ -2343,20 +2914,11 @@ } }, "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": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", - "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -2441,21 +3003,21 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "license": "MIT", "engines": { - "node": ">=8.6" + "node": ">=12" }, "funding": { "url": "https://github.com/sponsors/jonschlinkert" } }, "node_modules/postcss": { - "version": "8.5.3", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz", - "integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==", + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", "funding": [ { "type": "opencollective", @@ -2472,7 +3034,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.8", + "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -2521,9 +3083,9 @@ "license": "MIT" }, "node_modules/pump": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz", - "integrity": "sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz", + "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==", "license": "MIT", "dependencies": { "end-of-stream": "^1.1.0", @@ -2531,17 +3093,17 @@ } }, "node_modules/puppeteer": { - "version": "24.3.1", - "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-24.3.1.tgz", - "integrity": "sha512-k0OJ7itRwkr06owp0CP3f/PsRD7Pdw4DjoCUZvjGr+aNgS1z6n/61VajIp0uBjl+V5XAQO1v/3k9bzeZLWs9OQ==", + "version": "24.37.0", + "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-24.37.0.tgz", + "integrity": "sha512-s1jHugVhPtQjiJE6wUyonj4VEGWF+mfRDASqPMPsXgKcjZX0GaznBmcT9nLQ7bBL90phuQUqO4jiV5vTecZg4g==", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { - "@puppeteer/browsers": "2.7.1", - "chromium-bidi": "2.1.2", + "@puppeteer/browsers": "2.12.0", + "chromium-bidi": "13.1.0", "cosmiconfig": "^9.0.0", - "devtools-protocol": "0.0.1402036", - "puppeteer-core": "24.3.1", + "devtools-protocol": "0.0.1566079", + "puppeteer-core": "24.37.0", "typed-query-selector": "^2.12.0" }, "bin": { @@ -2552,17 +3114,18 @@ } }, "node_modules/puppeteer-core": { - "version": "24.3.1", - "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-24.3.1.tgz", - "integrity": "sha512-585ccfcTav4KmlSmYbwwOSeC8VdutQHn2Fuk0id/y/9OoeO7Gg5PK1aUGdZjEmos0TAq+pCpChqFurFbpNd3wA==", + "version": "24.37.0", + "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-24.37.0.tgz", + "integrity": "sha512-WoCBK36cBlbaxwuvPWhOp2+lR6O6ynHdDuvD8rEIkxPOPpUoMXSJuyiOWhHtexJBCLaMCAJk33QdYambvQl+og==", "license": "Apache-2.0", "dependencies": { - "@puppeteer/browsers": "2.7.1", - "chromium-bidi": "2.1.2", - "debug": "^4.4.0", - "devtools-protocol": "0.0.1402036", + "@puppeteer/browsers": "2.12.0", + "chromium-bidi": "13.1.0", + "debug": "^4.4.3", + "devtools-protocol": "0.0.1566079", "typed-query-selector": "^2.12.0", - "ws": "^8.18.1" + "webdriver-bidi-protocol": "0.4.0", + "ws": "^8.19.0" }, "engines": { "node": ">=18" @@ -2587,12 +3150,12 @@ } }, "node_modules/rollup": { - "version": "4.34.9", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.34.9.tgz", - "integrity": "sha512-nF5XYqWWp9hx/LrpC8sZvvvmq0TeTjQgaZHYmAgwysT9nh8sWnZhBnM8ZyVbbJFIQBLwHDNoMqsBZBbUo4U8sQ==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz", + "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==", "license": "MIT", "dependencies": { - "@types/estree": "1.0.6" + "@types/estree": "1.0.8" }, "bin": { "rollup": "dist/bin/rollup" @@ -2602,32 +3165,38 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.34.9", - "@rollup/rollup-android-arm64": "4.34.9", - "@rollup/rollup-darwin-arm64": "4.34.9", - "@rollup/rollup-darwin-x64": "4.34.9", - "@rollup/rollup-freebsd-arm64": "4.34.9", - "@rollup/rollup-freebsd-x64": "4.34.9", - "@rollup/rollup-linux-arm-gnueabihf": "4.34.9", - "@rollup/rollup-linux-arm-musleabihf": "4.34.9", - "@rollup/rollup-linux-arm64-gnu": "4.34.9", - "@rollup/rollup-linux-arm64-musl": "4.34.9", - "@rollup/rollup-linux-loongarch64-gnu": "4.34.9", - "@rollup/rollup-linux-powerpc64le-gnu": "4.34.9", - "@rollup/rollup-linux-riscv64-gnu": "4.34.9", - "@rollup/rollup-linux-s390x-gnu": "4.34.9", - "@rollup/rollup-linux-x64-gnu": "4.34.9", - "@rollup/rollup-linux-x64-musl": "4.34.9", - "@rollup/rollup-win32-arm64-msvc": "4.34.9", - "@rollup/rollup-win32-ia32-msvc": "4.34.9", - "@rollup/rollup-win32-x64-msvc": "4.34.9", + "@rollup/rollup-android-arm-eabi": "4.59.0", + "@rollup/rollup-android-arm64": "4.59.0", + "@rollup/rollup-darwin-arm64": "4.59.0", + "@rollup/rollup-darwin-x64": "4.59.0", + "@rollup/rollup-freebsd-arm64": "4.59.0", + "@rollup/rollup-freebsd-x64": "4.59.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", + "@rollup/rollup-linux-arm-musleabihf": "4.59.0", + "@rollup/rollup-linux-arm64-gnu": "4.59.0", + "@rollup/rollup-linux-arm64-musl": "4.59.0", + "@rollup/rollup-linux-loong64-gnu": "4.59.0", + "@rollup/rollup-linux-loong64-musl": "4.59.0", + "@rollup/rollup-linux-ppc64-gnu": "4.59.0", + "@rollup/rollup-linux-ppc64-musl": "4.59.0", + "@rollup/rollup-linux-riscv64-gnu": "4.59.0", + "@rollup/rollup-linux-riscv64-musl": "4.59.0", + "@rollup/rollup-linux-s390x-gnu": "4.59.0", + "@rollup/rollup-linux-x64-gnu": "4.59.0", + "@rollup/rollup-linux-x64-musl": "4.59.0", + "@rollup/rollup-openbsd-x64": "4.59.0", + "@rollup/rollup-openharmony-arm64": "4.59.0", + "@rollup/rollup-win32-arm64-msvc": "4.59.0", + "@rollup/rollup-win32-ia32-msvc": "4.59.0", + "@rollup/rollup-win32-x64-gnu": "4.59.0", + "@rollup/rollup-win32-x64-msvc": "4.59.0", "fsevents": "~2.3.2" } }, "node_modules/rollup/node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.34.9", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.34.9.tgz", - "integrity": "sha512-FwBHNSOjUTQLP4MG7y6rR6qbGw4MFeQnIBrMe161QGaQoBQLqSUEKlHIiVgF3g/mb3lxlxzJOpIBhaP+C+KP2A==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz", + "integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==", "cpu": [ "x64" ], @@ -2647,9 +3216,9 @@ } }, "node_modules/semver": { - "version": "7.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", - "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -2659,9 +3228,9 @@ } }, "node_modules/shell-quote": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.2.tgz", - "integrity": "sha512-AzqKpGKjrj7EM6rKVQEPpB288oCfnrEIuyoT9cyF4nmGa7V8Zk6f7RRqYisX8X9m+Q7bd632aZW4ky7EhbQztA==", + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz", + "integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==", "license": "MIT", "engines": { "node": ">= 0.4" @@ -2681,12 +3250,12 @@ } }, "node_modules/socks": { - "version": "2.8.4", - "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.4.tgz", - "integrity": "sha512-D3YaD0aRxR3mEcqnidIs7ReYJFVzWdd6fXJYUM8ixcQcJRGTka/b3saV0KflYhyVJXKhb947GndU35SxYNResQ==", + "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": { @@ -2727,23 +3296,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.0", - "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.22.0.tgz", - "integrity": "sha512-sLh1evHOzBy/iWRiR6d1zRcLao4gGZr3C1kzNz4fopCOKJb6xD9ub8Mpi9Mr1R6id5o43S+d93fI48UC5uM9aw==", + "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": { @@ -2772,6 +3333,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", @@ -2788,24 +3355,28 @@ } }, "node_modules/tailwindcss": { - "version": "4.0.9", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.0.9.tgz", - "integrity": "sha512-12laZu+fv1ONDRoNR9ipTOpUD7RN9essRVkX36sjxuRUInpN7hIiHN4lBd/SIFjbISvnXzp8h/hXzmU8SQQYhw==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.0.tgz", + "integrity": "sha512-yYzTZ4++b7fNYxFfpnberEEKu43w44aqDMNM9MHMmcKuCH7lL8jJ4yJ7LGHv7rSwiqM0nkiobF9I6cLlpS2P7Q==", "license": "MIT" }, "node_modules/tapable": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", - "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", + "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" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" } }, "node_modules/tar-fs": { - "version": "3.0.8", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.8.tgz", - "integrity": "sha512-ZoROL70jptorGAlgAYiLoBLItEKw/fUxg9BSYK/dF/GAGYFJOJJJMvjPAKDJraCXFwadD456FCuvLWgfhMsPwg==", + "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", @@ -2827,15 +3398,41 @@ "streamx": "^2.15.0" } }, + "node_modules/teex": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/teex/-/teex-1.0.1.tgz", + "integrity": "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==", + "license": "MIT", + "optional": true, + "dependencies": { + "streamx": "^2.12.5" + } + }, "node_modules/text-decoder": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.3.tgz", - "integrity": "sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA==", + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.7.tgz", + "integrity": "sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==", "license": "Apache-2.0", "dependencies": { "b4a": "^1.6.4" } }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, "node_modules/tree-kill": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", @@ -2858,16 +3455,16 @@ "license": "MIT" }, "node_modules/undici-types": { - "version": "6.20.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", - "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", "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.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", "funding": [ { "type": "opencollective", @@ -2895,20 +3492,23 @@ } }, "node_modules/vite": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/vite/-/vite-6.2.0.tgz", - "integrity": "sha512-7dPxoo+WsT/64rDcwoOjk76XHj+TqNTIvHKcuMQ1k4/SeHDaQt5GFAeLYzrimZrMpn/O6DtdI03WUjdxuPM0oQ==", + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", + "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", "license": "MIT", "dependencies": { - "esbuild": "^0.25.0", - "postcss": "^8.5.3", - "rollup": "^4.30.1" + "esbuild": "^0.27.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" }, "bin": { "vite": "bin/vite.js" }, "engines": { - "node": "^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" @@ -2917,14 +3517,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" @@ -2975,6 +3575,30 @@ "picomatch": "^2.3.1" } }, + "node_modules/vite-plugin-full-reload/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "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.4.0", + "resolved": "https://registry.npmjs.org/webdriver-bidi-protocol/-/webdriver-bidi-protocol-0.4.0.tgz", + "integrity": "sha512-U9VIlNRrq94d1xxR9JrCEAx5Gv/2W7ERSv8oWRoNe/QYbfccS0V3h/H6qeNeCRJxXGMhhnkqvwNrvPAYeuP9VA==", + "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", @@ -2999,9 +3623,9 @@ "license": "ISC" }, "node_modules/ws": { - "version": "8.18.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.1.tgz", - "integrity": "sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w==", + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", + "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", "license": "MIT", "engines": { "node": ">=10.0.0" @@ -3066,9 +3690,9 @@ } }, "node_modules/zod": { - "version": "3.24.2", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.2.tgz", - "integrity": "sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ==", + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" diff --git a/package.json b/package.json index 0a537fe..ec6e486 100644 --- a/package.json +++ b/package.json @@ -6,14 +6,28 @@ "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/lang-yaml": "^6.1.2", + "@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.37.0", "tailwindcss": "^4.0.7", - "vite": "^6.0" + "vite": "^7.0.4" }, "optionalDependencies": { "@rollup/rollup-linux-x64-gnu": "4.9.5", diff --git a/phpstan.neon b/phpstan.neon new file mode 100644 index 0000000..e5d841f --- /dev/null +++ b/phpstan.neon @@ -0,0 +1,10 @@ +includes: + - vendor/larastan/larastan/extension.neon + - vendor/nesbot/carbon/extension.neon + +parameters: + + paths: + - app/ + + level: 4 diff --git a/pint.json b/pint.json new file mode 100644 index 0000000..6b39126 --- /dev/null +++ b/pint.json @@ -0,0 +1,34 @@ +{ + "preset": "laravel", + "rules": { + "array_push": true, + "backtick_to_shell_exec": true, + "date_time_immutable": true, + "lowercase_keywords": true, + "lowercase_static_reference": true, + "final_internal_class": true, + "final_public_method_for_abstract_class": true, + "fully_qualified_strict_types": true, + "global_namespace_import": { + "import_classes": true, + "import_constants": true, + "import_functions": true + }, + "mb_str_functions": true, + "modernize_types_casting": true, + "new_with_parentheses": false, + "no_superfluous_elseif": true, + "no_useless_else": true, + "no_multiple_statements_per_line": true, + "ordered_interfaces": true, + "ordered_traits": true, + "protected_to_private": true, + "self_accessor": true, + "self_static_accessor": true, + "strict_comparison": true, + "visibility_required": true, + "increment_style": { + "style": "pre" + } + } +} diff --git a/public/favicon.ico b/public/favicon.ico index e69de29..da17cd5 100644 Binary files a/public/favicon.ico and b/public/favicon.ico differ 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..02e99cf --- /dev/null +++ b/public/mirror/index.html @@ -0,0 +1,907 @@ + + + + + + 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..be9e15d --- /dev/null +++ b/resources/js/codemirror-core.js @@ -0,0 +1,268 @@ +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 { yaml } from '@codemirror/lang-yaml'; +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, + 'yaml': yaml, + 'yml': yaml, +}; + +// 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/action-message.blade.php b/resources/views/components/action-message.blade.php index db63acf..d313ee6 100644 --- a/resources/views/components/action-message.blade.php +++ b/resources/views/components/action-message.blade.php @@ -8,7 +8,7 @@ x-show.transition.out.opacity.duration.1500ms="shown" x-transition:leave.opacity.duration.1500ms style="display: none" - {{ $attributes->merge(['class' => 'text-sm text-gray-600']) }} + {{ $attributes->merge(['class' => 'text-sm']) }} > {{ $slot->isEmpty() ? __('Saved.') : $slot }}
diff --git a/resources/views/components/app-logo.blade.php b/resources/views/components/app-logo.blade.php index 35c3c07..f50634a 100644 --- a/resources/views/components/app-logo.blade.php +++ b/resources/views/components/app-logo.blade.php @@ -2,5 +2,9 @@
- Laravel TRMNL Server + @if(config('app.pixel_logo_enabled')) + + @else + LaraPaper + @endif
diff --git a/resources/views/components/auth-header.blade.php b/resources/views/components/auth-header.blade.php index a68f69d..78aa2e5 100644 --- a/resources/views/components/auth-header.blade.php +++ b/resources/views/components/auth-header.blade.php @@ -3,7 +3,11 @@ 'description', ]) -
-

{{ $title }}

-

{{ $description }}

+
+ @if(config('app.pixel_logo_enabled')) + + @else + LaraPaper + @endif + {{ $description }}
diff --git a/resources/views/components/desktop-user-menu.blade.php b/resources/views/components/desktop-user-menu.blade.php new file mode 100644 index 0000000..5b386c5 --- /dev/null +++ b/resources/views/components/desktop-user-menu.blade.php @@ -0,0 +1,39 @@ + + only('name') }} + :initials="auth()->user()->initials()" + icon:trailing="chevrons-up-down" + data-test="sidebar-menu-button" + /> + + +
+ +
+ {{ auth()->user()->name }} + {{ auth()->user()->email }} +
+
+ + + + {{ __('Settings') }} + +
+ @csrf + + {{ __('Log Out') }} + +
+
+
+
diff --git a/resources/views/components/layouts/app/sidebar.blade.php b/resources/views/components/layouts/app/sidebar.blade.php deleted file mode 100644 index d0e913e..0000000 --- a/resources/views/components/layouts/app/sidebar.blade.php +++ /dev/null @@ -1,132 +0,0 @@ - - - - @include('partials.head') - - - - - - - - - - - - Dashboard - - - - - - - - Repository - - - - Documentation - - - - - - - - - -
-
- - - {{ auth()->user()->initials() }} - - - -
- {{ auth()->user()->name }} - {{ auth()->user()->email }} -
-
-
-
- - - - - Settings - - - - -
- @csrf - - {{ __('Log Out') }} - -
-
-
-
- - - - - - - - - - - - -
-
- - - {{ auth()->user()->initials() }} - - - -
- {{ auth()->user()->name }} - {{ auth()->user()->email }} -
-
-
-
- - - - - Settings - - - - -
- @csrf - - {{ __('Log Out') }} - -
-
-
-
- - {{ $slot }} - - @fluxScripts - - diff --git a/resources/views/components/layouts/auth.blade.php b/resources/views/components/layouts/auth.blade.php deleted file mode 100644 index 4ddd14d..0000000 --- a/resources/views/components/layouts/auth.blade.php +++ /dev/null @@ -1,3 +0,0 @@ - - {{ $slot }} - diff --git a/resources/views/components/settings/layout.blade.php b/resources/views/components/settings/layout.blade.php deleted file mode 100644 index 1c2a9b7..0000000 --- a/resources/views/components/settings/layout.blade.php +++ /dev/null @@ -1,20 +0,0 @@ -
-
- - Profile - Password - Appearance - -
- - - -
- {{ $heading ?? '' }} - {{ $subheading ?? '' }} - -
- {{ $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..7f0d084 --- /dev/null +++ b/resources/views/default-screens/error.blade.php @@ -0,0 +1,25 @@ +@props([ + 'noBleed' => false, + 'darkMode' => false, + 'deviceVariant' => 'og', + 'deviceOrientation' => null, + 'colorDepth' => '1bit', + 'scaleLevel' => null, + 'cssVariables' => 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..9113eb6 --- /dev/null +++ b/resources/views/default-screens/setup.blade.php @@ -0,0 +1,24 @@ +@props([ + 'noBleed' => false, + 'darkMode' => false, + 'deviceVariant' => 'og', + 'deviceOrientation' => null, + 'colorDepth' => '1bit', + 'scaleLevel' => null, + 'cssVariables' => null, +]) + + + + + + Welcome to LaraPaper! + 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..ef0a80c --- /dev/null +++ b/resources/views/default-screens/sleep.blade.php @@ -0,0 +1,30 @@ +@props([ + 'noBleed' => false, + 'darkMode' => true, + 'deviceVariant' => 'og', + 'deviceOrientation' => null, + 'colorDepth' => '1bit', + 'scaleLevel' => null, + 'cssVariables' => null, +]) + + + + + +
+ + + +
+ Sleep Mode +
+
+ +
+
diff --git a/resources/views/flux/icon/flower.blade.php b/resources/views/flux/icon/flower.blade.php new file mode 100644 index 0000000..ddb1459 --- /dev/null +++ b/resources/views/flux/icon/flower.blade.php @@ -0,0 +1,50 @@ +{{-- 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/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/flux/icon/mashup-1Lx1R.blade.php b/resources/views/flux/icon/mashup-1Lx1R.blade.php new file mode 100644 index 0000000..75d1a3d --- /dev/null +++ b/resources/views/flux/icon/mashup-1Lx1R.blade.php @@ -0,0 +1,39 @@ +{{-- Credit: Lucide (https://lucide.dev) --}} + +@props([ + 'variant' => 'outline', +]) + +@php +$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, + default => 2, +}; +@endphp + +class($classes) }} + data-flux-icon + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 76 44" + fill="none" + stroke="currentColor" + stroke-width="{{ $strokeWidth }}" + stroke-linecap="round" + stroke-linejoin="round" + aria-hidden="true" + data-slot="icon" +> + + + diff --git a/resources/views/flux/icon/mashup-1Lx2R.blade.php b/resources/views/flux/icon/mashup-1Lx2R.blade.php new file mode 100644 index 0000000..5794416 --- /dev/null +++ b/resources/views/flux/icon/mashup-1Lx2R.blade.php @@ -0,0 +1,40 @@ +{{-- Credit: Lucide (https://lucide.dev) --}} + +@props([ + 'variant' => 'outline', +]) + +@php +$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, + default => 2, +}; +@endphp + +class($classes) }} + data-flux-icon + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 76 44" + fill="none" + stroke="currentColor" + stroke-width="{{ $strokeWidth }}" + stroke-linecap="round" + stroke-linejoin="round" + aria-hidden="true" + data-slot="icon" +> + + + + diff --git a/resources/views/flux/icon/mashup-1Tx1B.blade.php b/resources/views/flux/icon/mashup-1Tx1B.blade.php new file mode 100644 index 0000000..c392742 --- /dev/null +++ b/resources/views/flux/icon/mashup-1Tx1B.blade.php @@ -0,0 +1,39 @@ +{{-- Credit: Lucide (https://lucide.dev) --}} + +@props([ + 'variant' => 'outline', +]) + +@php +$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, + default => 2, +}; +@endphp + +class($classes) }} + data-flux-icon + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 76 44" + fill="none" + stroke="currentColor" + stroke-width="{{ $strokeWidth }}" + stroke-linecap="round" + stroke-linejoin="round" + aria-hidden="true" + data-slot="icon" +> + + + diff --git a/resources/views/flux/icon/mashup-1Tx2B.blade.php b/resources/views/flux/icon/mashup-1Tx2B.blade.php new file mode 100644 index 0000000..e66990f --- /dev/null +++ b/resources/views/flux/icon/mashup-1Tx2B.blade.php @@ -0,0 +1,40 @@ +{{-- Credit: Lucide (https://lucide.dev) --}} + +@props([ + 'variant' => 'outline', +]) + +@php +$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, + default => 2, +}; +@endphp + +class($classes) }} + data-flux-icon + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 76 44" + fill="none" + stroke="currentColor" + stroke-width="{{ $strokeWidth }}" + stroke-linecap="round" + stroke-linejoin="round" + aria-hidden="true" + data-slot="icon" +> + + + + diff --git a/resources/views/flux/icon/mashup-1x1.blade.php b/resources/views/flux/icon/mashup-1x1.blade.php new file mode 100644 index 0000000..398b3cf --- /dev/null +++ b/resources/views/flux/icon/mashup-1x1.blade.php @@ -0,0 +1,38 @@ +{{-- Credit: Lucide (https://lucide.dev) --}} + +@props([ + 'variant' => 'outline', +]) + +@php +$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, + default => 2, +}; +@endphp + +class($classes) }} + data-flux-icon + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 76 44" + fill="none" + stroke="currentColor" + stroke-width="{{ $strokeWidth }}" + stroke-linecap="round" + stroke-linejoin="round" + aria-hidden="true" + data-slot="icon" +> + + diff --git a/resources/views/flux/icon/mashup-2Lx1R.blade.php b/resources/views/flux/icon/mashup-2Lx1R.blade.php new file mode 100644 index 0000000..9f3a630 --- /dev/null +++ b/resources/views/flux/icon/mashup-2Lx1R.blade.php @@ -0,0 +1,40 @@ +{{-- Credit: Lucide (https://lucide.dev) --}} + +@props([ + 'variant' => 'outline', +]) + +@php +$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, + default => 2, +}; +@endphp + +class($classes) }} + data-flux-icon + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 76 44" + fill="none" + stroke="currentColor" + stroke-width="{{ $strokeWidth }}" + stroke-linecap="round" + stroke-linejoin="round" + aria-hidden="true" + data-slot="icon" +> + + + + diff --git a/resources/views/flux/icon/mashup-2Tx1B.blade.php b/resources/views/flux/icon/mashup-2Tx1B.blade.php new file mode 100644 index 0000000..2b4d29d --- /dev/null +++ b/resources/views/flux/icon/mashup-2Tx1B.blade.php @@ -0,0 +1,40 @@ +{{-- Credit: Lucide (https://lucide.dev) --}} + +@props([ + 'variant' => 'outline', +]) + +@php +$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, + default => 2, +}; +@endphp + +class($classes) }} + data-flux-icon + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 76 44" + fill="none" + stroke="currentColor" + stroke-width="{{ $strokeWidth }}" + stroke-linecap="round" + stroke-linejoin="round" + aria-hidden="true" + data-slot="icon" +> + + + + diff --git a/resources/views/flux/icon/mashup-2x2.blade.php b/resources/views/flux/icon/mashup-2x2.blade.php new file mode 100644 index 0000000..71077ca --- /dev/null +++ b/resources/views/flux/icon/mashup-2x2.blade.php @@ -0,0 +1,41 @@ +{{-- Credit: Lucide (https://lucide.dev) --}} + +@props([ + 'variant' => 'outline', +]) + +@php +$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, + default => 2, +}; +@endphp + +class($classes) }} + data-flux-icon + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 76 44" + fill="none" + stroke="currentColor" + stroke-width="{{ $strokeWidth }}" + stroke-linecap="round" + stroke-linejoin="round" + aria-hidden="true" + data-slot="icon" +> + + + + + diff --git a/resources/views/flux/icon/sunrise.blade.php b/resources/views/flux/icon/sunrise.blade.php new file mode 100644 index 0000000..e078da6 --- /dev/null +++ b/resources/views/flux/icon/sunrise.blade.php @@ -0,0 +1,48 @@ +{{-- 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/flux/icon/thermometer.blade.php b/resources/views/flux/icon/thermometer.blade.php new file mode 100644 index 0000000..decfe4f --- /dev/null +++ b/resources/views/flux/icon/thermometer.blade.php @@ -0,0 +1,41 @@ +{{-- 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/flux/icon/trmnl.blade.php b/resources/views/flux/icon/trmnl.blade.php new file mode 100644 index 0000000..2755f6c --- /dev/null +++ b/resources/views/flux/icon/trmnl.blade.php @@ -0,0 +1,56 @@ +{{-- Credit: Lucide (https://lucide.dev) --}} + +@props([ + 'variant' => 'outline', +]) + +@php + if ($variant === 'solid') { + throw new \Exception('The "solid" variant is not supported.'); + } + + $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 150 150" + fill="none" + stroke="currentColor" + stroke-width="{{ $strokeWidth }}" + stroke-linecap="round" + stroke-linejoin="round" + aria-hidden="true" + data-slot="icon" +> + + + + + + + + + diff --git a/resources/views/flux/navlist/group.blade.php b/resources/views/flux/navlist/group.blade.php index cecbabe..1c94dfb 100644 --- a/resources/views/flux/navlist/group.blade.php +++ b/resources/views/flux/navlist/group.blade.php @@ -4,7 +4,7 @@ 'heading' => null, ]) - + class('group/disclosure') }} @@ -15,7 +15,7 @@ type="button" class="group/disclosure-button mb-[2px] flex h-10 w-full items-center rounded-lg text-zinc-500 hover:bg-zinc-800/5 hover:text-zinc-800 lg:h-8 dark:text-white/80 dark:hover:bg-white/[7%] dark:hover:text-white" > -
+
@@ -23,14 +23,14 @@ {{ $heading }} -