From c4a17b65c8a4e58cfaf557db47d8bc5020b1fc4f Mon Sep 17 00:00:00 2001 From: Andreas Reinhold / reini Date: Sun, 13 Sep 2026 10:49:38 +0200 Subject: [PATCH] Move SealShare onto Livewire Material Replaces maryUI and daisyUI with nonameweb/livewire-material: the Vibrant indigo scheme, a system/light/dark theme under sealshare-theme, one top app bar with the account menu, the upload drop zone and link-ready moments, M3 fields, dialogs instead of wire:confirm, snackbars instead of flashed messages, a sortable admin table, and the starter-kit cleanup. Docker builds assets after Composer; CI drops the Flux step. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01V9NnLxnPp8vaaurb3Z1MFy --- .../skills/testing-best-practices/SKILL.md | 2 +- .../rules/endpoint-tests.md | 26 + .github/workflows/docker.yml | 3 - .github/workflows/lint.yml | 3 - .github/workflows/tests.yml | 6 +- Dockerfile | 34 +- app/Livewire/Admin/AdminDashboard.php | 34 +- app/Livewire/Admin/AdminSettings.php | 18 +- app/Providers/FortifyServiceProvider.php | 1 - composer.json | 11 +- composer.lock | 2135 +++++++++++++---- config/fortify.php | 2 +- config/livewire-material.php | 152 ++ docker/dev-entrypoint.sh | 5 + docker/dev.Dockerfile | 3 + docs/plans/livewire-material.md | 4 +- package-lock.json | 67 +- package.json | 5 +- resources/css/app.css | 22 +- resources/css/material-scheme.css | 150 ++ resources/css/material-scheme.json | 140 ++ resources/js/app.js | 4 +- .../views/components/action-message.blade.php | 14 - resources/views/components/app-logo.blade.php | 4 - .../views/components/auth-header.blade.php | 4 +- .../components/auth-session-status.blade.php | 4 +- .../components/desktop-user-menu.blade.php | 1 - .../components/placeholder-pattern.blade.php | 12 - resources/views/dashboard.blade.php | 18 - resources/views/layouts/app.blade.php | 18 +- resources/views/layouts/app/header.blade.php | 4 - resources/views/layouts/app/sidebar.blade.php | 42 - resources/views/layouts/auth.blade.php | 22 +- resources/views/layouts/auth/card.blade.php | 4 - resources/views/layouts/auth/simple.blade.php | 27 - resources/views/layouts/auth/split.blade.php | 4 - .../livewire/admin/admin-dashboard.blade.php | 125 +- .../livewire/admin/admin-settings.blade.php | 181 +- .../views/livewire/file-uploader.blade.php | 160 +- .../views/livewire/setup-wizard.blade.php | 20 +- .../views/livewire/share-created.blade.php | 97 +- .../views/livewire/share-download.blade.php | 64 +- .../livewire/system-password-prompt.blade.php | 6 +- .../pages/auth/confirm-password.blade.php | 40 +- .../pages/auth/forgot-password.blade.php | 45 +- resources/views/pages/auth/login.blade.php | 81 +- resources/views/pages/auth/register.blade.php | 63 - .../views/pages/auth/reset-password.blade.php | 69 +- .../pages/auth/two-factor-challenge.blade.php | 147 +- .../views/pages/auth/verify-email.blade.php | 39 +- .../views/pages/settings/layout.blade.php | 34 +- .../two-factor/⚡recovery-codes.blade.php | 105 +- .../pages/settings/⚡appearance.blade.php | 2 +- .../settings/⚡delete-user-form.blade.php | 37 +- .../views/pages/settings/⚡password.blade.php | 37 +- .../views/pages/settings/⚡profile.blade.php | 46 +- .../pages/settings/⚡two-factor.blade.php | 141 +- resources/views/partials/app-bar.blade.php | 47 + resources/views/partials/head.blade.php | 5 +- .../views/partials/settings-heading.blade.php | 7 +- resources/views/welcome.blade.php | 17 - routes/web.php | 4 - tests/Feature/Admin/AdminDashboardTest.php | 20 +- tests/Feature/Admin/AdminSettingsTest.php | 29 +- tests/Feature/Auth/AuthenticationTest.php | 2 +- tests/Feature/Auth/EmailVerificationTest.php | 4 +- tests/Feature/DashboardTest.php | 18 +- tests/Feature/DesignLanguageTest.php | 19 + tests/Feature/Settings/PasswordUpdateTest.php | 3 +- tests/Feature/Settings/ProfileUpdateTest.php | 4 +- 70 files changed, 3043 insertions(+), 1680 deletions(-) create mode 100644 config/livewire-material.php create mode 100644 resources/css/material-scheme.css create mode 100644 resources/css/material-scheme.json delete mode 100644 resources/views/components/action-message.blade.php delete mode 100644 resources/views/components/app-logo.blade.php delete mode 100644 resources/views/components/desktop-user-menu.blade.php delete mode 100644 resources/views/components/placeholder-pattern.blade.php delete mode 100644 resources/views/dashboard.blade.php delete mode 100644 resources/views/layouts/app/header.blade.php delete mode 100644 resources/views/layouts/app/sidebar.blade.php delete mode 100644 resources/views/layouts/auth/card.blade.php delete mode 100644 resources/views/layouts/auth/simple.blade.php delete mode 100644 resources/views/layouts/auth/split.blade.php delete mode 100644 resources/views/pages/auth/register.blade.php create mode 100644 resources/views/partials/app-bar.blade.php delete mode 100644 resources/views/welcome.blade.php create mode 100644 tests/Feature/DesignLanguageTest.php diff --git a/.claude/skills/testing-best-practices/SKILL.md b/.claude/skills/testing-best-practices/SKILL.md index 35d02a1..9b0aa7f 100644 --- a/.claude/skills/testing-best-practices/SKILL.md +++ b/.claude/skills/testing-best-practices/SKILL.md @@ -29,7 +29,7 @@ Read this section before you write a test. - Leave framework behavior to framework tests. Testing project configuration is not testing the framework. A constrained relationship, cast, scope, or validation rule belongs to this project. - Keep every test that can detect a distinct defect. When two tests detect the same defect, trim the higher-layer test to one case and report the duplication. Do not delete an existing test. - Write a feature test first. Write a unit test only for logic that does not use the framework. -- Write a feature test for every behavior reachable through a request. Real-browser tests require `pestphp/pest-plugin-browser` and a browser download, neither of which this project installs. Mention the package only if the user asks for a real-browser test. +- Write a browser test only for behavior in JavaScript that a feature test cannot reach. Put a browser test in `tests/Browser`, and call `assertNoJavaScriptErrors()` in it. - Judge an architecture test by the convention it protects, not by the rules above. An `arch()` test declares a rule for an entire directory, such as the parent class of every model, the classes that may use an enum, or the methods every factory declares. It intentionally checks declarations and fails when a new file breaks the convention. - Use the test tools that the project installs. Add a new test dependency, plugin, or browser only after the user asks for it. diff --git a/.claude/skills/testing-best-practices/rules/endpoint-tests.md b/.claude/skills/testing-best-practices/rules/endpoint-tests.md index 32e32e6..e3fc18b 100644 --- a/.claude/skills/testing-best-practices/rules/endpoint-tests.md +++ b/.claude/skills/testing-best-practices/rules/endpoint-tests.md @@ -31,6 +31,32 @@ An HTTP test shows that the endpoint performs authorization. It cannot identify - Write one HTTP test for one refused role, which shows that the endpoint calls the authorization. - Use the helper of the project that asserts the ability and the arguments of the gate, if such a helper exists. +## Browser Tests + +Write a browser test only for JavaScript behavior that an HTTP test cannot reach, such as modal interaction, drag-and-drop, live search, or client-side validation. Browser tests are slower than HTTP tests and can fail for reasons unrelated to the code under test. + +- Assert the state that the user can see, and assert the state in the database that the interaction saves. +- Wait until the test reaches the required state. Do not wait for a fixed number of seconds, which can fail on a slower machine. +- Call `assertNoJavaScriptErrors()` in each browser test. An error in the console is a defect. + +### Where a Browser Test Lives and How to Run It + +The plugin runs browser tests as normal Pest tests, so they need no separate suite. Put them in `tests/Browser` to separate them from faster tests and run the directory with one command. + +- Run a browser test with `vendor/bin/pest tests/Browser`, and add `--parallel` for the complete suite. +- Run `vendor/bin/pest --debug` to open the window of the browser and to pause at a failure. Use `--headed` to watch a run that passes. +- Add `--browser firefox` or `--browser safari` to run the test in a different browser. The default browser is Chrome. +- The run needs Playwright and a browser on the machine. Follow the plugin documentation for local and CI installation commands. +- Fetch `https://pestphp.com/docs/browser-testing` for the interactions, the assertions, and the devices that the plugin gives. + +### Browser Test Pitfalls + +- The plugin waits five seconds for an element. Raise the value with `pest()->browser()->timeout(10000)` in `Pest.php` for a page that is slower, and do not add a wait for a number of seconds to the test. +- Apply `RefreshDatabase` to the browser tests in `Pest.php`. A browser test hits the application through a real request, and the records that it leaves break the next test. +- Add `tests/Browser/Screenshots` to `.gitignore`. A failure writes a screenshot, and the file is not part of the repository. +- Give `withKeyDown()` a key code, such as `KeyA`. A letter such as `'a'` gives the lowercase character, whatever modifier the test holds. +- Interact inside the callback of `withinFrame()`. An interaction outside the callback does not reach the frame. + ## Testing Validation - Write one test for each validation rule when each failure represents a separate contract. diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 8ed1085..c2e7994 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -42,9 +42,6 @@ jobs: - name: Install Node Dependencies run: npm ci - - name: Add Flux Credentials Loaded From ENV - run: composer config http-basic.composer.fluxui.dev "${{ secrets.FLUX_USERNAME }}" "${{ secrets.FLUX_LICENSE_KEY }}" - - name: Install Dependencies run: composer install --no-interaction --prefer-dist --optimize-autoloader diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 7175e23..4c476e0 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -29,9 +29,6 @@ jobs: with: php-version: '8.5' - - name: Add Flux Credentials Loaded From ENV - run: composer config http-basic.composer.fluxui.dev "${{ secrets.FLUX_USERNAME }}" "${{ secrets.FLUX_LICENSE_KEY }}" - - name: Install Dependencies run: | composer install -q --no-ansi --no-interaction --no-scripts --no-progress --prefer-dist diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 93451e8..35b5174 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -42,9 +42,6 @@ jobs: - name: Install Node Dependencies run: npm i - - name: Add Flux Credentials Loaded From ENV - run: composer config http-basic.composer.fluxui.dev "${{ secrets.FLUX_USERNAME }}" "${{ secrets.FLUX_LICENSE_KEY }}" - - name: Install Dependencies run: composer install --no-interaction --prefer-dist --optimize-autoloader @@ -57,5 +54,8 @@ jobs: - name: Build Assets run: npm run build + - name: Install Playwright Browsers + run: npx playwright install --with-deps chromium + - name: Run Tests run: ./vendor/bin/pest diff --git a/Dockerfile b/Dockerfile index 64b6051..a173593 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,20 +1,5 @@ # ============================================ -# Stage 1: Build frontend assets -# ============================================ -FROM node:24-alpine AS assets - -WORKDIR /app - -COPY package.json package-lock.json* ./ -RUN npm ci --prefer-offline - -COPY vite.config.js ./ -COPY resources/ ./resources/ - -RUN npm run build - -# ============================================ -# Stage 2: Install PHP dependencies +# Stage 1: Install PHP dependencies # ============================================ FROM composer:2 AS vendor @@ -33,6 +18,23 @@ COPY . . RUN composer dump-autoload --optimize --no-dev +# ============================================ +# Stage 2: Build frontend assets +# ============================================ +# After Composer: the stylesheet and script import Livewire Material from vendor/. +FROM node:24-alpine AS assets + +WORKDIR /app + +COPY package.json package-lock.json* ./ +RUN npm ci --prefer-offline + +COPY vite.config.js ./ +COPY resources/ ./resources/ +COPY --from=vendor /app/vendor/nonameweb ./vendor/nonameweb + +RUN npm run build + # ============================================ # Stage 3: Production image (FrankenPHP/Octane) # ============================================ diff --git a/app/Livewire/Admin/AdminDashboard.php b/app/Livewire/Admin/AdminDashboard.php index d07979f..57f08b3 100644 --- a/app/Livewire/Admin/AdminDashboard.php +++ b/app/Livewire/Admin/AdminDashboard.php @@ -14,37 +14,38 @@ class AdminDashboard extends Component { use WithPagination; - /** @var array */ + /** + * The columns the table can be sorted by. + * + * @var list + */ + public const SORTABLE = ['token', 'files_count', 'total_size', 'download_count', 'expires_at', 'created_at']; + + /** @var array{column: string, direction: string} */ public array $sortBy = ['column' => 'created_at', 'direction' => 'desc']; + /** The share the delete dialog is asking about, while it is open. */ + public ?int $deletingShareId = null; + public function deleteShare(int $shareId, ShareService $shareService): void { $share = Share::query()->findOrFail($shareId); $shareService->deleteShare($share); - } - /** - * @return array> - */ - public function headers(): array - { - return [ - ['key' => 'token', 'label' => __('Token')], - ['key' => 'files_count', 'label' => __('Files')], - ['key' => 'total_size', 'label' => __('Size')], - ['key' => 'download_count', 'label' => __('Downloads')], - ['key' => 'expires_at', 'label' => __('Expires')], - ['key' => 'created_at', 'label' => __('Created')], - ]; + $this->deletingShareId = null; } public function render(): mixed { $shareService = app(ShareService::class); + // The sort comes from the browser: only a known column and direction reach the query. + $column = in_array($this->sortBy['column'] ?? null, self::SORTABLE, true) ? $this->sortBy['column'] : 'created_at'; + $direction = ($this->sortBy['direction'] ?? null) === 'asc' ? 'asc' : 'desc'; + $shares = Share::query() ->withCount('files') - ->orderBy($this->sortBy['column'], $this->sortBy['direction']) + ->orderBy($column, $direction) ->paginate(15); return view('livewire.admin.admin-dashboard', [ @@ -56,7 +57,6 @@ class AdminDashboard extends Component 'totalFiles' => ShareFile::query()->count(), 'usedSpace' => $shareService->getTotalUsedSpace(), 'maxQuota' => $shareService->getMaxStorageQuota(), - 'headers' => $this->headers(), ]); } } diff --git a/app/Livewire/Admin/AdminSettings.php b/app/Livewire/Admin/AdminSettings.php index c5728da..ef6ce9a 100644 --- a/app/Livewire/Admin/AdminSettings.php +++ b/app/Livewire/Admin/AdminSettings.php @@ -8,10 +8,12 @@ use Illuminate\Support\Facades\Storage; use Livewire\Attributes\Layout; use Livewire\Component; use Livewire\WithFileUploads; +use NoNameWeb\LivewireMaterial\Concerns\Toasts; #[Layout('layouts.app')] class AdminSettings extends Component { + use Toasts; use WithFileUploads; public string $systemPassword = ''; @@ -34,6 +36,12 @@ class AdminSettings extends Component public $siteLogo; + /** Whether the "Remove the logo?" dialog is open. */ + public bool $confirmingLogoRemoval = false; + + /** Whether the "Remove the system password?" dialog is open. */ + public bool $confirmingPasswordRemoval = false; + public function mount(): void { $this->defaultExpiration = Setting::get('default_expiration', '') ?? ''; @@ -113,7 +121,7 @@ class AdminSettings extends Component $this->systemPassword = ''; - session()->flash('message', __('Settings saved successfully.')); + $this->success(__('Settings saved successfully.')); } public function removeLogo(): void @@ -125,14 +133,18 @@ class AdminSettings extends Component Setting::set('site_logo', null); } - session()->flash('message', __('Logo removed.')); + $this->confirmingLogoRemoval = false; + + $this->success(__('Logo removed.')); } public function clearSystemPassword(): void { Setting::set('system_password', null); - session()->flash('message', __('System password cleared.')); + $this->confirmingPasswordRemoval = false; + + $this->success(__('System password cleared.')); } public function render(): mixed diff --git a/app/Providers/FortifyServiceProvider.php b/app/Providers/FortifyServiceProvider.php index 6ef37bd..74c4913 100644 --- a/app/Providers/FortifyServiceProvider.php +++ b/app/Providers/FortifyServiceProvider.php @@ -49,7 +49,6 @@ class FortifyServiceProvider extends ServiceProvider Fortify::verifyEmailView(fn () => view('pages::auth.verify-email')); Fortify::twoFactorChallengeView(fn () => view('pages::auth.two-factor-challenge')); Fortify::confirmPasswordView(fn () => view('pages::auth.confirm-password')); - Fortify::registerView(fn () => view('pages::auth.register')); Fortify::resetPasswordView(fn () => view('pages::auth.reset-password')); Fortify::requestPasswordResetLinkView(fn () => view('pages::auth.forgot-password')); } diff --git a/composer.json b/composer.json index a3769b8..d37136b 100644 --- a/composer.json +++ b/composer.json @@ -15,7 +15,7 @@ "laravel/octane": "^2.13", "laravel/tinker": "^3.0", "livewire/livewire": "^4.0", - "robsontenorio/mary": "^2.7" + "nonameweb/livewire-material": "^1.0" }, "require-dev": { "fakerphp/faker": "^1.23", @@ -26,6 +26,7 @@ "mockery/mockery": "^1.6", "nunomaduro/collision": "^8.6", "pestphp/pest": "^5.1", + "pestphp/pest-plugin-browser": "^5.0", "pestphp/pest-plugin-laravel": "^5.0" }, "autoload": { @@ -99,5 +100,11 @@ } }, "minimum-stability": "stable", - "prefer-stable": true + "prefer-stable": true, + "repositories": { + "livewire-material": { + "type": "vcs", + "url": "https://gitea.nonameweb.ch/noNameWEB/livewire-material.git" + } + } } diff --git a/composer.lock b/composer.lock index 0b2c267..56f8573 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "d6013de4a9e435be0e66b093000408af", + "content-hash": "c3e08a235fe90acdef7903ef3fd01376", "packages": [ { "name": "bacon/bacon-qr-code", @@ -61,156 +61,6 @@ }, "time": "2026-04-05T21:06:35+00:00" }, - { - "name": "blade-ui-kit/blade-heroicons", - "version": "2.7.0", - "source": { - "type": "git", - "url": "https://github.com/driesvints/blade-heroicons.git", - "reference": "66fa8ba09dba12e0cdb410b8cb94f3b890eca440" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/driesvints/blade-heroicons/zipball/66fa8ba09dba12e0cdb410b8cb94f3b890eca440", - "reference": "66fa8ba09dba12e0cdb410b8cb94f3b890eca440", - "shasum": "" - }, - "require": { - "blade-ui-kit/blade-icons": "^1.6", - "illuminate/support": "^9.0|^10.0|^11.0|^12.0|^13.0", - "php": "^8.0" - }, - "require-dev": { - "orchestra/testbench": "^7.0|^8.0|^9.0|^10.0|^11.0", - "phpunit/phpunit": "^9.0|^10.5|^11.0|^12.0" - }, - "type": "library", - "extra": { - "laravel": { - "providers": [ - "BladeUI\\Heroicons\\BladeHeroiconsServiceProvider" - ] - } - }, - "autoload": { - "psr-4": { - "BladeUI\\Heroicons\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Dries Vints", - "homepage": "https://driesvints.com" - } - ], - "description": "A package to easily make use of Heroicons in your Laravel Blade views.", - "homepage": "https://github.com/driesvints/blade-heroicons", - "keywords": [ - "Heroicons", - "blade", - "laravel" - ], - "support": { - "issues": "https://github.com/driesvints/blade-heroicons/issues", - "source": "https://github.com/driesvints/blade-heroicons/tree/2.7.0" - }, - "funding": [ - { - "url": "https://github.com/sponsors/driesvints", - "type": "github" - }, - { - "url": "https://www.paypal.com/paypalme/driesvints", - "type": "paypal" - } - ], - "time": "2026-03-16T13:00:23+00:00" - }, - { - "name": "blade-ui-kit/blade-icons", - "version": "1.10.1", - "source": { - "type": "git", - "url": "https://github.com/driesvints/blade-icons.git", - "reference": "6e072d021ea6249986c330b93293c33d0c4f0e34" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/driesvints/blade-icons/zipball/6e072d021ea6249986c330b93293c33d0c4f0e34", - "reference": "6e072d021ea6249986c330b93293c33d0c4f0e34", - "shasum": "" - }, - "require": { - "illuminate/contracts": "^8.0|^9.0|^10.0|^11.0|^12.0|^13.0", - "illuminate/filesystem": "^8.0|^9.0|^10.0|^11.0|^12.0|^13.0", - "illuminate/support": "^8.0|^9.0|^10.0|^11.0|^12.0|^13.0", - "illuminate/view": "^8.0|^9.0|^10.0|^11.0|^12.0|^13.0", - "php": "^7.4|^8.0", - "symfony/console": "^5.3|^6.0|^7.0|^8.0", - "symfony/finder": "^5.3|^6.0|^7.0|^8.0" - }, - "require-dev": { - "mockery/mockery": "^1.5.1", - "orchestra/testbench": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0", - "phpunit/phpunit": "^9.0|^10.5|^11.0" - }, - "bin": [ - "bin/blade-icons-generate" - ], - "type": "library", - "extra": { - "laravel": { - "providers": [ - "BladeUI\\Icons\\BladeIconsServiceProvider" - ] - } - }, - "autoload": { - "files": [ - "src/helpers.php" - ], - "psr-4": { - "BladeUI\\Icons\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Dries Vints", - "homepage": "https://driesvints.com" - } - ], - "description": "A package to easily make use of icons in your Laravel Blade views.", - "homepage": "https://github.com/driesvints/blade-icons", - "keywords": [ - "blade", - "icons", - "laravel", - "svg" - ], - "support": { - "issues": "https://github.com/driesvints/blade-icons/issues", - "source": "https://github.com/driesvints/blade-icons" - }, - "funding": [ - { - "url": "https://github.com/sponsors/driesvints", - "type": "github" - }, - { - "url": "https://www.paypal.com/paypalme/driesvints", - "type": "paypal" - } - ], - "time": "2026-06-30T09:44:12+00:00" - }, { "name": "brick/math", "version": "0.18.0", @@ -1362,243 +1212,6 @@ ], "time": "2026-08-24T17:13:02+00:00" }, - { - "name": "jfcherng/php-color-output", - "version": "3.0.0", - "source": { - "type": "git", - "url": "https://github.com/jfcherng/php-color-output.git", - "reference": "6c7bf16686cc6a291647fcb87491640a2d5edd20" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/jfcherng/php-color-output/zipball/6c7bf16686cc6a291647fcb87491640a2d5edd20", - "reference": "6c7bf16686cc6a291647fcb87491640a2d5edd20", - "shasum": "" - }, - "require": { - "php": ">=7.1.3" - }, - "require-dev": { - "friendsofphp/php-cs-fixer": "^2.19", - "liip/rmt": "^1.6", - "phan/phan": "^2 || ^3 || ^4", - "phpunit/phpunit": ">=7 <10", - "squizlabs/php_codesniffer": "^3.5" - }, - "type": "library", - "autoload": { - "psr-4": { - "Jfcherng\\Utility\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Jack Cherng", - "email": "jfcherng@gmail.com" - } - ], - "description": "Make your PHP command-line application colorful.", - "keywords": [ - "ansi-colors", - "color", - "command-line", - "str-color" - ], - "support": { - "issues": "https://github.com/jfcherng/php-color-output/issues", - "source": "https://github.com/jfcherng/php-color-output/tree/3.0.0" - }, - "funding": [ - { - "url": "https://www.paypal.me/jfcherng/5usd", - "type": "custom" - } - ], - "time": "2021-05-27T02:45:54+00:00" - }, - { - "name": "jfcherng/php-diff", - "version": "6.16.3", - "source": { - "type": "git", - "url": "https://github.com/jfcherng/php-diff.git", - "reference": "6d7332b6080cdd50011a364b58f24c8d0cdeb5da" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/jfcherng/php-diff/zipball/6d7332b6080cdd50011a364b58f24c8d0cdeb5da", - "reference": "6d7332b6080cdd50011a364b58f24c8d0cdeb5da", - "shasum": "" - }, - "require": { - "jfcherng/php-color-output": "^3", - "jfcherng/php-mb-string": "^1.4.6 || ^2", - "jfcherng/php-sequence-matcher": "^3.2.10 || ^4", - "php": ">=7.4" - }, - "require-dev": { - "friendsofphp/php-cs-fixer": "^3.51", - "liip/rmt": "^1.6", - "phan/phan": "^5", - "phpunit/phpunit": "^9", - "squizlabs/php_codesniffer": "^3.6" - }, - "type": "library", - "autoload": { - "psr-4": { - "Jfcherng\\Diff\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Jack Cherng", - "email": "jfcherng@gmail.com" - }, - { - "name": "Chris Boulton", - "email": "chris.boulton@interspire.com" - } - ], - "description": "A comprehensive library for generating differences between two strings in multiple formats (unified, side by side HTML etc).", - "keywords": [ - "diff", - "udiff", - "unidiff", - "unified diff" - ], - "support": { - "issues": "https://github.com/jfcherng/php-diff/issues", - "source": "https://github.com/jfcherng/php-diff/tree/6.16.3" - }, - "funding": [ - { - "url": "https://www.paypal.me/jfcherng/5usd", - "type": "custom" - } - ], - "time": "2026-06-22T13:08:56+00:00" - }, - { - "name": "jfcherng/php-mb-string", - "version": "2.0.1", - "source": { - "type": "git", - "url": "https://github.com/jfcherng/php-mb-string.git", - "reference": "8407bfefde47849c9e7c9594e6de2ac85a0f845d" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/jfcherng/php-mb-string/zipball/8407bfefde47849c9e7c9594e6de2ac85a0f845d", - "reference": "8407bfefde47849c9e7c9594e6de2ac85a0f845d", - "shasum": "" - }, - "require": { - "ext-iconv": "*", - "php": ">=8.1" - }, - "require-dev": { - "friendsofphp/php-cs-fixer": "^3", - "phan/phan": "^5", - "phpunit/phpunit": "^9 || ^10" - }, - "suggest": { - "ext-iconv": "Either \"ext-iconv\" or \"ext-mbstring\" is requried.", - "ext-mbstring": "Either \"ext-iconv\" or \"ext-mbstring\" is requried." - }, - "type": "library", - "autoload": { - "psr-4": { - "Jfcherng\\Utility\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Jack Cherng", - "email": "jfcherng@gmail.com" - } - ], - "description": "A high performance multibytes sting implementation for frequently reading/writing operations.", - "support": { - "issues": "https://github.com/jfcherng/php-mb-string/issues", - "source": "https://github.com/jfcherng/php-mb-string/tree/2.0.1" - }, - "funding": [ - { - "url": "https://www.paypal.me/jfcherng/5usd", - "type": "custom" - } - ], - "time": "2023-04-17T14:23:16+00:00" - }, - { - "name": "jfcherng/php-sequence-matcher", - "version": "4.0.3", - "source": { - "type": "git", - "url": "https://github.com/jfcherng/php-sequence-matcher.git", - "reference": "d2038ac29627340a7458609072a8ba355e80ec5b" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/jfcherng/php-sequence-matcher/zipball/d2038ac29627340a7458609072a8ba355e80ec5b", - "reference": "d2038ac29627340a7458609072a8ba355e80ec5b", - "shasum": "" - }, - "require": { - "php": ">=8.1" - }, - "require-dev": { - "friendsofphp/php-cs-fixer": "^3", - "phan/phan": "^5", - "phpunit/phpunit": "^9 || ^10", - "squizlabs/php_codesniffer": "^3.7" - }, - "type": "library", - "autoload": { - "psr-4": { - "Jfcherng\\Diff\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Jack Cherng", - "email": "jfcherng@gmail.com" - }, - { - "name": "Chris Boulton", - "email": "chris.boulton@interspire.com" - } - ], - "description": "A longest sequence matcher. The logic is primarily based on the Python difflib package.", - "support": { - "issues": "https://github.com/jfcherng/php-sequence-matcher/issues", - "source": "https://github.com/jfcherng/php-sequence-matcher/tree/4.0.3" - }, - "funding": [ - { - "url": "https://www.paypal.me/jfcherng/5usd", - "type": "custom" - } - ], - "time": "2023-05-21T07:57:08+00:00" - }, { "name": "laminas/laminas-diactoros", "version": "3.8.0", @@ -3384,6 +2997,94 @@ }, "time": "2026-07-04T14:30:18+00:00" }, + { + "name": "nonameweb/livewire-material", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://gitea.nonameweb.ch/noNameWEB/livewire-material.git", + "reference": "51bf034b74813575a53aa81ee8ead64a315f35ef" + }, + "require": { + "laravel/framework": "^13.0", + "livewire/livewire": "^4.0", + "php": "^8.4" + }, + "require-dev": { + "blade-ui-kit/blade-icons": "^1.10", + "laravel/pint": "^1.32", + "orchestra/testbench": "^11.2", + "pestphp/pest": "^5.1", + "pestphp/pest-plugin-browser": "^5.0", + "pestphp/pest-plugin-laravel": "^5.0" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "NoNameWeb\\LivewireMaterial\\LivewireMaterialServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "NoNameWeb\\LivewireMaterial\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "NoNameWeb\\LivewireMaterial\\Tests\\": "tests/", + "Workbench\\App\\": "workbench/app/" + } + }, + "scripts": { + "post-autoload-dump": [ + "@clear", + "@prepare" + ], + "clear": [ + "@php vendor/bin/testbench package:purge-skeleton --ansi" + ], + "prepare": [ + "@php vendor/bin/testbench package:discover --ansi" + ], + "build": [ + "@php vendor/bin/testbench workbench:build --ansi" + ], + "serve": [ + "Composer\\Config::disableProcessTimeout", + "@build", + "@php vendor/bin/testbench serve --ansi" + ], + "lint": [ + "pint" + ], + "test": [ + "pest" + ] + }, + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Andreas Reinhold" + } + ], + "description": "Material 3 Expressive components for Laravel and Livewire.", + "homepage": "https://gitea.nonameweb.ch/noNameWEB/livewire-material", + "keywords": [ + "blade", + "components", + "laravel", + "livewire", + "material", + "material-3", + "material-design", + "tailwindcss" + ], + "time": "2026-09-13T07:56:00+00:00" + }, { "name": "nunomaduro/termwind", "version": "v2.4.0", @@ -4547,92 +4248,6 @@ }, "time": "2026-06-18T03:57:49+00:00" }, - { - "name": "robsontenorio/mary", - "version": "2.9.10", - "source": { - "type": "git", - "url": "https://github.com/robsontenorio/mary.git", - "reference": "072f43636fa71f7d2a1fb40ec76ad503ed3505cb" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/robsontenorio/mary/zipball/072f43636fa71f7d2a1fb40ec76ad503ed3505cb", - "reference": "072f43636fa71f7d2a1fb40ec76ad503ed3505cb", - "shasum": "" - }, - "require": { - "blade-ui-kit/blade-heroicons": "^2.6", - "illuminate/support": "^10.0|^11.0|^12.0|^13.0", - "jfcherng/php-diff": "^6.15", - "laravel/prompts": "^0|^1" - }, - "require-dev": { - "orchestra/testbench": "^8|^9|^10|^11.0", - "phpunit/phpunit": "^10|^11|^12.5.12" - }, - "type": "library", - "extra": { - "laravel": { - "aliases": { - "Mary": "Mary\\Facades\\Mary" - }, - "providers": [ - "Mary\\MaryServiceProvider" - ] - } - }, - "autoload": { - "psr-4": { - "Mary\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Robson Tenório", - "email": "rrtenorio@gmail.com", - "homepage": "https://github.com/robsontenorio" - } - ], - "description": "Gorgeous UI components for Livewire powered by daisyUI and Tailwind", - "homepage": "https://mary-ui.com", - "keywords": [ - "DaisyUI", - "alpinejs", - "blade", - "blade ui components", - "components", - "laravel", - "livewire", - "livewire components", - "livewire packages", - "livewire ui", - "livewire ui components", - "livewire-components", - "livewire-packages", - "tailwind", - "tallstack", - "tallstack components", - "tallstack ui", - "tallstackui", - "ui" - ], - "support": { - "issues": "https://github.com/robsontenorio/mary/issues", - "source": "https://github.com/robsontenorio/mary/tree/2.9.10" - }, - "funding": [ - { - "url": "https://github.com/robsontenorio", - "type": "github" - } - ], - "time": "2026-09-03T13:18:49+00:00" - }, { "name": "spomky-labs/cbor-php", "version": "3.3.4", @@ -8247,6 +7862,1236 @@ } ], "packages-dev": [ + { + "name": "amphp/amp", + "version": "v3.1.3", + "source": { + "type": "git", + "url": "https://github.com/amphp/amp.git", + "reference": "73c38b323ff8d790abf0f76c56fcc892bda4111a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/amphp/amp/zipball/73c38b323ff8d790abf0f76c56fcc892bda4111a", + "reference": "73c38b323ff8d790abf0f76c56fcc892bda4111a", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "revolt/event-loop": "^1 || ^0.2" + }, + "require-dev": { + "amphp/php-cs-fixer-config": "^2", + "phpunit/phpunit": "^9", + "psalm/phar": "6.16.1" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions.php", + "src/Future/functions.php", + "src/Internal/functions.php" + ], + "psr-4": { + "Amp\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Bob Weinand", + "email": "bobwei9@hotmail.com" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + }, + { + "name": "Daniel Lowrey", + "email": "rdlowrey@php.net" + } + ], + "description": "A non-blocking concurrency framework for PHP applications.", + "homepage": "https://amphp.org/amp", + "keywords": [ + "async", + "asynchronous", + "awaitable", + "concurrency", + "event", + "event-loop", + "future", + "non-blocking", + "promise" + ], + "support": { + "issues": "https://github.com/amphp/amp/issues", + "source": "https://github.com/amphp/amp/tree/v3.1.3" + }, + "funding": [ + { + "url": "https://github.com/amphp", + "type": "github" + } + ], + "time": "2026-07-19T17:59:20+00:00" + }, + { + "name": "amphp/byte-stream", + "version": "v2.1.2", + "source": { + "type": "git", + "url": "https://github.com/amphp/byte-stream.git", + "reference": "55a6bd071aec26fa2a3e002618c20c35e3df1b46" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/amphp/byte-stream/zipball/55a6bd071aec26fa2a3e002618c20c35e3df1b46", + "reference": "55a6bd071aec26fa2a3e002618c20c35e3df1b46", + "shasum": "" + }, + "require": { + "amphp/amp": "^3", + "amphp/parser": "^1.1", + "amphp/pipeline": "^1", + "amphp/serialization": "^1", + "amphp/sync": "^2", + "php": ">=8.1", + "revolt/event-loop": "^1 || ^0.2.3" + }, + "require-dev": { + "amphp/php-cs-fixer-config": "^2", + "amphp/phpunit-util": "^3", + "phpunit/phpunit": "^9", + "psalm/phar": "5.22.1" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions.php", + "src/Internal/functions.php" + ], + "psr-4": { + "Amp\\ByteStream\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + } + ], + "description": "A stream abstraction to make working with non-blocking I/O simple.", + "homepage": "https://amphp.org/byte-stream", + "keywords": [ + "amp", + "amphp", + "async", + "io", + "non-blocking", + "stream" + ], + "support": { + "issues": "https://github.com/amphp/byte-stream/issues", + "source": "https://github.com/amphp/byte-stream/tree/v2.1.2" + }, + "funding": [ + { + "url": "https://github.com/amphp", + "type": "github" + } + ], + "time": "2025-03-16T17:10:27+00:00" + }, + { + "name": "amphp/cache", + "version": "v2.0.1", + "source": { + "type": "git", + "url": "https://github.com/amphp/cache.git", + "reference": "46912e387e6aa94933b61ea1ead9cf7540b7797c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/amphp/cache/zipball/46912e387e6aa94933b61ea1ead9cf7540b7797c", + "reference": "46912e387e6aa94933b61ea1ead9cf7540b7797c", + "shasum": "" + }, + "require": { + "amphp/amp": "^3", + "amphp/serialization": "^1", + "amphp/sync": "^2", + "php": ">=8.1", + "revolt/event-loop": "^1 || ^0.2" + }, + "require-dev": { + "amphp/php-cs-fixer-config": "^2", + "amphp/phpunit-util": "^3", + "phpunit/phpunit": "^9", + "psalm/phar": "^5.4" + }, + "type": "library", + "autoload": { + "psr-4": { + "Amp\\Cache\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + }, + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Daniel Lowrey", + "email": "rdlowrey@php.net" + } + ], + "description": "A fiber-aware cache API based on Amp and Revolt.", + "homepage": "https://amphp.org/cache", + "support": { + "issues": "https://github.com/amphp/cache/issues", + "source": "https://github.com/amphp/cache/tree/v2.0.1" + }, + "funding": [ + { + "url": "https://github.com/amphp", + "type": "github" + } + ], + "time": "2024-04-19T03:38:06+00:00" + }, + { + "name": "amphp/dns", + "version": "v2.4.1", + "source": { + "type": "git", + "url": "https://github.com/amphp/dns.git", + "reference": "ec5948bfafac808f410406e18bc7b52a2d4629b7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/amphp/dns/zipball/ec5948bfafac808f410406e18bc7b52a2d4629b7", + "reference": "ec5948bfafac808f410406e18bc7b52a2d4629b7", + "shasum": "" + }, + "require": { + "amphp/amp": "^3", + "amphp/byte-stream": "^2", + "amphp/cache": "^2", + "amphp/parser": "^1", + "amphp/process": "^2", + "daverandom/libdns": "^2.0.2", + "ext-filter": "*", + "ext-json": "*", + "php": ">=8.1", + "revolt/event-loop": "^1 || ^0.2" + }, + "require-dev": { + "amphp/php-cs-fixer-config": "^2", + "amphp/phpunit-util": "^3", + "phpunit/phpunit": "^9", + "psalm/phar": "6.16.1" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "Amp\\Dns\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Chris Wright", + "email": "addr@daverandom.com" + }, + { + "name": "Daniel Lowrey", + "email": "rdlowrey@php.net" + }, + { + "name": "Bob Weinand", + "email": "bobwei9@hotmail.com" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + }, + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + } + ], + "description": "Async DNS resolution for Amp.", + "homepage": "https://github.com/amphp/dns", + "keywords": [ + "amp", + "amphp", + "async", + "client", + "dns", + "resolve" + ], + "support": { + "issues": "https://github.com/amphp/dns/issues", + "source": "https://github.com/amphp/dns/tree/v2.4.1" + }, + "funding": [ + { + "url": "https://github.com/amphp", + "type": "github" + } + ], + "time": "2026-07-26T17:53:54+00:00" + }, + { + "name": "amphp/hpack", + "version": "v3.2.2", + "source": { + "type": "git", + "url": "https://github.com/amphp/hpack.git", + "reference": "291da27078e7e149a9bad4d08ff05bf7d81c89f4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/amphp/hpack/zipball/291da27078e7e149a9bad4d08ff05bf7d81c89f4", + "reference": "291da27078e7e149a9bad4d08ff05bf7d81c89f4", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "require-dev": { + "amphp/php-cs-fixer-config": "^2", + "http2jp/hpack-test-case": "^1", + "nikic/php-fuzzer": "^0.0.11", + "phpunit/phpunit": "^7 | ^8 | ^9" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Amp\\Http\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Daniel Lowrey", + "email": "rdlowrey@php.net" + }, + { + "name": "Bob Weinand" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + }, + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + } + ], + "description": "HTTP/2 HPack implementation.", + "homepage": "https://github.com/amphp/hpack", + "keywords": [ + "headers", + "hpack", + "http-2" + ], + "support": { + "issues": "https://github.com/amphp/hpack/issues", + "source": "https://github.com/amphp/hpack/tree/v3.2.2" + }, + "funding": [ + { + "url": "https://github.com/amphp", + "type": "github" + } + ], + "time": "2026-05-03T19:28:59+00:00" + }, + { + "name": "amphp/http", + "version": "v2.1.2", + "source": { + "type": "git", + "url": "https://github.com/amphp/http.git", + "reference": "3680d80bd38b5d6f3c2cef2214ca6dd6cef26588" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/amphp/http/zipball/3680d80bd38b5d6f3c2cef2214ca6dd6cef26588", + "reference": "3680d80bd38b5d6f3c2cef2214ca6dd6cef26588", + "shasum": "" + }, + "require": { + "amphp/hpack": "^3", + "amphp/parser": "^1.1", + "league/uri-components": "^2.4.2 | ^7.1", + "php": ">=8.1", + "psr/http-message": "^1 | ^2" + }, + "require-dev": { + "amphp/php-cs-fixer-config": "^2", + "league/uri": "^6.8 | ^7.1", + "phpunit/phpunit": "^9", + "psalm/phar": "^5.26.1" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions.php", + "src/Internal/constants.php" + ], + "psr-4": { + "Amp\\Http\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + }, + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + } + ], + "description": "Basic HTTP primitives which can be shared by servers and clients.", + "support": { + "issues": "https://github.com/amphp/http/issues", + "source": "https://github.com/amphp/http/tree/v2.1.2" + }, + "funding": [ + { + "url": "https://github.com/amphp", + "type": "github" + } + ], + "time": "2024-11-23T14:57:26+00:00" + }, + { + "name": "amphp/http-client", + "version": "v5.3.6", + "source": { + "type": "git", + "url": "https://github.com/amphp/http-client.git", + "reference": "ca155026acafa74a612d776a97202d53077fee86" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/amphp/http-client/zipball/ca155026acafa74a612d776a97202d53077fee86", + "reference": "ca155026acafa74a612d776a97202d53077fee86", + "shasum": "" + }, + "require": { + "amphp/amp": "^3", + "amphp/byte-stream": "^2", + "amphp/hpack": "^3", + "amphp/http": "^2", + "amphp/pipeline": "^1", + "amphp/socket": "^2", + "amphp/sync": "^2", + "league/uri": "^7", + "league/uri-components": "^7", + "league/uri-interfaces": "^7.1", + "php": ">=8.1", + "psr/http-message": "^1 | ^2", + "revolt/event-loop": "^1" + }, + "conflict": { + "amphp/file": "<3 | >=5" + }, + "require-dev": { + "amphp/file": "^3 | ^4", + "amphp/http-server": "^3", + "amphp/php-cs-fixer-config": "^2", + "amphp/phpunit-util": "^3", + "ext-json": "*", + "kelunik/link-header-rfc5988": "^1", + "phpunit/phpunit": "^9", + "psalm/phar": "6.16.1" + }, + "suggest": { + "amphp/file": "Required for file request bodies and HTTP archive logging", + "ext-json": "Required for logging HTTP archives", + "ext-zlib": "Allows using compression for response bodies." + }, + "type": "library", + "autoload": { + "files": [ + "src/functions.php", + "src/Internal/functions.php" + ], + "psr-4": { + "Amp\\Http\\Client\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Daniel Lowrey", + "email": "rdlowrey@gmail.com" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + }, + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + } + ], + "description": "An advanced async HTTP client library for PHP, enabling efficient, non-blocking, and concurrent requests and responses.", + "homepage": "https://amphp.org/http-client", + "keywords": [ + "async", + "client", + "concurrent", + "http", + "non-blocking", + "rest" + ], + "support": { + "issues": "https://github.com/amphp/http-client/issues", + "source": "https://github.com/amphp/http-client/tree/v5.3.6" + }, + "funding": [ + { + "url": "https://github.com/amphp", + "type": "github" + } + ], + "time": "2026-05-15T23:29:38+00:00" + }, + { + "name": "amphp/http-server", + "version": "v3.4.6", + "source": { + "type": "git", + "url": "https://github.com/amphp/http-server.git", + "reference": "8a971bf92cf8cf2bc511f37a75b39126d5305315" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/amphp/http-server/zipball/8a971bf92cf8cf2bc511f37a75b39126d5305315", + "reference": "8a971bf92cf8cf2bc511f37a75b39126d5305315", + "shasum": "" + }, + "require": { + "amphp/amp": "^3", + "amphp/byte-stream": "^2", + "amphp/cache": "^2", + "amphp/hpack": "^3", + "amphp/http": "^2", + "amphp/pipeline": "^1", + "amphp/socket": "^2.1", + "amphp/sync": "^2.2", + "league/uri": "^7.1", + "league/uri-interfaces": "^7.1", + "php": ">=8.1", + "psr/http-message": "^1 | ^2", + "psr/log": "^1 | ^2 | ^3", + "revolt/event-loop": "^1" + }, + "require-dev": { + "amphp/http-client": "^5", + "amphp/log": "^2", + "amphp/php-cs-fixer-config": "^2", + "amphp/phpunit-util": "^3", + "league/uri-components": "^7.1", + "monolog/monolog": "^3", + "phpunit/phpunit": "^9", + "psalm/phar": "6.16.1" + }, + "suggest": { + "ext-zlib": "Allows GZip compression of response bodies" + }, + "type": "library", + "autoload": { + "files": [ + "src/Driver/functions.php", + "src/Middleware/functions.php", + "src/functions.php" + ], + "psr-4": { + "Amp\\Http\\Server\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Daniel Lowrey", + "email": "rdlowrey@php.net" + }, + { + "name": "Bob Weinand" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + }, + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + } + ], + "description": "A non-blocking HTTP application server for PHP based on Amp.", + "homepage": "https://github.com/amphp/http-server", + "keywords": [ + "amp", + "amphp", + "async", + "http", + "non-blocking", + "server" + ], + "support": { + "issues": "https://github.com/amphp/http-server/issues", + "source": "https://github.com/amphp/http-server/tree/v3.4.6" + }, + "funding": [ + { + "url": "https://github.com/amphp", + "type": "github" + } + ], + "time": "2026-06-27T10:31:48+00:00" + }, + { + "name": "amphp/parser", + "version": "v1.1.1", + "source": { + "type": "git", + "url": "https://github.com/amphp/parser.git", + "reference": "3cf1f8b32a0171d4b1bed93d25617637a77cded7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/amphp/parser/zipball/3cf1f8b32a0171d4b1bed93d25617637a77cded7", + "reference": "3cf1f8b32a0171d4b1bed93d25617637a77cded7", + "shasum": "" + }, + "require": { + "php": ">=7.4" + }, + "require-dev": { + "amphp/php-cs-fixer-config": "^2", + "phpunit/phpunit": "^9", + "psalm/phar": "^5.4" + }, + "type": "library", + "autoload": { + "psr-4": { + "Amp\\Parser\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + } + ], + "description": "A generator parser to make streaming parsers simple.", + "homepage": "https://github.com/amphp/parser", + "keywords": [ + "async", + "non-blocking", + "parser", + "stream" + ], + "support": { + "issues": "https://github.com/amphp/parser/issues", + "source": "https://github.com/amphp/parser/tree/v1.1.1" + }, + "funding": [ + { + "url": "https://github.com/amphp", + "type": "github" + } + ], + "time": "2024-03-21T19:16:53+00:00" + }, + { + "name": "amphp/pipeline", + "version": "v1.2.7", + "source": { + "type": "git", + "url": "https://github.com/amphp/pipeline.git", + "reference": "cf2d67696c2015ea7c7fd8f6ec5f8ed7c2d17c17" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/amphp/pipeline/zipball/cf2d67696c2015ea7c7fd8f6ec5f8ed7c2d17c17", + "reference": "cf2d67696c2015ea7c7fd8f6ec5f8ed7c2d17c17", + "shasum": "" + }, + "require": { + "amphp/amp": "^3", + "php": ">=8.1", + "revolt/event-loop": "^1" + }, + "require-dev": { + "amphp/php-cs-fixer-config": "^2", + "amphp/phpunit-util": "^3", + "phpunit/phpunit": "^9", + "psalm/phar": "6.16.1" + }, + "type": "library", + "autoload": { + "psr-4": { + "Amp\\Pipeline\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + } + ], + "description": "Asynchronous iterators and operators.", + "homepage": "https://amphp.org/pipeline", + "keywords": [ + "amp", + "amphp", + "async", + "io", + "iterator", + "non-blocking" + ], + "support": { + "issues": "https://github.com/amphp/pipeline/issues", + "source": "https://github.com/amphp/pipeline/tree/v1.2.7" + }, + "funding": [ + { + "url": "https://github.com/amphp", + "type": "github" + } + ], + "time": "2026-07-26T14:50:43+00:00" + }, + { + "name": "amphp/process", + "version": "v2.1.0", + "source": { + "type": "git", + "url": "https://github.com/amphp/process.git", + "reference": "583959df17d00304ad7b0b32285373f985935643" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/amphp/process/zipball/583959df17d00304ad7b0b32285373f985935643", + "reference": "583959df17d00304ad7b0b32285373f985935643", + "shasum": "" + }, + "require": { + "amphp/amp": "^3", + "amphp/byte-stream": "^2", + "amphp/sync": "^2", + "php": ">=8.1", + "revolt/event-loop": "^1 || ^0.2" + }, + "require-dev": { + "amphp/php-cs-fixer-config": "^2", + "amphp/phpunit-util": "^3", + "phpunit/phpunit": "^9", + "psalm/phar": "6.16.1" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "Amp\\Process\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Bob Weinand", + "email": "bobwei9@hotmail.com" + }, + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + } + ], + "description": "A fiber-aware process manager based on Amp and Revolt.", + "homepage": "https://amphp.org/process", + "support": { + "issues": "https://github.com/amphp/process/issues", + "source": "https://github.com/amphp/process/tree/v2.1.0" + }, + "funding": [ + { + "url": "https://github.com/amphp", + "type": "github" + } + ], + "time": "2026-05-31T15:11:55+00:00" + }, + { + "name": "amphp/serialization", + "version": "v1.1.0", + "source": { + "type": "git", + "url": "https://github.com/amphp/serialization.git", + "reference": "fdf2834d78cebb0205fb2672676c1b1eb84371f0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/amphp/serialization/zipball/fdf2834d78cebb0205fb2672676c1b1eb84371f0", + "reference": "fdf2834d78cebb0205fb2672676c1b1eb84371f0", + "shasum": "" + }, + "require": { + "php": ">=7.4" + }, + "require-dev": { + "amphp/php-cs-fixer-config": "^2", + "ext-json": "*", + "ext-zlib": "*", + "phpunit/phpunit": "^9", + "psalm/phar": "6.16.1" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "Amp\\Serialization\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + } + ], + "description": "Serialization tools for IPC and data storage in PHP.", + "homepage": "https://github.com/amphp/serialization", + "keywords": [ + "async", + "asynchronous", + "serialization", + "serialize" + ], + "support": { + "issues": "https://github.com/amphp/serialization/issues", + "source": "https://github.com/amphp/serialization/tree/v1.1.0" + }, + "funding": [ + { + "url": "https://github.com/amphp", + "type": "github" + } + ], + "time": "2026-04-05T15:59:53+00:00" + }, + { + "name": "amphp/socket", + "version": "v2.4.1", + "source": { + "type": "git", + "url": "https://github.com/amphp/socket.git", + "reference": "b347be5aff6b2cc025208bb4d896607eb470c018" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/amphp/socket/zipball/b347be5aff6b2cc025208bb4d896607eb470c018", + "reference": "b347be5aff6b2cc025208bb4d896607eb470c018", + "shasum": "" + }, + "require": { + "amphp/amp": "^3", + "amphp/byte-stream": "^2", + "amphp/dns": "^2", + "ext-openssl": "*", + "kelunik/certificate": "^1.1", + "league/uri": "^7", + "league/uri-interfaces": "^7", + "php": ">=8.1", + "revolt/event-loop": "^1" + }, + "require-dev": { + "amphp/php-cs-fixer-config": "^2", + "amphp/phpunit-util": "^3", + "amphp/process": "^2", + "phpunit/phpunit": "^9", + "psalm/phar": "6.16.1" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions.php", + "src/Internal/functions.php", + "src/SocketAddress/functions.php" + ], + "psr-4": { + "Amp\\Socket\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Daniel Lowrey", + "email": "rdlowrey@gmail.com" + }, + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + } + ], + "description": "Non-blocking socket connection / server implementations based on Amp and Revolt.", + "homepage": "https://github.com/amphp/socket", + "keywords": [ + "amp", + "async", + "encryption", + "non-blocking", + "sockets", + "tcp", + "tls" + ], + "support": { + "issues": "https://github.com/amphp/socket/issues", + "source": "https://github.com/amphp/socket/tree/v2.4.1" + }, + "funding": [ + { + "url": "https://github.com/amphp", + "type": "github" + } + ], + "time": "2026-08-22T18:13:20+00:00" + }, + { + "name": "amphp/sync", + "version": "v2.3.0", + "source": { + "type": "git", + "url": "https://github.com/amphp/sync.git", + "reference": "217097b785130d77cfcc58ff583cf26cd1770bf1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/amphp/sync/zipball/217097b785130d77cfcc58ff583cf26cd1770bf1", + "reference": "217097b785130d77cfcc58ff583cf26cd1770bf1", + "shasum": "" + }, + "require": { + "amphp/amp": "^3", + "amphp/pipeline": "^1", + "amphp/serialization": "^1", + "php": ">=8.1", + "revolt/event-loop": "^1 || ^0.2" + }, + "require-dev": { + "amphp/php-cs-fixer-config": "^2", + "amphp/phpunit-util": "^3", + "phpunit/phpunit": "^9", + "psalm/phar": "5.23" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "Amp\\Sync\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + }, + { + "name": "Stephen Coakley", + "email": "me@stephencoakley.com" + } + ], + "description": "Non-blocking synchronization primitives for PHP based on Amp and Revolt.", + "homepage": "https://github.com/amphp/sync", + "keywords": [ + "async", + "asynchronous", + "mutex", + "semaphore", + "synchronization" + ], + "support": { + "issues": "https://github.com/amphp/sync/issues", + "source": "https://github.com/amphp/sync/tree/v2.3.0" + }, + "funding": [ + { + "url": "https://github.com/amphp", + "type": "github" + } + ], + "time": "2024-08-03T19:31:26+00:00" + }, + { + "name": "amphp/websocket", + "version": "v2.0.4", + "source": { + "type": "git", + "url": "https://github.com/amphp/websocket.git", + "reference": "963904b6a883c4b62d9222d1d9749814fac96a3b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/amphp/websocket/zipball/963904b6a883c4b62d9222d1d9749814fac96a3b", + "reference": "963904b6a883c4b62d9222d1d9749814fac96a3b", + "shasum": "" + }, + "require": { + "amphp/amp": "^3", + "amphp/byte-stream": "^2", + "amphp/parser": "^1", + "amphp/pipeline": "^1", + "amphp/socket": "^2", + "php": ">=8.1", + "revolt/event-loop": "^1" + }, + "require-dev": { + "amphp/php-cs-fixer-config": "^2", + "amphp/phpunit-util": "^3", + "phpunit/phpunit": "^9", + "psalm/phar": "^5.18" + }, + "suggest": { + "ext-zlib": "Required for compression" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "Amp\\Websocket\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + }, + { + "name": "Bob Weinand", + "email": "bobwei9@hotmail.com" + } + ], + "description": "Shared code for websocket servers and clients.", + "homepage": "https://github.com/amphp/websocket", + "keywords": [ + "amp", + "amphp", + "async", + "http", + "non-blocking", + "websocket" + ], + "support": { + "issues": "https://github.com/amphp/websocket/issues", + "source": "https://github.com/amphp/websocket/tree/v2.0.4" + }, + "funding": [ + { + "url": "https://github.com/amphp", + "type": "github" + } + ], + "time": "2024-10-28T21:28:45+00:00" + }, + { + "name": "amphp/websocket-client", + "version": "v2.0.2", + "source": { + "type": "git", + "url": "https://github.com/amphp/websocket-client.git", + "reference": "dc033fdce0af56295a23f63ac4f579b34d470d6c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/amphp/websocket-client/zipball/dc033fdce0af56295a23f63ac4f579b34d470d6c", + "reference": "dc033fdce0af56295a23f63ac4f579b34d470d6c", + "shasum": "" + }, + "require": { + "amphp/amp": "^3", + "amphp/byte-stream": "^2.1", + "amphp/http": "^2.1", + "amphp/http-client": "^5", + "amphp/socket": "^2.2", + "amphp/websocket": "^2", + "league/uri": "^7.1", + "php": ">=8.1", + "psr/http-message": "^1|^2", + "revolt/event-loop": "^1" + }, + "require-dev": { + "amphp/http-server": "^3", + "amphp/php-cs-fixer-config": "^2", + "amphp/phpunit-util": "^3", + "amphp/websocket-server": "^3|^4", + "phpunit/phpunit": "^9", + "psalm/phar": "~5.26.1", + "psr/log": "^1" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "Amp\\Websocket\\Client\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Bob Weinand", + "email": "bobwei9@hotmail.com" + }, + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + } + ], + "description": "Async WebSocket client for PHP based on Amp.", + "keywords": [ + "amp", + "amphp", + "async", + "client", + "http", + "non-blocking", + "websocket" + ], + "support": { + "issues": "https://github.com/amphp/websocket-client/issues", + "source": "https://github.com/amphp/websocket-client/tree/v2.0.2" + }, + "funding": [ + { + "url": "https://github.com/amphp", + "type": "github" + } + ], + "time": "2025-08-24T17:25:34+00:00" + }, { "name": "brianium/paratest", "version": "v7.24.1", @@ -8417,6 +9262,50 @@ ], "time": "2025-08-20T19:15:30+00:00" }, + { + "name": "daverandom/libdns", + "version": "v2.1.0", + "source": { + "type": "git", + "url": "https://github.com/DaveRandom/LibDNS.git", + "reference": "b84c94e8fe6b7ee4aecfe121bfe3b6177d303c8a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/DaveRandom/LibDNS/zipball/b84c94e8fe6b7ee4aecfe121bfe3b6177d303c8a", + "reference": "b84c94e8fe6b7ee4aecfe121bfe3b6177d303c8a", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "php": ">=7.1" + }, + "suggest": { + "ext-intl": "Required for IDN support" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "LibDNS\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "DNS protocol implementation written in pure PHP", + "keywords": [ + "dns" + ], + "support": { + "issues": "https://github.com/DaveRandom/LibDNS/issues", + "source": "https://github.com/DaveRandom/LibDNS/tree/v2.1.0" + }, + "time": "2024-04-12T12:12:48+00:00" + }, { "name": "fakerphp/faker", "version": "v1.24.1", @@ -8727,6 +9616,64 @@ }, "time": "2025-03-19T14:43:43+00:00" }, + { + "name": "kelunik/certificate", + "version": "v1.1.3", + "source": { + "type": "git", + "url": "https://github.com/kelunik/certificate.git", + "reference": "7e00d498c264d5eb4f78c69f41c8bd6719c0199e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/kelunik/certificate/zipball/7e00d498c264d5eb4f78c69f41c8bd6719c0199e", + "reference": "7e00d498c264d5eb4f78c69f41c8bd6719c0199e", + "shasum": "" + }, + "require": { + "ext-openssl": "*", + "php": ">=7.0" + }, + "require-dev": { + "amphp/php-cs-fixer-config": "^2", + "phpunit/phpunit": "^6 | 7 | ^8 | ^9" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Kelunik\\Certificate\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + } + ], + "description": "Access certificate details and transform between different formats.", + "keywords": [ + "DER", + "certificate", + "certificates", + "openssl", + "pem", + "x509" + ], + "support": { + "issues": "https://github.com/kelunik/certificate/issues", + "source": "https://github.com/kelunik/certificate/tree/v1.1.3" + }, + "time": "2023-02-03T21:26:53+00:00" + }, { "name": "laravel/boost", "version": "v2.8.0", @@ -9142,6 +10089,90 @@ }, "time": "2026-08-12T13:55:56+00:00" }, + { + "name": "league/uri-components", + "version": "7.8.1", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/uri-components.git", + "reference": "848ff9db2f0be06229d6034b7c2e33d41b4fd675" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/uri-components/zipball/848ff9db2f0be06229d6034b7c2e33d41b4fd675", + "reference": "848ff9db2f0be06229d6034b7c2e33d41b4fd675", + "shasum": "" + }, + "require": { + "league/uri": "^7.8.1", + "php": "^8.1" + }, + "suggest": { + "ext-bcmath": "to improve IPV4 host parsing", + "ext-fileinfo": "to create Data URI from file contennts", + "ext-gmp": "to improve IPV4 host parsing", + "ext-intl": "to handle IDN host with the best performance", + "ext-mbstring": "to use the sorting algorithm of URLSearchParams", + "jeremykendall/php-domain-parser": "to further parse the URI host and resolve its Public Suffix and Top Level Domain", + "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", + "extra": { + "branch-alias": { + "dev-master": "7.x-dev" + } + }, + "autoload": { + "psr-4": { + "League\\Uri\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ignace Nyamagana Butera", + "email": "nyamsprod@gmail.com", + "homepage": "https://nyamsprod.com" + } + ], + "description": "URI components manipulation library", + "homepage": "http://uri.thephpleague.com", + "keywords": [ + "authority", + "components", + "fragment", + "host", + "middleware", + "modifier", + "path", + "port", + "query", + "rfc3986", + "scheme", + "uri", + "url", + "userinfo" + ], + "support": { + "docs": "https://uri.thephpleague.com", + "forum": "https://thephpleague.slack.com", + "issues": "https://github.com/thephpleague/uri-src/issues", + "source": "https://github.com/thephpleague/uri-components/tree/7.8.1" + }, + "funding": [ + { + "url": "https://github.com/nyamsprod", + "type": "github" + } + ], + "time": "2026-03-15T20:22:25+00:00" + }, { "name": "mockery/mockery", "version": "1.6.15", @@ -9643,6 +10674,90 @@ ], "time": "2026-07-21T07:48:55+00:00" }, + { + "name": "pestphp/pest-plugin-browser", + "version": "v5.0.1", + "source": { + "type": "git", + "url": "https://github.com/pestphp/pest-plugin-browser.git", + "reference": "8f1c2103dea37f68a674e59869e42a80fbf8a679" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/pestphp/pest-plugin-browser/zipball/8f1c2103dea37f68a674e59869e42a80fbf8a679", + "reference": "8f1c2103dea37f68a674e59869e42a80fbf8a679", + "shasum": "" + }, + "require": { + "amphp/amp": "^3.1.3", + "amphp/http-server": "^3.4.6", + "amphp/websocket-client": "^2.0.2", + "ext-sockets": "*", + "pestphp/pest": "^5.0.4", + "pestphp/pest-plugin": "^5.0.0", + "php": "^8.4", + "symfony/process": "^8.1.0" + }, + "conflict": { + "pestphp/pest": "<5.0.0" + }, + "require-dev": { + "livewire/livewire": "^4.3.5", + "nunomaduro/collision": "^8.9.5", + "orchestra/testbench": "^11.1", + "pestphp/pest-dev-tools": "^5.0.0", + "pestphp/pest-plugin-laravel": "^5.0.1", + "pestphp/pest-plugin-type-coverage": "^5.0.2" + }, + "type": "library", + "extra": { + "pest": { + "plugins": [ + "Pest\\Browser\\Plugin" + ] + } + }, + "autoload": { + "files": [ + "src/Autoload.php" + ], + "psr-4": { + "Pest\\Browser\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Pest plugin to test browser interactions", + "keywords": [ + "browser", + "framework", + "pest", + "php", + "test", + "testing", + "unit" + ], + "support": { + "source": "https://github.com/pestphp/pest-plugin-browser/tree/v5.0.1" + }, + "funding": [ + { + "url": "https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=66BYDWAT92N6L", + "type": "custom" + }, + { + "url": "https://github.com/nunomaduro", + "type": "github" + }, + { + "url": "https://www.patreon.com/nunomaduro", + "type": "patreon" + } + ], + "time": "2026-08-10T14:48:55+00:00" + }, { "name": "pestphp/pest-plugin-laravel", "version": "v5.0.1", @@ -10452,6 +11567,78 @@ ], "time": "2026-08-27T08:40:49+00:00" }, + { + "name": "revolt/event-loop", + "version": "v1.0.9", + "source": { + "type": "git", + "url": "https://github.com/revoltphp/event-loop.git", + "reference": "44061cf513e53c6200372fc935ac42271566295d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/revoltphp/event-loop/zipball/44061cf513e53c6200372fc935ac42271566295d", + "reference": "44061cf513e53c6200372fc935ac42271566295d", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "ext-json": "*", + "jetbrains/phpstorm-stubs": "^2019.3", + "phpunit/phpunit": "^9", + "psalm/phar": "6.16.*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Revolt\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "ceesjank@gmail.com" + }, + { + "name": "Christian Lück", + "email": "christian@clue.engineering" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + } + ], + "description": "Rock-solid event loop for concurrent PHP applications.", + "keywords": [ + "async", + "asynchronous", + "concurrency", + "event", + "event-loop", + "non-blocking", + "scheduler" + ], + "support": { + "issues": "https://github.com/revoltphp/event-loop/issues", + "source": "https://github.com/revoltphp/event-loop/tree/v1.0.9" + }, + "time": "2026-05-16T17:55:38+00:00" + }, { "name": "sebastian/cli-parser", "version": "5.0.1", diff --git a/config/fortify.php b/config/fortify.php index cad446f..7ef977b 100644 --- a/config/fortify.php +++ b/config/fortify.php @@ -73,7 +73,7 @@ return [ | */ - 'home' => '/dashboard', + 'home' => '/admin/dashboard', /* |-------------------------------------------------------------------------- diff --git a/config/livewire-material.php b/config/livewire-material.php new file mode 100644 index 0000000..52e2f3c --- /dev/null +++ b/config/livewire-material.php @@ -0,0 +1,152 @@ +, and so on; set a prefix such as 'm' when a name + | clashes with one of the application's own components, and they become + | , . They are always + | as well. + | + */ + + 'prefix' => '', + + /* + |-------------------------------------------------------------------------- + | Theme + |-------------------------------------------------------------------------- + | + | The head script decides the theme before the first paint and writes it to + | . 'default' is used until the visitor chooses: 'light', + | 'dark' or 'system' (follow the operating system). The choice is kept in + | localStorage under 'storage_key'; values found under 'legacy_keys' (an + | earlier theme toggle's key) are adopted once and then removed. + | + */ + + 'theme' => [ + 'default' => 'system', + 'storage_key' => 'sealshare-theme', + 'legacy_keys' => ['mary-theme'], + ], + + /* + |-------------------------------------------------------------------------- + | Navigation rail + |-------------------------------------------------------------------------- + | + | Whether a collapsible navigation rail starts 'expanded' or 'collapsed' + | until the visitor toggles it. The head script applies the choice before + | the first paint, from localStorage under 'storage_key'. + | + */ + + 'rail' => [ + 'default' => 'expanded', + 'storage_key' => 'material-rail', + ], + + /* + |-------------------------------------------------------------------------- + | Fields + |-------------------------------------------------------------------------- + | + | Text fields, selects and pickers come in M3's two styles: 'outlined' (a + | notched outline) and 'filled' (a tinted box with an indicator line). This + | is the style a field takes when its `variant` is not given. + | + */ + + 'fields' => [ + 'variant' => 'outlined', + ], + + /* + |-------------------------------------------------------------------------- + | Pagination + |-------------------------------------------------------------------------- + | + | Draw Laravel's and Livewire's paginators in M3: the package's views are + | put in front of `pagination::tailwind` and `livewire::tailwind` (and + | their simple versions). An application's own published pagination views + | still win. + | + */ + + 'pagination' => true, + + /* + |-------------------------------------------------------------------------- + | Node + |-------------------------------------------------------------------------- + | + | `php artisan material:scheme` runs Google's colour utilities through Node. + | Set the binary when `node` is not on the PATH of the user running Artisan. + | + */ + + 'node' => env('MATERIAL_NODE', 'node'), + + /* + |-------------------------------------------------------------------------- + | Scheme data + |-------------------------------------------------------------------------- + | + | The light and dark hexes `php artisan material:scheme` writes beside the + | stylesheet. The mail theme reads its colours here, and so does an error + | page when the build is missing; without the file both use the package's + | default scheme. + | + */ + + 'scheme' => resource_path('css/material-scheme.json'), + + /* + |-------------------------------------------------------------------------- + | Mail + |-------------------------------------------------------------------------- + | + | Markdown mail takes the theme when `mail.markdown.theme` (MAIL_MARKDOWN_THEME) + | is 'livewire-material::mail.theme'. 'components' puts this package's mail + | header and message after the application's own mail components — or + | publish them with `vendor:publish --tag=livewire-material-mail` instead. + | 'logo' replaces the app name in that header with an image: an absolute + | 'src', with 'width' and 'height' in pixels, which Outlook sizes it by. + | + */ + + 'mail' => [ + 'components' => (bool) env('MATERIAL_MAIL_COMPONENTS', false), + 'logo' => [ + 'src' => null, + 'width' => null, + 'height' => null, + ], + ], + + /* + |-------------------------------------------------------------------------- + | Showcase + |-------------------------------------------------------------------------- + | + | Every component in every variant, rendered in the application's own + | scheme. Off unless the application runs locally. 'vite' names the entry + | points that import this package's CSS and JavaScript; the error pages + | load them too, showcase or not. + | + */ + + 'showcase' => [ + 'enabled' => (bool) env('MATERIAL_SHOWCASE', env('APP_ENV', 'production') === 'local'), + 'path' => 'material', + 'middleware' => ['web'], + 'vite' => ['resources/css/app.css', 'resources/js/app.js'], + ], + +]; diff --git a/docker/dev-entrypoint.sh b/docker/dev-entrypoint.sh index aeefbfa..32212ab 100755 --- a/docker/dev-entrypoint.sh +++ b/docker/dev-entrypoint.sh @@ -13,6 +13,11 @@ max_input_time = ${PHP_MAX_INPUT_TIME:-300} memory_limit = ${PHP_MEMORY_LIMIT:-512M} EOF +if [ ! -f vendor/autoload.php ]; then + echo "[dev] Installing PHP dependencies..." + composer install --no-interaction 2>&1 +fi + echo "[dev] Installing Node dependencies..." npm install 2>&1 diff --git a/docker/dev.Dockerfile b/docker/dev.Dockerfile index f821183..9f5d5e8 100644 --- a/docker/dev.Dockerfile +++ b/docker/dev.Dockerfile @@ -8,6 +8,9 @@ RUN install-php-extensions \ # Install Node.js for Vite / frontend asset building RUN apk add --no-cache nodejs npm +# Composer, for a checkout without vendor/: the assets import Livewire Material from it +COPY --from=composer:2 /usr/bin/composer /usr/bin/composer + WORKDIR /app COPY docker/dev-entrypoint.sh /usr/local/bin/dev-entrypoint.sh diff --git a/docs/plans/livewire-material.md b/docs/plans/livewire-material.md index 524c69f..7006309 100644 --- a/docs/plans/livewire-material.md +++ b/docs/plans/livewire-material.md @@ -74,8 +74,8 @@ The package's decisions are in its own plan. SealShare's: - **Converts after `1.0.0`, in one pass, by hand** (~150 tags; no codemod), on branch `material`, released as **2.0.0**. - **Moving SealShare to Gitea is a separate plan** — this plan works wherever it is hosted. -- **Seed `#4f46e5` (the favicon's indigo), Tonal Spot**; Vibrant generated alongside on the - upload page for one visual comparison before committing. +- **Seed `#4f46e5` (the favicon's indigo), Vibrant** — chosen after comparing it with Tonal Spot + on the upload page in both themes (2026-09-13): Tonal Spot read grey-lavender on this seed. - **Theme default `system`**, storage key `sealshare-theme`, legacy `mary-theme` adopted once. Appearance is a Light / Dark / System connected button group. - **One top app bar everywhere** — logo and site title; a theme toggle for guests, an avatar diff --git a/package-lock.json b/package-lock.json index 94594e8..dc8735b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6,16 +6,15 @@ "": { "dependencies": { "@tailwindcss/vite": "^4.3.3", - "alpinejs": "^3.17.2", "autoprefixer": "^10.5.5", "concurrently": "^10.0.5", - "daisyui": "^5.7.32", "laravel-vite-plugin": "^3.2.0", "tailwindcss": "^4.3.3", "vite": "^8.2.2" }, "devDependencies": { - "chokidar": "^5.0.0" + "chokidar": "^5.0.0", + "playwright": "^1.63.0" }, "optionalDependencies": { "@tailwindcss/oxide-linux-x64-gnu": "^4.0.1", @@ -609,30 +608,6 @@ "vite": "^5.2.0 || ^6 || ^7 || ^8" } }, - "node_modules/@vue/reactivity": { - "version": "3.5.42", - "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.42.tgz", - "integrity": "sha512-TzNNfKpb7hDxbQltwAut8VDQA5YP+BuRlxntHUuRjyKwlMvmAPbs3unhCvieijifY6vFfVBwsS7wG/C7uq+bEQ==", - "license": "MIT", - "dependencies": { - "@vue/shared": "3.5.42" - } - }, - "node_modules/@vue/shared": { - "version": "3.5.42", - "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.42.tgz", - "integrity": "sha512-2rPxex1jQf4jvl9MOHl6YaXCPcrNqz/FstMOEh3QWY+/OME9nQTvl9WYeCwhW7AFjaR0SnngZGlp/wkR6rkI6g==", - "license": "MIT" - }, - "node_modules/alpinejs": { - "version": "3.17.2", - "resolved": "https://registry.npmjs.org/alpinejs/-/alpinejs-3.17.2.tgz", - "integrity": "sha512-xbnG3LlnmFkiNO49C+qhqjEDqOdB0snKqvT48ZVp8TakpZOpsoQbc/jAxpptQRjZi7c9rLeCfvwQ/XOSirIZFQ==", - "license": "MIT", - "dependencies": { - "@vue/reactivity": "~3.5.40" - } - }, "node_modules/ansi-regex": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.3.0.tgz", @@ -824,15 +799,6 @@ "url": "https://github.com/open-cli-tools/concurrently?sponsor=1" } }, - "node_modules/daisyui": { - "version": "5.7.32", - "resolved": "https://registry.npmjs.org/daisyui/-/daisyui-5.7.32.tgz", - "integrity": "sha512-8JQFneb+okHxzPimTGvRKX+5XF03GyAwGw7BaV6+6XpzW0klGuzc4rdfUpLQxIYPKEziwwlrsYfcxoltF2Rowg==", - "license": "MIT", - "funding": { - "url": "https://github.com/saadeghi/daisyui?sponsor=1" - } - }, "node_modules/detect-libc": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", @@ -1320,6 +1286,35 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/playwright": { + "version": "1.63.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.63.0.tgz", + "integrity": "sha512-+7ziBLidS4NaNCdt57SUDT+wYmmd5fmiQejUic/kb+YsYSCPyOOE9sebzMjNmQrsnNpDJqd4WHvV/8lfKfUDUg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.63.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/playwright-core": { + "version": "1.63.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.63.0.tgz", + "integrity": "sha512-rYCsBF/M5HjUch52bbtVONEFjv6Xu8sm8h72dNlR5bzIE1fvC/bxgspzkjSfU+MweEMmPM8KJebG6nnyxo5mCg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/postcss": { "version": "8.5.28", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.28.tgz", diff --git a/package.json b/package.json index a792b37..d783ed0 100644 --- a/package.json +++ b/package.json @@ -8,10 +8,8 @@ }, "dependencies": { "@tailwindcss/vite": "^4.3.3", - "alpinejs": "^3.17.2", "autoprefixer": "^10.5.5", "concurrently": "^10.0.5", - "daisyui": "^5.7.32", "laravel-vite-plugin": "^3.2.0", "tailwindcss": "^4.3.3", "vite": "^8.2.2" @@ -24,6 +22,7 @@ "shell-quote": "^1.9.0" }, "devDependencies": { - "chokidar": "^5.0.0" + "chokidar": "^5.0.0", + "playwright": "^1.63.0" } } diff --git a/resources/css/app.css b/resources/css/app.css index 0ac24fc..d9bfc69 100644 --- a/resources/css/app.css +++ b/resources/css/app.css @@ -1,10 +1,22 @@ @import 'tailwindcss'; +@import '../../vendor/nonameweb/livewire-material/resources/css/material.css'; +@import './material-scheme.css'; @source '../views'; -@source '../../vendor/robsontenorio/mary/src/View/Components/**/*.php'; -@source '../../vendor/laravel/framework/src/Illuminate/Pagination/resources/views/*.blade.php'; -@source inline("swap swap-rotate swap-on swap-off theme-controller"); +@source '../../vendor/nonameweb/livewire-material/resources/views'; +@source '../../vendor/nonameweb/livewire-material/src'; -@plugin "daisyui" { - themes: light --default, dark --prefersdark; +/* share-created: the check on its shape settles in once the link is ready. */ +@keyframes share-ready { + from { + opacity: 0; + rotate: -90deg; + scale: 0.4; + } + + to { + opacity: 1; + rotate: 0deg; + scale: 1; + } } diff --git a/resources/css/material-scheme.css b/resources/css/material-scheme.css new file mode 100644 index 0000000..94f99b7 --- /dev/null +++ b/resources/css/material-scheme.css @@ -0,0 +1,150 @@ +/* + * Material 3 colour roles, generated by Google's material-color-utilities (spec 2025). + * + * php artisan material:scheme "#4f46e5" --variant=vibrant + * + * Regenerate rather than editing a value: every pair here (a role and its on-role) carries + * M3's contrast guarantee only as generated. The head script sets data-theme before the + * first paint; the light block also stands without it. + */ + +:root, +[data-theme='light'] { + color-scheme: light; + + --md-sys-color-background: #faf4ff; + --md-sys-color-on-background: #32294f; + --md-sys-color-surface: #faf4ff; + --md-sys-color-surface-dim: #dacdff; + --md-sys-color-surface-bright: #faf4ff; + --md-sys-color-surface-container-lowest: #ffffff; + --md-sys-color-surface-container-low: #f5eeff; + --md-sys-color-surface-container: #ede4ff; + --md-sys-color-surface-container-high: #e8deff; + --md-sys-color-surface-container-highest: #e2d7ff; + --md-sys-color-on-surface: #32294f; + --md-sys-color-on-surface-variant: #5f557f; + --md-sys-color-outline: #7b719c; + --md-sys-color-outline-variant: #b2a6d5; + --md-sys-color-inverse-surface: #10062d; + --md-sys-color-inverse-on-surface: #a296c4; + --md-sys-color-primary: #4a3fe2; + --md-sys-color-primary-dim: #3d2fd6; + --md-sys-color-on-primary: #f4f1ff; + --md-sys-color-primary-container: #9795ff; + --md-sys-color-on-primary-container: #14007e; + --md-sys-color-primary-fixed: #9795ff; + --md-sys-color-primary-fixed-dim: #8885ff; + --md-sys-color-on-primary-fixed: #000000; + --md-sys-color-on-primary-fixed-variant: #1a0099; + --md-sys-color-inverse-primary: #8582ff; + --md-sys-color-secondary: #6249b2; + --md-sys-color-secondary-dim: #563ca5; + --md-sys-color-on-secondary: #f7f0ff; + --md-sys-color-secondary-container: #d8caff; + --md-sys-color-on-secondary-container: #4e339c; + --md-sys-color-secondary-fixed: #d8caff; + --md-sys-color-secondary-fixed-dim: #cbbaff; + --md-sys-color-on-secondary-fixed: #3a1b88; + --md-sys-color-on-secondary-fixed-variant: #573da6; + --md-sys-color-tertiary: #983772; + --md-sys-color-tertiary-dim: #892a65; + --md-sys-color-on-tertiary: #ffeff4; + --md-sys-color-tertiary-container: #fd8bca; + --md-sys-color-on-tertiary-container: #610244; + --md-sys-color-tertiary-fixed: #fd8bca; + --md-sys-color-tertiary-fixed-dim: #ee7ebc; + --md-sys-color-on-tertiary-fixed: #360024; + --md-sys-color-on-tertiary-fixed-variant: #6d104e; + --md-sys-color-error: #b41340; + --md-sys-color-error-dim: #a20036; + --md-sys-color-on-error: #ffefef; + --md-sys-color-error-container: #f74b6d; + --md-sys-color-on-error-container: #510017; + --md-sys-color-success: #006c45; + --md-sys-color-on-success: #ffffff; + --md-sys-color-success-container: #86f9bc; + --md-sys-color-on-success-container: #002112; + --md-sys-color-warning: #7c5800; + --md-sys-color-on-warning: #ffffff; + --md-sys-color-warning-container: #ffdea6; + --md-sys-color-on-warning-container: #271900; + --md-sys-color-info: #005ac4; + --md-sys-color-on-info: #ffffff; + --md-sys-color-info-container: #d8e2ff; + --md-sys-color-on-info-container: #001a42; + --md-sys-color-inverse-error: #ff6e84; + --md-sys-color-inverse-success: #69dca1; + --md-sys-color-inverse-warning: #fdbb28; + --md-sys-color-inverse-info: #aec6ff; +} + +[data-theme='dark'] { + color-scheme: dark; + + --md-sys-color-background: #10062d; + --md-sys-color-on-background: #eae1ff; + --md-sys-color-surface: #10062d; + --md-sys-color-surface-dim: #10062d; + --md-sys-color-surface-bright: #30215d; + --md-sys-color-surface-container-lowest: #000000; + --md-sys-color-surface-container-low: #160b36; + --md-sys-color-surface-container: #1c113f; + --md-sys-color-surface-container-high: #231649; + --md-sys-color-surface-container-highest: #291c53; + --md-sys-color-on-surface: #eae1ff; + --md-sys-color-on-surface-variant: #b0a4d3; + --md-sys-color-outline: #7a6f9b; + --md-sys-color-outline-variant: #4b426a; + --md-sys-color-inverse-surface: #fdf7ff; + --md-sys-color-inverse-on-surface: #594f78; + --md-sys-color-primary: #a7a5ff; + --md-sys-color-primary-dim: #645dfc; + --md-sys-color-on-primary: #1c00a0; + --md-sys-color-primary-container: #9795ff; + --md-sys-color-on-primary-container: #14007e; + --md-sys-color-primary-fixed: #9795ff; + --md-sys-color-primary-fixed-dim: #8885ff; + --md-sys-color-on-primary-fixed: #000000; + --md-sys-color-on-primary-fixed-variant: #1a0099; + --md-sys-color-inverse-primary: #4d44e6; + --md-sys-color-secondary: #a98ffd; + --md-sys-color-secondary-dim: #a68dfa; + --md-sys-color-on-secondary: #280072; + --md-sys-color-secondary-container: #4d329b; + --md-sys-color-on-secondary-container: #d6c9ff; + --md-sys-color-secondary-fixed: #d8caff; + --md-sys-color-secondary-fixed-dim: #cbbaff; + --md-sys-color-on-secondary-fixed: #3a1b88; + --md-sys-color-on-secondary-fixed-variant: #573da6; + --md-sys-color-tertiary: #ff9dd1; + --md-sys-color-tertiary-dim: #fa88c8; + --md-sys-color-on-tertiary: #6c0f4d; + --md-sys-color-tertiary-container: #fa88c8; + --md-sys-color-on-tertiary-container: #5e0042; + --md-sys-color-tertiary-fixed: #fd8bca; + --md-sys-color-tertiary-fixed-dim: #ee7ebc; + --md-sys-color-on-tertiary-fixed: #360024; + --md-sys-color-on-tertiary-fixed-variant: #6d104e; + --md-sys-color-error: #ff6e84; + --md-sys-color-error-dim: #d73357; + --md-sys-color-on-error: #490013; + --md-sys-color-error-container: #a70138; + --md-sys-color-on-error-container: #ffb2b9; + --md-sys-color-success: #69dca1; + --md-sys-color-on-success: #003822; + --md-sys-color-success-container: #005233; + --md-sys-color-on-success-container: #86f9bc; + --md-sys-color-warning: #fdbb28; + --md-sys-color-on-warning: #412d00; + --md-sys-color-warning-container: #5e4200; + --md-sys-color-on-warning-container: #ffdea6; + --md-sys-color-info: #aec6ff; + --md-sys-color-on-info: #002e6a; + --md-sys-color-info-container: #004396; + --md-sys-color-on-info-container: #d8e2ff; + --md-sys-color-inverse-error: #b41340; + --md-sys-color-inverse-success: #006c45; + --md-sys-color-inverse-warning: #7c5800; + --md-sys-color-inverse-info: #005ac4; +} diff --git a/resources/css/material-scheme.json b/resources/css/material-scheme.json new file mode 100644 index 0000000..a39ec6b --- /dev/null +++ b/resources/css/material-scheme.json @@ -0,0 +1,140 @@ +{ + "seed": "#4f46e5", + "variant": "vibrant", + "spec": "2025", + "contrast": 0, + "light": { + "background": "#faf4ff", + "on-background": "#32294f", + "surface": "#faf4ff", + "surface-dim": "#dacdff", + "surface-bright": "#faf4ff", + "surface-container-lowest": "#ffffff", + "surface-container-low": "#f5eeff", + "surface-container": "#ede4ff", + "surface-container-high": "#e8deff", + "surface-container-highest": "#e2d7ff", + "on-surface": "#32294f", + "on-surface-variant": "#5f557f", + "outline": "#7b719c", + "outline-variant": "#b2a6d5", + "inverse-surface": "#10062d", + "inverse-on-surface": "#a296c4", + "primary": "#4a3fe2", + "primary-dim": "#3d2fd6", + "on-primary": "#f4f1ff", + "primary-container": "#9795ff", + "on-primary-container": "#14007e", + "primary-fixed": "#9795ff", + "primary-fixed-dim": "#8885ff", + "on-primary-fixed": "#000000", + "on-primary-fixed-variant": "#1a0099", + "inverse-primary": "#8582ff", + "secondary": "#6249b2", + "secondary-dim": "#563ca5", + "on-secondary": "#f7f0ff", + "secondary-container": "#d8caff", + "on-secondary-container": "#4e339c", + "secondary-fixed": "#d8caff", + "secondary-fixed-dim": "#cbbaff", + "on-secondary-fixed": "#3a1b88", + "on-secondary-fixed-variant": "#573da6", + "tertiary": "#983772", + "tertiary-dim": "#892a65", + "on-tertiary": "#ffeff4", + "tertiary-container": "#fd8bca", + "on-tertiary-container": "#610244", + "tertiary-fixed": "#fd8bca", + "tertiary-fixed-dim": "#ee7ebc", + "on-tertiary-fixed": "#360024", + "on-tertiary-fixed-variant": "#6d104e", + "error": "#b41340", + "error-dim": "#a20036", + "on-error": "#ffefef", + "error-container": "#f74b6d", + "on-error-container": "#510017", + "success": "#006c45", + "on-success": "#ffffff", + "success-container": "#86f9bc", + "on-success-container": "#002112", + "warning": "#7c5800", + "on-warning": "#ffffff", + "warning-container": "#ffdea6", + "on-warning-container": "#271900", + "info": "#005ac4", + "on-info": "#ffffff", + "info-container": "#d8e2ff", + "on-info-container": "#001a42", + "inverse-error": "#ff6e84", + "inverse-success": "#69dca1", + "inverse-warning": "#fdbb28", + "inverse-info": "#aec6ff" + }, + "dark": { + "background": "#10062d", + "on-background": "#eae1ff", + "surface": "#10062d", + "surface-dim": "#10062d", + "surface-bright": "#30215d", + "surface-container-lowest": "#000000", + "surface-container-low": "#160b36", + "surface-container": "#1c113f", + "surface-container-high": "#231649", + "surface-container-highest": "#291c53", + "on-surface": "#eae1ff", + "on-surface-variant": "#b0a4d3", + "outline": "#7a6f9b", + "outline-variant": "#4b426a", + "inverse-surface": "#fdf7ff", + "inverse-on-surface": "#594f78", + "primary": "#a7a5ff", + "primary-dim": "#645dfc", + "on-primary": "#1c00a0", + "primary-container": "#9795ff", + "on-primary-container": "#14007e", + "primary-fixed": "#9795ff", + "primary-fixed-dim": "#8885ff", + "on-primary-fixed": "#000000", + "on-primary-fixed-variant": "#1a0099", + "inverse-primary": "#4d44e6", + "secondary": "#a98ffd", + "secondary-dim": "#a68dfa", + "on-secondary": "#280072", + "secondary-container": "#4d329b", + "on-secondary-container": "#d6c9ff", + "secondary-fixed": "#d8caff", + "secondary-fixed-dim": "#cbbaff", + "on-secondary-fixed": "#3a1b88", + "on-secondary-fixed-variant": "#573da6", + "tertiary": "#ff9dd1", + "tertiary-dim": "#fa88c8", + "on-tertiary": "#6c0f4d", + "tertiary-container": "#fa88c8", + "on-tertiary-container": "#5e0042", + "tertiary-fixed": "#fd8bca", + "tertiary-fixed-dim": "#ee7ebc", + "on-tertiary-fixed": "#360024", + "on-tertiary-fixed-variant": "#6d104e", + "error": "#ff6e84", + "error-dim": "#d73357", + "on-error": "#490013", + "error-container": "#a70138", + "on-error-container": "#ffb2b9", + "success": "#69dca1", + "on-success": "#003822", + "success-container": "#005233", + "on-success-container": "#86f9bc", + "warning": "#fdbb28", + "on-warning": "#412d00", + "warning-container": "#5e4200", + "on-warning-container": "#ffdea6", + "info": "#aec6ff", + "on-info": "#002e6a", + "info-container": "#004396", + "on-info-container": "#d8e2ff", + "inverse-error": "#b41340", + "inverse-success": "#006c45", + "inverse-warning": "#7c5800", + "inverse-info": "#005ac4" + } +} diff --git a/resources/js/app.js b/resources/js/app.js index 19db2a3..6dfabc7 100644 --- a/resources/js/app.js +++ b/resources/js/app.js @@ -1,2 +1,2 @@ -// Alpine.js is bundled and started automatically by Livewire 4. -// Do not import it here to avoid "multiple instances of Alpine" errors. +// Livewire Material. Alpine is bundled and started by Livewire 4: never import it here as well. +import '../../vendor/nonameweb/livewire-material/resources/js/material.js' diff --git a/resources/views/components/action-message.blade.php b/resources/views/components/action-message.blade.php deleted file mode 100644 index d313ee6..0000000 --- a/resources/views/components/action-message.blade.php +++ /dev/null @@ -1,14 +0,0 @@ -@props([ - 'on', -]) - -
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 deleted file mode 100644 index 872af8e..0000000 --- a/resources/views/components/app-logo.blade.php +++ /dev/null @@ -1,4 +0,0 @@ -merge(['class' => 'flex items-center gap-2 font-semibold']) }} wire:navigate> - - {{ \App\Models\Setting::get('site_title') ?: config('app.name', 'SealShare') }} - diff --git a/resources/views/components/auth-header.blade.php b/resources/views/components/auth-header.blade.php index f8f0cb7..51d9ad7 100644 --- a/resources/views/components/auth-header.blade.php +++ b/resources/views/components/auth-header.blade.php @@ -4,6 +4,6 @@ ])
-

{{ $title }}

-

{{ $description }}

+

{{ $title }}

+

{{ $description }}

diff --git a/resources/views/components/auth-session-status.blade.php b/resources/views/components/auth-session-status.blade.php index 98e0011..4bcae8c 100644 --- a/resources/views/components/auth-session-status.blade.php +++ b/resources/views/components/auth-session-status.blade.php @@ -3,7 +3,5 @@ ]) @if ($status) -
merge(['class' => 'font-medium text-sm text-green-600']) }}> - {{ $status }} -
+ {{ $status }} @endif diff --git a/resources/views/components/desktop-user-menu.blade.php b/resources/views/components/desktop-user-menu.blade.php deleted file mode 100644 index 7622200..0000000 --- a/resources/views/components/desktop-user-menu.blade.php +++ /dev/null @@ -1 +0,0 @@ -{{-- Desktop user menu - integrated into sidebar layout --}} diff --git a/resources/views/components/placeholder-pattern.blade.php b/resources/views/components/placeholder-pattern.blade.php deleted file mode 100644 index 8a434f0..0000000 --- a/resources/views/components/placeholder-pattern.blade.php +++ /dev/null @@ -1,12 +0,0 @@ -@props([ - 'id' => uniqid(), -]) - - - - - - - - - diff --git a/resources/views/dashboard.blade.php b/resources/views/dashboard.blade.php deleted file mode 100644 index fb52e77..0000000 --- a/resources/views/dashboard.blade.php +++ /dev/null @@ -1,18 +0,0 @@ - -
-
-
- -
-
- -
-
- -
-
-
- -
-
-
diff --git a/resources/views/layouts/app.blade.php b/resources/views/layouts/app.blade.php index f2e85e6..675c514 100644 --- a/resources/views/layouts/app.blade.php +++ b/resources/views/layouts/app.blade.php @@ -1,3 +1,15 @@ - - {{ $slot }} - + + + + @include('partials.head') + + + @include('partials.app-bar') + +
+ {{ $slot }} +
+ + + + diff --git a/resources/views/layouts/app/header.blade.php b/resources/views/layouts/app/header.blade.php deleted file mode 100644 index 919020a..0000000 --- a/resources/views/layouts/app/header.blade.php +++ /dev/null @@ -1,4 +0,0 @@ -{{-- Header layout not used - redirects to sidebar layout --}} - - {{ $slot }} - diff --git a/resources/views/layouts/app/sidebar.blade.php b/resources/views/layouts/app/sidebar.blade.php deleted file mode 100644 index c11123d..0000000 --- a/resources/views/layouts/app/sidebar.blade.php +++ /dev/null @@ -1,42 +0,0 @@ - - - - - @include('partials.head') - - - - {{-- MAIN CONTENT --}} -
- {{ $slot }} -
- - {{-- FOOTER NAV --}} - - - {{-- Toast --}} - - - diff --git a/resources/views/layouts/auth.blade.php b/resources/views/layouts/auth.blade.php index 7150091..15c3406 100644 --- a/resources/views/layouts/auth.blade.php +++ b/resources/views/layouts/auth.blade.php @@ -1,3 +1,19 @@ - - {{ $slot }} - + + + + @include('partials.head') + + + @include('partials.app-bar') + +
+
+
+ {{ $slot }} +
+
+
+ + + + diff --git a/resources/views/layouts/auth/card.blade.php b/resources/views/layouts/auth/card.blade.php deleted file mode 100644 index f3582f0..0000000 --- a/resources/views/layouts/auth/card.blade.php +++ /dev/null @@ -1,4 +0,0 @@ -{{-- Card auth layout - delegates to simple layout --}} - - {{ $slot }} - diff --git a/resources/views/layouts/auth/simple.blade.php b/resources/views/layouts/auth/simple.blade.php deleted file mode 100644 index 9ad4a14..0000000 --- a/resources/views/layouts/auth/simple.blade.php +++ /dev/null @@ -1,27 +0,0 @@ - - - - - @include('partials.head') - @livewireStyles - - - - @livewireScripts - - diff --git a/resources/views/layouts/auth/split.blade.php b/resources/views/layouts/auth/split.blade.php deleted file mode 100644 index 40e7ac0..0000000 --- a/resources/views/layouts/auth/split.blade.php +++ /dev/null @@ -1,4 +0,0 @@ -{{-- Split auth layout - delegates to simple layout --}} - - {{ $slot }} - diff --git a/resources/views/livewire/admin/admin-dashboard.blade.php b/resources/views/livewire/admin/admin-dashboard.blade.php index 0052035..e0094b9 100644 --- a/resources/views/livewire/admin/admin-dashboard.blade.php +++ b/resources/views/livewire/admin/admin-dashboard.blade.php @@ -1,70 +1,69 @@
-

{{ __('Admin Dashboard') }}

+

{{ __('Admin Dashboard') }}

- {{-- Stats --}} -
-
-
-

{{ __('Total Shares') }}

-

{{ $totalShares }}

-
-
-
-
-

{{ __('Active Shares') }}

-

{{ $activeShares }}

-
-
-
-
-

{{ __('Total Files') }}

-

{{ $totalFiles }}

-
-
-
-
-

{{ __('Disk Usage') }}

-

{{ Number::fileSize($usedSpace) }}

- -

{{ Number::fileSize($usedSpace) }} / {{ Number::fileSize($maxQuota) }}

-
-
+
+ + + + + +
- {{-- Shares Table --}} - - - @scope('cell_total_size', $share) - {{ Number::fileSize($share->total_size) }} - @endscope + +
+ + + + {{ __('Token') }} + {{ __('Files') }} + {{ __('Size') }} + {{ __('Downloads') }} + {{ __('Expires') }} + {{ __('Created') }} + {{ __('Actions') }} + + + + @forelse ($shares as $share) + + {{ $share->token }} + {{ $share->files_count }} + {{ Number::fileSize($share->total_size) }} + {{ $share->download_count }} + + @if ($share->expires_at) + $share->isExpired()])>{{ $share->expires_at->diffForHumans() }} + @else + {{ __('Never') }} + @endif + + {{ $share->created_at->diffForHumans() }} + + + + + + @empty + + + + + + @endforelse + + +
- @scope('cell_expires_at', $share) - @if ($share->expires_at) - - {{ $share->expires_at->diffForHumans() }} - - @else - {{ __('Never') }} - @endif - @endscope - - @scope('cell_created_at', $share) - {{ $share->created_at->diffForHumans() }} - @endscope - - @scope('actions', $share) -
- - - - -
- @endscope -
+
{{ $shares->links() }}
+ + + {{ __('Are you sure you want to delete this share?') }} + + + + + +
diff --git a/resources/views/livewire/admin/admin-settings.blade.php b/resources/views/livewire/admin/admin-settings.blade.php index 2627b6a..fa5833d 100644 --- a/resources/views/livewire/admin/admin-settings.blade.php +++ b/resources/views/livewire/admin/admin-settings.blade.php @@ -1,100 +1,66 @@ -
-

{{ __('System Settings') }}

+
+

{{ __('System Settings') }}

- @if (session('message')) -
- - {{ session('message') }} -
- @endif +
+ +
+ - - -
- - - - -
- + +
@if ($currentLogo) -
- {{ __('Site Logo') }} - +
+ {{ __('Site Logo') }} +
@endif - + @if ($siteLogo && is_object($siteLogo)) -
- @if (str_contains($siteLogo->getMimeType(), 'svg')) -

{{ __('SVG selected: :name', ['name' => $siteLogo->getClientOriginalName()]) }}

- @else -

{{ __('Preview:') }}

- {{ __('Logo preview') }} - @endif -
- @endif - - @error('siteLogo') -

{{ $message }}

- @enderror - -

{{ __('Max 2MB. Recommended: PNG or SVG.') }}

-
-
- - - -
-
- - - @if ($hasSystemPassword) -
- -
+ @if (str_contains($siteLogo->getMimeType(), 'svg')) +

{{ __('SVG selected: :name', ['name' => $siteLogo->getClientOriginalName()]) }}

+ @else +
+

{{ __('Preview:') }}

+ {{ __('Logo preview') }} +
+ @endif @endif
- -
+ +
+ + + @if ($hasSystemPassword) +
+ +
+ @endif +
+
+ + +
- + - +
- -
- -
+ + - + + + + {{ __('The upload and download pages show the default mark again.') }} + + + + + + + + + {{ __('Anyone who can reach the upload page can upload files again.') }} + + + + + +
diff --git a/resources/views/livewire/file-uploader.blade.php b/resources/views/livewire/file-uploader.blade.php index 59e1c1c..db23d41 100644 --- a/resources/views/livewire/file-uploader.blade.php +++ b/resources/views/livewire/file-uploader.blade.php @@ -1,21 +1,16 @@ -
-
+
+
@if ($siteLogo) - {{ $siteTitle ?: config('app.name', 'SealShare') }} - @else - + {{ $siteTitle ?: config('app.name', 'SealShare') }} @endif -

{{ $siteTitle ?: config('app.name', 'SealShare') }}

+

{{ $siteTitle ?: config('app.name', 'SealShare') }}

-

{{ $siteDescription ?: __('Share your files safely and securely') }}

+

{{ $siteDescription ?: __('Share your files safely and securely') }}

@if ($isStorageFull) -
- - {{ __('Storage is full. Uploads are temporarily disabled.') }} -
+ @else
- {{-- Drop Zone --}} + {{-- Drop zone: the shape behind the icon turns into a burst while files are over it. --}}
- -

{{ __('Drag & drop files or folders here') }}

-

{{ __('or click to browse') }}

+
+ + + +
-
- {{-- Upload Progress --}} -
- - + +
+
+ + {{ __('Processing files...') }} +
- {{-- Errors --}} @error('files') -
{{ $message }}
+ {{ $message }} @enderror - {{-- File List --}} + {{-- Selected files --}} @if (count($files))
-

{{ __('Selected Files') }} ({{ count($files) }})

-
- @foreach ($files as $index => $file) -
-
- - - {{ $relativePaths[$index] ?? $file->getClientOriginalName() }} - - - ({{ Number::fileSize($file->getSize()) }}) - -
- -
- @endforeach +

{{ __('Selected Files') }} ({{ count($files) }})

+
+ + @foreach ($files as $index => $file) + + + + + + @endforeach +
@endif {{-- Options --}} - -
- + +
+ @if ($usePassword) - + @endif
- {{-- Submit --}} @endif diff --git a/resources/views/livewire/setup-wizard.blade.php b/resources/views/livewire/setup-wizard.blade.php index 07028eb..eaf7ccc 100644 --- a/resources/views/livewire/setup-wizard.blade.php +++ b/resources/views/livewire/setup-wizard.blade.php @@ -4,37 +4,37 @@
- +
diff --git a/resources/views/livewire/share-created.blade.php b/resources/views/livewire/share-created.blade.php index a251eb2..019bfd0 100644 --- a/resources/views/livewire/share-created.blade.php +++ b/resources/views/livewire/share-created.blade.php @@ -1,65 +1,38 @@ -
- -
- {{-- Share URL --}} -
- -
- - -
-
- - {{-- Details --}} -
-
- {{ __('Files') }} -

{{ $share->files->count() }}

-
-
- {{ __('Total Size') }} -

{{ Number::fileSize($share->total_size) }}

-
-
- {{ __('Expires') }} -

{{ $share->expires_at ? $share->expires_at->diffForHumans() : __('Never') }}

-
-
- {{ __('Max Downloads') }} -

{{ $share->max_downloads ?? __('Unlimited') }}

-
-
- - @if ($share->isPasswordProtected()) -
- - {{ __('This share is password protected') }} -
- @endif +
+
+ {{-- The link is ready: a check on an Expressive shape that settles in. --}} +
+ +
- - - - +

{{ __('Share Created!') }}

+

{{ __('Your files are ready to share') }}

+
+ +
+ + +
+ + + + +
+ + @if ($share->isPasswordProtected()) + + @endif + +
+ +
+
diff --git a/resources/views/livewire/share-download.blade.php b/resources/views/livewire/share-download.blade.php index 59940f2..5f5c524 100644 --- a/resources/views/livewire/share-download.blade.php +++ b/resources/views/livewire/share-download.blade.php @@ -1,67 +1,61 @@ -
-
+{{-- The page a recipient opens. No anchored components (menus, tooltips) on it: it has to work on + iOS before Safari 18.4, which cannot position them. --}} + +
+
@if ($siteLogo) - {{ $siteTitle ?: config('app.name', 'SealShare') }} - @else - + {{ $siteTitle ?: config('app.name', 'SealShare') }} @endif -

{{ $siteTitle ?: config('app.name', 'SealShare') }}

+

{{ $siteTitle ?: config('app.name', 'SealShare') }}

-

{{ $siteDescription ?: __('Share your files safely and securely') }}

+

{{ $siteDescription ?: __('Share your files safely and securely') }}

@if (! $authenticated) - {{-- Password form --}}
- + - + @else - {{-- File list --}} - -
+ + @foreach ($share->files as $file) -
-
- - {{ $file->relative_path ?: $file->original_name }} - ({{ Number::fileSize($file->file_size) }}) -
- - - -
+ + + + + @endforeach -
+ @if ($share->expires_at) -

+

{{ __('Expires') }}: {{ $share->expires_at->diffForHumans() }}

@endif @if ($share->files->count() > 1) - - - {{ __('Download All as ZIP') }} - + @else - - - {{ __('Download') }} - + @endif
diff --git a/resources/views/livewire/system-password-prompt.blade.php b/resources/views/livewire/system-password-prompt.blade.php index ef8e738..7fbf87e 100644 --- a/resources/views/livewire/system-password-prompt.blade.php +++ b/resources/views/livewire/system-password-prompt.blade.php @@ -4,11 +4,11 @@
- +
diff --git a/resources/views/pages/auth/confirm-password.blade.php b/resources/views/pages/auth/confirm-password.blade.php index d27d193..221e4b7 100644 --- a/resources/views/pages/auth/confirm-password.blade.php +++ b/resources/views/pages/auth/confirm-password.blade.php @@ -1,24 +1,22 @@ - -
- + + + + +
+ @csrf + + - - - - @csrf - - - - - -
+ +
diff --git a/resources/views/pages/auth/forgot-password.blade.php b/resources/views/pages/auth/forgot-password.blade.php index f6fec84..ffd9ecb 100644 --- a/resources/views/pages/auth/forgot-password.blade.php +++ b/resources/views/pages/auth/forgot-password.blade.php @@ -1,30 +1,27 @@ - -
- + + - - + -
- @csrf + + @csrf - - + - - + + -
- {{ __('Or, return to') }} - {{ __('log in') }} -
-
+

+ {{ __('Or, return to') }} + {{ __('log in') }} +

diff --git a/resources/views/pages/auth/login.blade.php b/resources/views/pages/auth/login.blade.php index f2b3808..bcc5b68 100644 --- a/resources/views/pages/auth/login.blade.php +++ b/resources/views/pages/auth/login.blade.php @@ -1,59 +1,40 @@ - -
- + + - - + -
- @csrf + + @csrf - - + +
+ - -
- + @if (Route::has('password.request')) + + {{ __('Forgot your password?') }} + + @endif +
- @if (Route::has('password.request')) - - {{ __('Forgot your password?') }} - - @endif -
+ - - - -
- -
- - - @if (Route::has('register')) -
- {{ __('Don\'t have an account?') }} - {{ __('Sign up') }} -
- @endif -
+ +
diff --git a/resources/views/pages/auth/register.blade.php b/resources/views/pages/auth/register.blade.php deleted file mode 100644 index e559bed..0000000 --- a/resources/views/pages/auth/register.blade.php +++ /dev/null @@ -1,63 +0,0 @@ - -
- - - - - -
- @csrf - - - - - - - - - - - - -
- -
- - -
- {{ __('Already have an account?') }} - {{ __('Log in') }} -
-
-
diff --git a/resources/views/pages/auth/reset-password.blade.php b/resources/views/pages/auth/reset-password.blade.php index 53d69a4..fcc5ae3 100644 --- a/resources/views/pages/auth/reset-password.blade.php +++ b/resources/views/pages/auth/reset-password.blade.php @@ -1,47 +1,36 @@ - -
- + + - - + -
- @csrf - - + + @csrf + - - + - - + - - + -
- -
- -
+ +
diff --git a/resources/views/pages/auth/two-factor-challenge.blade.php b/resources/views/pages/auth/two-factor-challenge.blade.php index c209893..de98e8d 100644 --- a/resources/views/pages/auth/two-factor-challenge.blade.php +++ b/resources/views/pages/auth/two-factor-challenge.blade.php @@ -1,90 +1,65 @@ - -
-
+
-
- -
- -
- -
- -
- @csrf - -
-
-
- -
-
- -
-
- -
- - @error('recovery_code') -

{{ $message }}

- @enderror -
- - -
- -
- {{ __('or you can') }} -
- {{ __('login using a recovery code') }} - {{ __('login using an authentication code') }} -
-
-
+ }); + }, + }" + > +
+
+ +
+ +
+ +
+ @csrf + +
+ +
+ +
+ +
+ + + + +

+ {{ __('or you can') }} + + +

diff --git a/resources/views/pages/auth/verify-email.blade.php b/resources/views/pages/auth/verify-email.blade.php index 121f434..53fa872 100644 --- a/resources/views/pages/auth/verify-email.blade.php +++ b/resources/views/pages/auth/verify-email.blade.php @@ -1,25 +1,24 @@ - -
-

- {{ __('Please verify your email address by clicking on the link we just emailed to you.') }} -

+ + - @if (session('status') == 'verification-link-sent') -

- {{ __('A new verification link has been sent to the email address you provided during registration.') }} -

- @endif + @if (session('status') == 'verification-link-sent') + + {{ __('A new verification link has been sent to the email address you provided during registration.') }} + + @endif -
-
- @csrf - - +
+
+ @csrf + + -
- @csrf - - -
+
+ @csrf + +
diff --git a/resources/views/pages/settings/layout.blade.php b/resources/views/pages/settings/layout.blade.php index 68d4e85..684d4b3 100644 --- a/resources/views/pages/settings/layout.blade.php +++ b/resources/views/pages/settings/layout.blade.php @@ -1,22 +1,24 @@ -
-
- - - - @if (Laravel\Fortify\Features::canManageTwoFactorAuthentication()) - - @endif - - -
+@php + $items = [ + ['title' => __('Profile'), 'icon' => 'person', 'url' => route('profile.edit'), 'active' => request()->routeIs('profile.edit')], + ['title' => __('Password'), 'icon' => 'password', 'url' => route('user-password.edit'), 'active' => request()->routeIs('user-password.edit')], + ]; -
+ if (Laravel\Fortify\Features::canManageTwoFactorAuthentication()) { + $items[] = ['title' => __('Two-Factor Auth'), 'icon' => 'shield_lock', 'url' => route('two-factor.show'), 'active' => request()->routeIs('two-factor.show')]; + } -
-

{{ $heading ?? '' }}

-

{{ $subheading ?? '' }}

+ $items[] = ['title' => __('Appearance'), 'icon' => 'contrast', 'url' => route('appearance.edit'), 'active' => request()->routeIs('appearance.edit')]; +@endphp -
+
+ + +
+

{{ $heading ?? '' }}

+

{{ $subheading ?? '' }}

+ +
{{ $slot }}
diff --git a/resources/views/pages/settings/two-factor/⚡recovery-codes.blade.php b/resources/views/pages/settings/two-factor/⚡recovery-codes.blade.php index 9e9f8a5..19ecb63 100644 --- a/resources/views/pages/settings/two-factor/⚡recovery-codes.blade.php +++ b/resources/views/pages/settings/two-factor/⚡recovery-codes.blade.php @@ -45,83 +45,48 @@ new class extends Component { } }; ?> -
-
-
- -

{{ __('2FA Recovery Codes') }}

+ +
+
+
+ +

{{ __('2FA Recovery Codes') }}

+
+

+ {{ __('Recovery codes let you regain access if you lose your 2FA device. Store them in a secure password manager.') }} +

-

- {{ __('Recovery codes let you regain access if you lose your 2FA device. Store them in a secure password manager.') }} -

-
-
-
- - - +
+ + + + + + @if (filled($recoveryCodes)) - + + + @endif
-
-
- @error('recoveryCodes') -
{{ $message }}
- @enderror +
+ @error('recoveryCodes') + {{ $message }} + @enderror - @if (filled($recoveryCodes)) -
- @foreach($recoveryCodes as $code) -
- {{ $code }} -
- @endforeach -
-

- {{ __('Each recovery code can be used once to access your account and will be removed after use. If you need more, click Regenerate Codes above.') }} -

- @endif -
+ @if (filled($recoveryCodes)) +
+ @foreach ($recoveryCodes as $code) +
{{ $code }}
+ @endforeach +
+

+ {{ __('Each recovery code can be used once to access your account and will be removed after use. If you need more, click Regenerate Codes above.') }} +

+ @endif
-
+ diff --git a/resources/views/pages/settings/⚡appearance.blade.php b/resources/views/pages/settings/⚡appearance.blade.php index 9964cb3..8e696f0 100644 --- a/resources/views/pages/settings/⚡appearance.blade.php +++ b/resources/views/pages/settings/⚡appearance.blade.php @@ -10,6 +10,6 @@ new class extends Component { @include('partials.settings-heading') - + diff --git a/resources/views/pages/settings/⚡delete-user-form.blade.php b/resources/views/pages/settings/⚡delete-user-form.blade.php index b06471c..4d11b99 100644 --- a/resources/views/pages/settings/⚡delete-user-form.blade.php +++ b/resources/views/pages/settings/⚡delete-user-form.blade.php @@ -26,31 +26,30 @@ new class extends Component { } }; ?> -
-
-

{{ __('Delete account') }}

-

{{ __('Delete your account and all of its resources') }}

+
+ + +
+

{{ __('Delete account') }}

+

{{ __('Delete your account and all of its resources') }}

- +
+ +
- -

+ +

{{ __('Once your account is deleted, all of its resources and data will be permanently deleted. Please enter your password to confirm you would like to permanently delete your account.') }}

-
- - - - - - + + + + + + +
diff --git a/resources/views/pages/settings/⚡password.blade.php b/resources/views/pages/settings/⚡password.blade.php index 0e4cf1a..660d399 100644 --- a/resources/views/pages/settings/⚡password.blade.php +++ b/resources/views/pages/settings/⚡password.blade.php @@ -5,9 +5,11 @@ use Illuminate\Support\Facades\Auth; use Illuminate\Validation\Rules\Password; use Illuminate\Validation\ValidationException; use Livewire\Component; +use NoNameWeb\LivewireMaterial\Concerns\Toasts; new class extends Component { use PasswordValidationRules; + use Toasts; public string $current_password = ''; public string $password = ''; @@ -36,6 +38,8 @@ new class extends Component { $this->reset('current_password', 'password', 'password_confirmation'); $this->dispatch('password-updated'); + + $this->success(__('Saved.')); } }; ?> @@ -43,34 +47,13 @@ new class extends Component { @include('partials.settings-heading') -
- - - + + + + -
-
- -
- - - {{ __('Saved.') }} - +
+
diff --git a/resources/views/pages/settings/⚡profile.blade.php b/resources/views/pages/settings/⚡profile.blade.php index 30d6f2e..f772880 100644 --- a/resources/views/pages/settings/⚡profile.blade.php +++ b/resources/views/pages/settings/⚡profile.blade.php @@ -8,9 +8,11 @@ use Illuminate\Support\Facades\Session; use Illuminate\Validation\Rule; use Livewire\Attributes\Computed; use Livewire\Component; +use NoNameWeb\LivewireMaterial\Concerns\Toasts; new class extends Component { use ProfileValidationRules; + use Toasts; public string $name = ''; public string $email = ''; @@ -42,6 +44,8 @@ new class extends Component { $user->save(); $this->dispatch('profile-updated', name: $user->name); + + $this->success(__('Saved.')); } /** @@ -52,7 +56,7 @@ new class extends Component { $user = Auth::user(); if ($user->hasVerifiedEmail()) { - $this->redirectIntended(default: route('dashboard', absolute: false)); + $this->redirectIntended(default: route('admin.dashboard', absolute: false)); return; } @@ -80,39 +84,29 @@ new class extends Component { @include('partials.settings-heading') -
- + + -
- +
+ @if ($this->hasUnverifiedEmail) -
-

- {{ __('Your email address is unverified.') }} +

+ {{ __('Your email address is unverified.') }} - - {{ __('Click here to re-send the verification email.') }} - -

+ +

- @if (session('status') === 'verification-link-sent') -

- {{ __('A new verification link has been sent to your email address.') }} -

- @endif -
+ @if (session('status') === 'verification-link-sent') + {{ __('A new verification link has been sent to your email address.') }} + @endif @endif
-
-
- -
- - - {{ __('Saved.') }} - +
+
diff --git a/resources/views/pages/settings/⚡two-factor.blade.php b/resources/views/pages/settings/⚡two-factor.blade.php index be85574..0f8358d 100644 --- a/resources/views/pages/settings/⚡two-factor.blade.php +++ b/resources/views/pages/settings/⚡two-factor.blade.php @@ -184,142 +184,81 @@ new class extends Component { :heading="__('Two Factor Authentication')" :subheading="__('Manage your two-factor authentication settings')" > -
+
@if ($twoFactorEnabled) -
-
- {{ __('Enabled') }} -
+
+ -

+

{{ __('With two-factor authentication enabled, you will be prompted for a secure, random pin during login, which you can retrieve from the TOTP-supported application on your phone.') }}

+
- + -
- -
+
+
@else -
-
- {{ __('Disabled') }} -
+
+ -

+

{{ __('When you enable two-factor authentication, you will be prompted for a secure pin during login. This pin can be retrieved from a TOTP-supported application on your phone.') }}

- +
@endif
- -

{{ $this->modalConfig['description'] }}

- + @if ($showVerificationStep) -
-
- -
- - - - - +
+
+ + + + + @else @error('setupData') -
{{ $message }}
+ {{ $message }} @enderror -
-
+
+ {{-- The QR code keeps a white ground in both themes: scanners read dark on light. --}} +
@empty($qrCodeSvg) -
- -
+ @else -
- {!! $qrCodeSvg !!} -
+ {!! $qrCodeSvg !!} @endempty
-
-
-
- - {{ __('or, enter the code manually') }} - -
+
+

{{ __('or, enter the code manually') }}

-
-
- - -
-
+
diff --git a/resources/views/partials/app-bar.blade.php b/resources/views/partials/app-bar.blade.php new file mode 100644 index 0000000..ebcf04f --- /dev/null +++ b/resources/views/partials/app-bar.blade.php @@ -0,0 +1,47 @@ +{{-- The one top app bar on every page: the site's logo and title, then the account menu for a + signed-in user, or the theme toggle and a way to sign in for everyone else. --}} + +@php + $siteTitle = \App\Models\Setting::get('site_title') ?: config('app.name', 'SealShare'); + $siteLogo = \App\Models\Setting::get('site_logo'); +@endphp + + + + + @if ($siteLogo) + + @else + + @endif + {{ $siteTitle }} + + + + + @auth + + + + @if (auth()->user()->is_admin) + + + @endif + + + +
+ @csrf + + +
+
+
+ @else + + @if (Route::has('login') && ! request()->routeIs('login')) + + @endif + @endauth +
+
diff --git a/resources/views/partials/head.blade.php b/resources/views/partials/head.blade.php index de19ac7..0e7a4b1 100644 --- a/resources/views/partials/head.blade.php +++ b/resources/views/partials/head.blade.php @@ -1,5 +1,5 @@ - + {{ $title ?? (\App\Models\Setting::get('site_title') ?: config('app.name')) }} @@ -7,7 +7,6 @@ - - + @vite(['resources/css/app.css', 'resources/js/app.js']) diff --git a/resources/views/partials/settings-heading.blade.php b/resources/views/partials/settings-heading.blade.php index 5090a47..8f7d30f 100644 --- a/resources/views/partials/settings-heading.blade.php +++ b/resources/views/partials/settings-heading.blade.php @@ -1,5 +1,4 @@ -
-

{{ __('Settings') }}

-

{{ __('Manage your profile and account settings') }}

-
+
+

{{ __('Settings') }}

+

{{ __('Manage your profile and account settings') }}

diff --git a/resources/views/welcome.blade.php b/resources/views/welcome.blade.php deleted file mode 100644 index 7bd3979..0000000 --- a/resources/views/welcome.blade.php +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - {{ \App\Models\Setting::get('site_title') ?: config('app.name', 'SealShare') }} - @include('partials.head') - - -
-

{{ \App\Models\Setting::get('site_title') ?: config('app.name', 'SealShare') }}

-

{{ __('Secure file sharing made simple.') }}

- {{ __('Upload Files') }} -
- - diff --git a/routes/web.php b/routes/web.php index 6bb2a5c..bbe58e4 100644 --- a/routes/web.php +++ b/routes/web.php @@ -32,8 +32,4 @@ Route::middleware(['auth', 'admin'])->prefix('admin')->group(function () { Route::livewire('settings', AdminSettings::class)->name('admin.settings'); }); -Route::view('dashboard', 'dashboard') - ->middleware(['auth', 'verified']) - ->name('dashboard'); - require __DIR__.'/settings.php'; diff --git a/tests/Feature/Admin/AdminDashboardTest.php b/tests/Feature/Admin/AdminDashboardTest.php index e0585d5..1c07199 100644 --- a/tests/Feature/Admin/AdminDashboardTest.php +++ b/tests/Feature/Admin/AdminDashboardTest.php @@ -54,7 +54,9 @@ test('admin can delete share', function () { Livewire::actingAs($admin) ->test(AdminDashboard::class) - ->call('deleteShare', $shareId); + ->set('deletingShareId', $shareId) + ->call('deleteShare', $shareId) + ->assertSet('deletingShareId', null); expect(Share::query()->find($shareId))->toBeNull(); }); @@ -69,3 +71,19 @@ test('admin dashboard shows shares table', function () { $response->assertOk(); $response->assertSee('testtoken12345678'); }); + +test('the shares table sorts only by its own columns', function () { + $admin = User::query()->where('is_admin', true)->first(); + + Share::factory()->create(['token' => 'aaaaaaaaaaaaaaaa', 'download_count' => 9]); + Share::factory()->create(['token' => 'zzzzzzzzzzzzzzzz', 'download_count' => 1]); + + Livewire::actingAs($admin) + ->test(AdminDashboard::class) + ->set('sortBy', ['column' => 'download_count', 'direction' => 'asc']) + ->assertSeeInOrder(['zzzzzzzzzzzzzzzz', 'aaaaaaaaaaaaaaaa']) + ->set('sortBy', ['column' => 'token; drop table shares', 'direction' => 'sideways']) + ->assertOk(); + + expect(Share::query()->count())->toBe(2); +}); diff --git a/tests/Feature/Admin/AdminSettingsTest.php b/tests/Feature/Admin/AdminSettingsTest.php index 9d644fc..1b005d1 100644 --- a/tests/Feature/Admin/AdminSettingsTest.php +++ b/tests/Feature/Admin/AdminSettingsTest.php @@ -3,7 +3,9 @@ use App\Livewire\Admin\AdminSettings; use App\Models\Setting; use App\Models\User; +use Illuminate\Http\UploadedFile; use Illuminate\Support\Facades\Hash; +use Illuminate\Support\Facades\Storage; use Livewire\Livewire; test('admin settings requires authentication', function () { @@ -41,7 +43,8 @@ test('admin can save settings', function () { ->set('maxSizePerShare', 5) ->set('defaultExpiration', '7d') ->call('saveSettings') - ->assertHasNoErrors(); + ->assertHasNoErrors() + ->assertDispatched('toast', type: 'success', title: 'Settings saved successfully.'); expect(Setting::get('max_file_size'))->toBe((string) (min(200, $phpMaxMb) * 1024 * 1024)); expect(Setting::get('max_storage_quota'))->toBe((string) (50 * 1024 * 1024 * 1024)); @@ -73,8 +76,11 @@ test('admin can clear system password', function () { Livewire::actingAs($admin) ->test(AdminSettings::class) + ->set('confirmingPasswordRemoval', true) ->call('clearSystemPassword') - ->assertHasNoErrors(); + ->assertHasNoErrors() + ->assertSet('confirmingPasswordRemoval', false) + ->assertDispatched('toast', type: 'success', title: 'System password cleared.'); expect(Setting::get('system_password'))->toBeNull(); }); @@ -103,3 +109,22 @@ test('settings validation rejects invalid values', function () { ->call('saveSettings') ->assertHasErrors(['maxFileSize', 'maxStorageQuota']); }); + +test('admin can remove the logo through its dialog', function () { + Storage::fake('public'); + + $admin = User::query()->where('is_admin', true)->first(); + $path = UploadedFile::fake()->image('logo.png')->store('branding', 'public'); + Setting::set('site_logo', $path); + + Livewire::actingAs($admin) + ->test(AdminSettings::class) + ->assertSeeHtml('data-test="remove-logo"') + ->set('confirmingLogoRemoval', true) + ->call('removeLogo') + ->assertSet('confirmingLogoRemoval', false) + ->assertDispatched('toast', type: 'success', title: 'Logo removed.'); + + expect(Setting::get('site_logo'))->toBeNull(); + Storage::disk('public')->assertMissing($path); +}); diff --git a/tests/Feature/Auth/AuthenticationTest.php b/tests/Feature/Auth/AuthenticationTest.php index 868b32f..0e1a511 100644 --- a/tests/Feature/Auth/AuthenticationTest.php +++ b/tests/Feature/Auth/AuthenticationTest.php @@ -19,7 +19,7 @@ test('users can authenticate using the login screen', function () { $response ->assertSessionHasNoErrors() - ->assertRedirect(route('dashboard', absolute: false)); + ->assertRedirect(route('admin.dashboard', absolute: false)); $this->assertAuthenticated(); }); diff --git a/tests/Feature/Auth/EmailVerificationTest.php b/tests/Feature/Auth/EmailVerificationTest.php index ef5ce75..fa034ac 100644 --- a/tests/Feature/Auth/EmailVerificationTest.php +++ b/tests/Feature/Auth/EmailVerificationTest.php @@ -29,7 +29,7 @@ test('email can be verified', function () { Event::assertDispatched(Verified::class); expect($user->fresh()->hasVerifiedEmail())->toBeTrue(); - $response->assertRedirect(route('dashboard', absolute: false).'?verified=1'); + $response->assertRedirect(route('admin.dashboard', absolute: false).'?verified=1'); }); test('email is not verified with invalid hash', function () { @@ -60,7 +60,7 @@ test('already verified user visiting verification link is redirected without fir ); $this->actingAs($user)->get($verificationUrl) - ->assertRedirect(route('dashboard', absolute: false).'?verified=1'); + ->assertRedirect(route('admin.dashboard', absolute: false).'?verified=1'); expect($user->fresh()->hasVerifiedEmail())->toBeTrue(); Event::assertNotDispatched(Verified::class); diff --git a/tests/Feature/DashboardTest.php b/tests/Feature/DashboardTest.php index 95c6a8b..2a77c66 100644 --- a/tests/Feature/DashboardTest.php +++ b/tests/Feature/DashboardTest.php @@ -2,15 +2,17 @@ use App\Models\User; -test('guests are redirected to the login page', function () { - $response = $this->get(route('dashboard')); - $response->assertRedirect(route('login')); +test('the starter placeholder dashboard is gone', function () { + $this->actingAs(User::factory()->admin()->create()) + ->get('/dashboard') + ->assertNotFound(); }); -test('authenticated users can visit the dashboard', function () { - $user = User::factory()->create(); - $this->actingAs($user); +test('a signed-in admin lands on the admin dashboard', function () { + $admin = User::factory()->admin()->create(); - $response = $this->get(route('dashboard')); - $response->assertOk(); + $this->post(route('login.store'), ['email' => $admin->email, 'password' => 'password']) + ->assertRedirect(route('admin.dashboard', absolute: false)); + + $this->get(route('admin.dashboard'))->assertOk(); }); diff --git a/tests/Feature/DesignLanguageTest.php b/tests/Feature/DesignLanguageTest.php new file mode 100644 index 0000000..9b2c87d --- /dev/null +++ b/tests/Feature/DesignLanguageTest.php @@ -0,0 +1,19 @@ +violations())->toBe([]); +}); + +test('nothing of maryUI or daisyUI is left behind', function () { + $leftovers = collect(['resources/views', 'resources/js', 'resources/css', 'app']) + ->flatMap(fn (string $path) => File::allFiles(base_path($path))) + ->filter(fn (SplFileInfo $file): bool => preg_match('/\b(base-content|bg-base-\d|btn(-[a-z]+)?|x-mary-|daisyui|robsontenorio)\b/', $file->getContents()) === 1) + ->map(fn (SplFileInfo $file): string => str_replace(base_path().'/', '', $file->getPathname())) + ->values() + ->all(); + + expect($leftovers)->toBe([]); +}); diff --git a/tests/Feature/Settings/PasswordUpdateTest.php b/tests/Feature/Settings/PasswordUpdateTest.php index 0baec7d..029fe20 100644 --- a/tests/Feature/Settings/PasswordUpdateTest.php +++ b/tests/Feature/Settings/PasswordUpdateTest.php @@ -17,7 +17,8 @@ test('password can be updated', function () { ->set('password_confirmation', 'new-password') ->call('updatePassword'); - $response->assertHasNoErrors(); + $response->assertHasNoErrors() + ->assertDispatched('toast', type: 'success', title: 'Saved.'); expect(Hash::check('new-password', $user->refresh()->password))->toBeTrue(); }); diff --git a/tests/Feature/Settings/ProfileUpdateTest.php b/tests/Feature/Settings/ProfileUpdateTest.php index 30624ff..70f3788 100644 --- a/tests/Feature/Settings/ProfileUpdateTest.php +++ b/tests/Feature/Settings/ProfileUpdateTest.php @@ -19,7 +19,9 @@ test('profile information can be updated', function () { ->set('email', 'test@example.com') ->call('updateProfileInformation'); - $response->assertHasNoErrors(); + $response->assertHasNoErrors() + ->assertDispatched('profile-updated') + ->assertDispatched('toast', type: 'success', title: 'Saved.'); $user->refresh();