From da606b1d8892a6d527f04d7c5b9a8b2ff1263602 Mon Sep 17 00:00:00 2001 From: "Ronald A. Richardson" Date: Fri, 23 Oct 2026 11:22:01 +0800 Subject: [PATCH 1/9] Rebuild the registry website with Inertia (React) from the Waypoints design The publisher website and staff review console, served by the Laravel app with Inertia v3, React 19, Tailwind v4 and Vite, replacing the Ember prototype. - Page controllers (app/Http/Controllers/Web) read through the /v1 API controllers in process, so props have the API's JSON shapes; pages write to /v1 with the session cookie and XSRF header. Website routes use auth:web; unknown paths render the website 404. - Public: Discover (search, intents, filters, console compatibility), extension pages with install steps and a version check, publisher profiles. - Account: sign in, create account, email code, profile and password, organizations and members, personal tokens shown once. - Studio: inbox, new listing, the listing editor with a live readiness rail, uploads, live release checks, submit / withdraw / rejected states, history, insights placeholder. - Review console: queue with tabs, filters and J/K, the R1 to R6 decision rail, publish, suspend and lift, the To publish page. - Light and dark themes, phone layouts, error pages through Inertia. - Vitest component and page-logic tests, Pest page tests, a Playwright smoke test with its own seeded server (scripts/e2e-server.sh), CI jobs, and a Docker stage for the assets. --- .dockerignore | 6 + .github/workflows/ci.yml | 80 + .gitignore | 4 + Dockerfile | 13 + README.md | 63 +- .../Controllers/Web/AccountPageController.php | 78 + .../Controllers/Web/CatalogPageController.php | 117 + .../Controllers/Web/ReviewPageController.php | 168 + .../Controllers/Web/StudioPageController.php | 135 + app/Http/Controllers/Web/WebController.php | 64 + app/Http/Controllers/WebsiteController.php | 46 - app/Http/Middleware/HandleInertiaRequests.php | 59 + bootstrap/app.php | 30 + composer.json | 1 + composer.lock | 74 +- e2e/smoke.spec.js | 114 + eslint.config.js | 26 + package-lock.json | 4301 +++++++++++++++++ package.json | 37 + playwright.config.js | 27 + resources/css/app.css | 67 + resources/css/registry.css | 333 ++ resources/js/app.jsx | 17 + resources/js/components/Checks.jsx | 80 + resources/js/components/CommandPalette.jsx | 192 + resources/js/components/CopyCommand.jsx | 36 + resources/js/components/Dialog.jsx | 82 + resources/js/components/Dialog.test.jsx | 33 + resources/js/components/ExtensionCard.jsx | 38 + resources/js/components/ExtensionIcon.jsx | 11 + resources/js/components/Icon.jsx | 82 + resources/js/components/Route.jsx | 65 + resources/js/components/Route.test.jsx | 26 + resources/js/components/Status.jsx | 30 + resources/js/components/Timeline.jsx | 75 + resources/js/components/Toasts.jsx | 51 + resources/js/components/Trust.jsx | 29 + resources/js/components/Ui.jsx | 112 + .../js/components/account/AccountHeader.jsx | 44 + resources/js/components/account/AuthShell.jsx | 28 + resources/js/components/account/CodeInput.jsx | 110 + .../js/components/account/CodeInput.test.jsx | 78 + .../js/components/account/NewTokenForm.jsx | 159 + .../js/components/account/TokenOnceDialog.jsx | 159 + .../account/TokenOnceDialog.test.jsx | 181 + resources/js/components/account/account.css | 33 + resources/js/components/account/tokens.js | 53 + .../js/components/review/DecisionRail.jsx | 287 ++ resources/js/components/review/Parts.jsx | 207 + .../js/components/review/ReviewPages.test.jsx | 184 + resources/js/components/review/decision.js | 79 + .../js/components/review/decision.test.js | 72 + resources/js/components/review/queue.js | 100 + resources/js/components/review/queue.test.js | 76 + .../js/components/studio/MediaSection.jsx | 289 ++ .../js/components/studio/OverviewEditor.jsx | 97 + .../js/components/studio/PricingSection.jsx | 114 + .../components/studio/ReleaseActionDialog.jsx | 134 + .../js/components/studio/StudioChrome.jsx | 73 + .../js/components/studio/StudioPages.test.jsx | 153 + resources/js/components/studio/TagsInput.jsx | 77 + resources/js/components/studio/UploadForm.jsx | 215 + .../js/components/studio/UploadForm.test.jsx | 90 + resources/js/components/studio/inbox.js | 86 + resources/js/components/studio/inbox.test.js | 76 + resources/js/components/studio/readiness.js | 87 + .../js/components/studio/readiness.test.js | 98 + resources/js/components/studio/studio.css | 102 + resources/js/layouts/SiteLayout.jsx | 196 + resources/js/lib/api.js | 75 + resources/js/lib/format.js | 118 + resources/js/lib/pages.js | 13 + resources/js/lib/statuses.js | 78 + resources/js/lib/theme.js | 38 + resources/js/pages/Account/Organizations.jsx | 447 ++ resources/js/pages/Account/Profile.jsx | 167 + resources/js/pages/Account/Tokens.jsx | 205 + resources/js/pages/Auth/Register.jsx | 148 + resources/js/pages/Auth/SignIn.jsx | 112 + resources/js/pages/Auth/Verify.jsx | 177 + resources/js/pages/Discover/Index.jsx | 288 ++ resources/js/pages/Error.jsx | 47 + resources/js/pages/Extension/Show.jsx | 443 ++ resources/js/pages/Publisher/Show.jsx | 133 + resources/js/pages/Review/Pending.jsx | 124 + resources/js/pages/Review/Queue.jsx | 190 + resources/js/pages/Review/Release.jsx | 303 ++ resources/js/pages/Studio/Create.jsx | 245 + resources/js/pages/Studio/Editor.jsx | 407 ++ resources/js/pages/Studio/History.jsx | 93 + resources/js/pages/Studio/Index.jsx | 325 ++ resources/js/pages/Studio/Insights.jsx | 63 + resources/js/pages/Studio/Release.jsx | 165 + resources/js/pages/Studio/Releases.jsx | 139 + resources/js/pages/Studio/Review.jsx | 394 ++ resources/js/ssr.jsx | 16 + resources/js/test/setup.js | 5 + resources/views/app.blade.php | 29 + routes/web.php | 56 +- scripts/e2e-server.sh | 44 + tests/Feature/Website/AccountPagesTest.php | 132 + tests/Feature/Website/ReviewPagesTest.php | 139 + tests/Feature/Website/StudioPagesTest.php | 208 + tests/Feature/Website/WebsiteTest.php | 90 +- tests/Helpers/website.php | 29 + tests/Pest.php | 1 + tests/TestCase.php | 12 + vite.config.js | 22 + 108 files changed, 16165 insertions(+), 94 deletions(-) create mode 100644 app/Http/Controllers/Web/AccountPageController.php create mode 100644 app/Http/Controllers/Web/CatalogPageController.php create mode 100644 app/Http/Controllers/Web/ReviewPageController.php create mode 100644 app/Http/Controllers/Web/StudioPageController.php create mode 100644 app/Http/Controllers/Web/WebController.php delete mode 100644 app/Http/Controllers/WebsiteController.php create mode 100644 app/Http/Middleware/HandleInertiaRequests.php create mode 100644 e2e/smoke.spec.js create mode 100644 eslint.config.js create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 playwright.config.js create mode 100644 resources/css/app.css create mode 100644 resources/css/registry.css create mode 100644 resources/js/app.jsx create mode 100644 resources/js/components/Checks.jsx create mode 100644 resources/js/components/CommandPalette.jsx create mode 100644 resources/js/components/CopyCommand.jsx create mode 100644 resources/js/components/Dialog.jsx create mode 100644 resources/js/components/Dialog.test.jsx create mode 100644 resources/js/components/ExtensionCard.jsx create mode 100644 resources/js/components/ExtensionIcon.jsx create mode 100644 resources/js/components/Icon.jsx create mode 100644 resources/js/components/Route.jsx create mode 100644 resources/js/components/Route.test.jsx create mode 100644 resources/js/components/Status.jsx create mode 100644 resources/js/components/Timeline.jsx create mode 100644 resources/js/components/Toasts.jsx create mode 100644 resources/js/components/Trust.jsx create mode 100644 resources/js/components/Ui.jsx create mode 100644 resources/js/components/account/AccountHeader.jsx create mode 100644 resources/js/components/account/AuthShell.jsx create mode 100644 resources/js/components/account/CodeInput.jsx create mode 100644 resources/js/components/account/CodeInput.test.jsx create mode 100644 resources/js/components/account/NewTokenForm.jsx create mode 100644 resources/js/components/account/TokenOnceDialog.jsx create mode 100644 resources/js/components/account/TokenOnceDialog.test.jsx create mode 100644 resources/js/components/account/account.css create mode 100644 resources/js/components/account/tokens.js create mode 100644 resources/js/components/review/DecisionRail.jsx create mode 100644 resources/js/components/review/Parts.jsx create mode 100644 resources/js/components/review/ReviewPages.test.jsx create mode 100644 resources/js/components/review/decision.js create mode 100644 resources/js/components/review/decision.test.js create mode 100644 resources/js/components/review/queue.js create mode 100644 resources/js/components/review/queue.test.js create mode 100644 resources/js/components/studio/MediaSection.jsx create mode 100644 resources/js/components/studio/OverviewEditor.jsx create mode 100644 resources/js/components/studio/PricingSection.jsx create mode 100644 resources/js/components/studio/ReleaseActionDialog.jsx create mode 100644 resources/js/components/studio/StudioChrome.jsx create mode 100644 resources/js/components/studio/StudioPages.test.jsx create mode 100644 resources/js/components/studio/TagsInput.jsx create mode 100644 resources/js/components/studio/UploadForm.jsx create mode 100644 resources/js/components/studio/UploadForm.test.jsx create mode 100644 resources/js/components/studio/inbox.js create mode 100644 resources/js/components/studio/inbox.test.js create mode 100644 resources/js/components/studio/readiness.js create mode 100644 resources/js/components/studio/readiness.test.js create mode 100644 resources/js/components/studio/studio.css create mode 100644 resources/js/layouts/SiteLayout.jsx create mode 100644 resources/js/lib/api.js create mode 100644 resources/js/lib/format.js create mode 100644 resources/js/lib/pages.js create mode 100644 resources/js/lib/statuses.js create mode 100644 resources/js/lib/theme.js create mode 100644 resources/js/pages/Account/Organizations.jsx create mode 100644 resources/js/pages/Account/Profile.jsx create mode 100644 resources/js/pages/Account/Tokens.jsx create mode 100644 resources/js/pages/Auth/Register.jsx create mode 100644 resources/js/pages/Auth/SignIn.jsx create mode 100644 resources/js/pages/Auth/Verify.jsx create mode 100644 resources/js/pages/Discover/Index.jsx create mode 100644 resources/js/pages/Error.jsx create mode 100644 resources/js/pages/Extension/Show.jsx create mode 100644 resources/js/pages/Publisher/Show.jsx create mode 100644 resources/js/pages/Review/Pending.jsx create mode 100644 resources/js/pages/Review/Queue.jsx create mode 100644 resources/js/pages/Review/Release.jsx create mode 100644 resources/js/pages/Studio/Create.jsx create mode 100644 resources/js/pages/Studio/Editor.jsx create mode 100644 resources/js/pages/Studio/History.jsx create mode 100644 resources/js/pages/Studio/Index.jsx create mode 100644 resources/js/pages/Studio/Insights.jsx create mode 100644 resources/js/pages/Studio/Release.jsx create mode 100644 resources/js/pages/Studio/Releases.jsx create mode 100644 resources/js/pages/Studio/Review.jsx create mode 100644 resources/js/ssr.jsx create mode 100644 resources/js/test/setup.js create mode 100644 resources/views/app.blade.php create mode 100755 scripts/e2e-server.sh create mode 100644 tests/Feature/Website/AccountPagesTest.php create mode 100644 tests/Feature/Website/ReviewPagesTest.php create mode 100644 tests/Feature/Website/StudioPagesTest.php create mode 100644 tests/Helpers/website.php create mode 100644 vite.config.js diff --git a/.dockerignore b/.dockerignore index a5a9ff2..bcdb71b 100644 --- a/.dockerignore +++ b/.dockerignore @@ -14,3 +14,9 @@ database/*.sqlite* !.env.example tests *.md +public/build +public/hot +bootstrap/ssr +playwright-report +test-results +storage/e2e diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ee5e5ba..a6b7598 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -109,6 +109,86 @@ jobs: cat storage/logs/serve.log || true cat storage/logs/laravel.log || true + website: + name: Website (lint, Vitest, build) + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Node + uses: actions/setup-node@v4 + with: + node-version: "22" + cache: npm + + - name: Install dependencies + run: npm ci --no-audit --no-fund + + - name: Lint (ESLint) + run: npm run lint + + - name: Vitest + run: npm test + + - name: Build (client and SSR) + run: npm run build + + - name: Upload build + uses: actions/upload-artifact@v4 + with: + name: website-build + path: public/build + retention-days: 3 + + website-e2e: + name: Website smoke test (Playwright) + runs-on: ubuntu-latest + needs: [tests, website] + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up PHP + uses: shivammathur/setup-php@v2 + with: + php-version: "8.3" + extensions: mbstring, intl, bcmath, gd, zip, pdo_sqlite, sqlite3, phar, zlib + coverage: none + tools: composer:v2 + + - name: Set up Node + uses: actions/setup-node@v4 + with: + node-version: "22" + cache: npm + + - name: Install dependencies + run: | + composer install --no-interaction --prefer-dist --no-progress + npm ci --no-audit --no-fund + npx playwright install --with-deps chromium + + - name: Download the website build + uses: actions/download-artifact@v4 + with: + name: website-build + path: public/build + + - name: Run the smoke test + run: CI=true npx playwright test + + - name: Playwright report + if: failure() + uses: actions/upload-artifact@v4 + with: + name: playwright-report + path: | + playwright-report + test-results + storage/logs + retention-days: 7 + docker: name: Docker image builds runs-on: ubuntu-latest diff --git a/.gitignore b/.gitignore index 8c57825..e5987c4 100644 --- a/.gitignore +++ b/.gitignore @@ -23,3 +23,7 @@ Homestead.json Homestead.yaml Thumbs.db /database/*.sqlite* +/bootstrap/ssr +/playwright-report +/test-results +/storage/e2e diff --git a/Dockerfile b/Dockerfile index 6e82b10..c6f5862 100644 --- a/Dockerfile +++ b/Dockerfile @@ -13,6 +13,18 @@ COPY composer.json composer.lock ./ RUN --mount=type=cache,target=/tmp/cache \ composer install --no-dev --no-interaction --no-scripts --prefer-dist --no-progress --ignore-platform-reqs +# ---- Website assets --------------------------------------------------------- +# Vite builds the Inertia (React) client into public/build. The SSR bundle is not built: the +# runtime image has no Node, and Inertia renders on the client when no SSR server is running. +FROM node:22-bookworm-slim AS assets +WORKDIR /app +COPY package.json package-lock.json ./ +RUN --mount=type=cache,target=/root/.npm \ + npm ci --no-audit --no-fund +COPY vite.config.js ./ +COPY resources ./resources +RUN npx vite build + # ---- Runtime --------------------------------------------------------------- FROM serversideup/php:8.3-fpm-nginx-alpine AS base @@ -32,6 +44,7 @@ RUN install-php-extensions bcmath intl gd zip pdo_mysql redis pcntl exif \ WORKDIR /var/www/html COPY --chown=www-data:www-data . . COPY --from=vendor --chown=www-data:www-data /app/vendor ./vendor +COPY --from=assets --chown=www-data:www-data /app/public/build ./public/build RUN composer dump-autoload --optimize --classmap-authoritative --no-dev 2>/dev/null || true \ && mkdir -p storage/framework/{cache,sessions,views} storage/logs bootstrap/cache \ diff --git a/README.md b/README.md index d6e39de..7e87abc 100644 --- a/README.md +++ b/README.md @@ -23,12 +23,14 @@ Phase 1, Workstreams A to C, and Phase 2 Workstream E: | Instance API: registration codes, register, me, updates, installs, licence seats | done | | `registry:import` from the cloud registry-bridge tables (dry run, anomaly report, tarball copy) | done | | Releases pipeline: publisher API, upload, queued checks, state machine, D6 review policy, staff review API, publishing | done | -| Website backend (Workstream F): cookie sessions, organizations and members, listing media and pricing, staff downloads and review items, SPA fallback | done; the website itself is in progress | +| Website backend (Workstream F): cookie sessions, organizations and members, listing media and pricing, staff downloads and review items | done | +| Website (Workstream F): Inertia (React) catalog, account, Studio and review console, light and dark | done | | CLI v2 (G), Stripe (Phase 4) | later workstreams | ## Stack -Laravel 12 on PHP 8.3, MySQL 8, S3-compatible object storage. The Fleetbase core-api +Laravel 12 on PHP 8.3, MySQL 8, S3-compatible object storage. The website is Inertia v3 with +React 19, Tailwind v4 and Vite, served by the same Laravel app. The Fleetbase core-api conventions the plan relies on (`Model` with UUID keys and public ids, `HasUuid`, `HasPublicId`, the `Money` cast, `Http\Filter\Filter`, `FleetbaseResource`) are ported into `app/` with the same names and behaviour, so code written against them reads like @@ -58,15 +60,30 @@ docker compose up --build | Mailpit (verification emails) | http://localhost:8025 | | MySQL | localhost:3307 (registry / registry) | +The website needs its assets. Build them once, or run Vite beside `artisan serve` for hot +reload (Node 22): + +```bash +npm install +npm run build # public/build (client) and bootstrap/ssr (SSR bundle) +npm run dev # or: Vite dev server with hot reload +``` + Release checks run on the queue. `docker compose` starts a worker; with `php artisan serve` run `php artisan queue:work` beside it (or set `QUEUE_CONNECTION=sync`). Run the tests: ```bash -composer test +composer test # Pest +npm test # Vitest (components and page logic) +npm run lint # ESLint +npm run test:e2e # Playwright smoke test (run npm run build first) ``` +The Playwright test boots its own server (`scripts/e2e-server.sh`: a fresh SQLite database in +`storage/e2e`, `registry:seed-website`, a synchronous queue) on port 8123. + `php artisan registry:seed-website [--json]` (never in production) adds what the website's smoke test needs on top of that: `staff@registry.test` in a staff organization and `publisher@registry.test` owning `acme`, both with password `password123`, a submitted release @@ -84,6 +101,38 @@ tests/Conformance/packages.sh http://127.0.0.1:8000 CI runs the same script in the `package-conformance` job. +## Website + +The website lives in this app: Laravel routes in `routes/web.php` render Inertia pages from +`app/Http/Controllers/Web`, and every path that is not `/v1`, `/npm`, `/composer` or `/up` is +a website page (unknown paths render the website's 404). + +- **Reads.** Page controllers call the `/v1` API controllers in process + (`WebController::api`), so page props have exactly the API's JSON shapes and share the + catalog cache. +- **Writes.** Pages call `/v1` from the browser with the session cookie and the `XSRF-TOKEN` + header (`resources/js/lib/api.js`), then reload their props. +- **Sessions.** Signing in (`POST /v1/auth/session`) puts an `flbs_` token in the Laravel + session; the `web` guard resolves it. Website routes use `auth:web`, which redirects to + `/sign-in`. +- **Console version.** The "works with my console" checks read `?console=` or the + `registry_console` cookie (unencrypted, so the page can read it too), set by `POST /console`. +- **Pages.** `resources/js/pages` (Discover, Extension, Publisher, Auth, Account, Studio, + Review), shared components in `resources/js/components`, the layout in + `resources/js/layouts/SiteLayout.jsx`, the design tokens in `resources/css/registry.css` + (`data-theme` on ``: light, dark, or the system setting). +- **SSR.** `npm run build` also builds `bootstrap/ssr/ssr.js`. Inertia uses it only when + `php artisan inertia:start-ssr` is running; otherwise pages render in the browser. The + Docker image ships the client build only. + +| Path | Page | +| --- | --- | +| `/`, `/extensions/{slug}`, `/publishers/{slug}` | Discover, extension, publisher | +| `/sign-in`, `/register`, `/verify` | Account access | +| `/account/profile`, `/account/organizations/{slug?}`, `/account/tokens` | Account | +| `/studio`, `/studio/new`, `/studio/{slug}` (listing), `/studio/{slug}/releases[/{id}]`, `/studio/{slug}/review`, `/studio/{slug}/history`, `/studio/insights` | Publisher Studio | +| `/review`, `/review/publish`, `/review/releases/{id}` | Staff review console | + ## API overview All routes live under `/v1`. Errors use the Fleetbase envelope `{"errors": ["..."]}`; @@ -403,12 +452,13 @@ app/ Http/Controllers/Packages/ npm and Composer endpoints Http/Controllers/Publisher/ publisher extensions and releases Http/Controllers/Admin/ staff review API + Http/Controllers/Web/ website pages (Inertia), reading through the /v1 controllers Jobs/RunReleaseChecks.php queued release checks Releases/ pipeline, state machine, review policy (D6), listing readiness, namespace rules, manifest schema, checks, archive inspection, secrets Import/ importer: SQL dump loader, source connection, mapping, report Http/Filter/ request-driven query filters (ExtensionFilter) - Http/Middleware/ scope:…, licence:npm|composer + Http/Middleware/ scope:…, licence:npm|composer, HandleInertiaRequests (shared props) Http/Resources/ Fleetbase-style JSON resources Models/ one model per table; Model.php is the base Services/ ReleaseStorage, LicenceGate, Packages/ (packument, Composer metadata) @@ -420,7 +470,10 @@ database/seeders/ CategorySeeder (41 categories from registry-bridg docker/ compose helpers legacy/ the Verdaccio registry (v1) resources/schemas/ extension.json JSON Schema (copy of registry-bridge's) -tests/Feature/{Auth,Catalog,Packages,Instance,Import,Releases} Pest feature tests (SQLite in memory) +resources/js/ website: pages/, components/, layouts/, lib/ (Inertia + React) +resources/css/ Tailwind entry and the design tokens (registry.css) +e2e/, playwright.config.js Playwright smoke test; scripts/e2e-server.sh boots its server +tests/Feature/{Auth,Catalog,Packages,Instance,Import,Releases,Website} Pest feature tests (SQLite in memory) tests/Conformance/packages.sh real npm and Composer clients against a running service tests/Fixtures/import/ mysqldump fixture of the cloud registry tables ``` diff --git a/app/Http/Controllers/Web/AccountPageController.php b/app/Http/Controllers/Web/AccountPageController.php new file mode 100644 index 0000000..b8bc576 --- /dev/null +++ b/app/Http/Controllers/Web/AccountPageController.php @@ -0,0 +1,78 @@ +check()) { + return redirect()->route('studio'); + } + + return Inertia::render('Auth/SignIn', [ + 'email' => $request->query('email'), + 'intended' => $request->session()->get('url.intended'), + // Set by the verify page, which sends a just-verified account here. + 'verified' => $request->boolean('verified'), + ]); + } + + public function register(): Response + { + return Inertia::render('Auth/Register'); + } + + public function verify(Request $request): Response + { + return Inertia::render('Auth/Verify', [ + 'email' => $request->query('email'), + 'sent' => $request->boolean('sent'), + ]); + } + + public function profile(): Response + { + return Inertia::render('Account/Profile'); + } + + /** + * GET /account/organizations[/{slug}] — the list, plus one organization's profile and + * members (the selected one, else the first that is not personal, else the first). + */ + public function organizations(Request $request, ?string $slug = null): Response + { + $organizations = $this->api($request, '/v1/organizations', [], OrganizationController::class, 'index'); + + $selected = $slug ?? (collect($organizations)->firstWhere('is_personal', false)['slug'] ?? ($organizations[0]['slug'] ?? null)); + + return Inertia::render('Account/Organizations', [ + 'organizations' => $organizations, + 'organization' => $selected + ? $this->api($request, "/v1/organizations/{$selected}", [], OrganizationController::class, 'show', ['slug' => $selected]) + : null, + ]); + } + + public function tokens(Request $request): Response + { + return Inertia::render('Account/Tokens', [ + 'tokens' => $this->api($request, '/v1/account/tokens', [], TokenController::class, 'index'), + ]); + } +} diff --git a/app/Http/Controllers/Web/CatalogPageController.php b/app/Http/Controllers/Web/CatalogPageController.php new file mode 100644 index 0000000..8047ee4 --- /dev/null +++ b/app/Http/Controllers/Web/CatalogPageController.php @@ -0,0 +1,117 @@ +only($request, self::FILTERS); + $results = $this->api($request, '/v1/catalog/extensions', $filters + ['limit' => 24], CatalogController::class, 'extensions'); + + // Each card says whether its latest release works with the visitor's console. + $console = $this->consoleVersion($request); + $results['data'] = array_map(fn (array $item) => $item + [ + 'compatible' => $console ? Compat::satisfies($console, $item['compat']['console'] ?? null) : null, + ], $results['data'] ?? []); + + return Inertia::render('Discover/Index', [ + 'filters' => $filters, + 'results' => $results, + 'categories' => $this->api($request, '/v1/catalog/categories', [], CatalogController::class, 'categories'), + ]); + } + + /** + * GET /extensions/{slug} — overview, screenshots, versions, install. + */ + public function extension(Request $request, string $slug): Response + { + $extension = $this->api($request, "/v1/catalog/extensions/{$slug}", [], CatalogController::class, 'extension', ['slug' => $slug]); + $releases = $this->api($request, "/v1/catalog/extensions/{$slug}/releases", [], CatalogController::class, 'releases', ['slug' => $slug]); + + return Inertia::render('Extension/Show', [ + 'extension' => $extension, + 'releases' => $releases['releases'] ?? [], + 'compatibility' => fn () => $this->compatibility($releases['releases'] ?? [], $request->query('check') ?: $this->consoleVersion($request)), + ]); + } + + /** + * GET /publishers/{slug} + */ + public function publisher(Request $request, string $slug): Response + { + return Inertia::render('Publisher/Show', [ + 'publisher' => $this->api($request, "/v1/catalog/publishers/{$slug}", [], CatalogController::class, 'publisher', ['slug' => $slug]), + ]); + } + + /** + * Whether the latest release works with a console version, and if not, the newest release + * that does. Null when no version is known. `?check=` asks for another version (a partial + * reload of this prop). + * + * @param list> $releases newest first + */ + protected function compatibility(array $releases, ?string $version): ?array + { + if (! $version || ! Semver::valid($version)) { + return $version ? ['version' => $version, 'valid' => false] : null; + } + + $published = array_values(array_filter($releases, fn ($release) => ($release['status'] ?? null) === 'published')); + $latest = $published[0] ?? null; + $newest = null; + foreach ($published as $release) { + if (Compat::satisfies($version, $release['compat']['console'] ?? null)) { + $newest = $release['version']; + break; + } + } + + return [ + 'version' => $version, + 'valid' => true, + 'compatible' => $latest !== null && Compat::satisfies($version, $latest['compat']['console'] ?? null), + 'newest_compatible' => $newest, + ]; + } + + protected function consoleVersion(Request $request): ?string + { + $version = $request->query('console') ?: $request->cookie(HandleInertiaRequests::CONSOLE_COOKIE); + + return is_string($version) && Semver::valid($version) ? $version : null; + } + + /** + * POST /console — remember the console version the visitor checks compatibility against. + */ + public function console(Request $request) + { + $request->validate(['version' => ['nullable', 'string', 'max:32', 'regex:/^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/']]); + + $cookie = $request->filled('version') + ? cookie()->forever(HandleInertiaRequests::CONSOLE_COOKIE, $request->input('version'), httpOnly: false) + : cookie()->forget(HandleInertiaRequests::CONSOLE_COOKIE); + + return back()->withCookie($cookie); + } +} diff --git a/app/Http/Controllers/Web/ReviewPageController.php b/app/Http/Controllers/Web/ReviewPageController.php new file mode 100644 index 0000000..be8d9f7 --- /dev/null +++ b/app/Http/Controllers/Web/ReviewPageController.php @@ -0,0 +1,168 @@ + [Release::STATUS_SUBMITTED, Release::STATUS_IN_REVIEW], + 'decided' => [Release::STATUS_REJECTED, Release::STATUS_PUBLISHED], + 'failed' => [Release::STATUS_FAILED_CHECKS], + ]; + + /** + * GET /review?tab=waiting|decided|failed — oldest first. `claimers` names the reviewer who + * took each in-review release, keyed by release id (the queue API leaves the timeline out). + */ + public function queue(Request $request): Response + { + $this->authorizeStaff($request); + $tab = array_key_exists((string) $request->query('tab'), self::TABS) ? $request->query('tab') : 'waiting'; + $releases = $this->api($request, '/v1/admin/releases', ['status' => implode(',', self::TABS[$tab]), 'limit' => 100], ReviewController::class, 'queue'); + + return Inertia::render('Review/Queue', [ + 'tab' => $tab, + 'releases' => $releases, + 'claimers' => $this->latestReviewers($releases['data'] ?? [], Release::STATUS_IN_REVIEW, Review::ACTION_CLAIM), + 'counts' => $this->counts(), + ]); + } + + /** + * GET /review/publish — approved releases waiting to go live (with who approved them), and + * suspended listings (with the reason and who suspended them). + */ + public function pending(Request $request): Response + { + $this->authorizeStaff($request); + $releases = $this->api($request, '/v1/admin/releases', ['status' => Release::STATUS_APPROVED, 'limit' => 100], ReviewController::class, 'queue'); + + $suspended = Extension::query() + ->where('status', Extension::STATUS_SUSPENDED) + ->with(['organization', 'icon']) + ->orderByDesc('updated_at') + ->limit(50) + ->get(); + $suspenders = Account::query() + ->whereIn('public_id', $suspended->pluck('meta.suspension.by')->filter()->unique()->values()) + ->pluck('name', 'public_id'); + + return Inertia::render('Review/Pending', [ + 'releases' => $releases, + 'approvers' => $this->latestReviewers($releases['data'] ?? [], Release::STATUS_APPROVED, Review::ACTION_APPROVE), + 'suspended' => $suspended->map(fn (Extension $extension) => [ + 'id' => $extension->public_id, + 'slug' => $extension->slug, + 'name' => $extension->name, + 'icon_url' => $extension->icon?->url, + 'organization' => $extension->organization?->only(['slug', 'name']), + 'suspension' => $this->suspension($extension, $suspenders->all()), + ])->all(), + 'counts' => $this->counts(), + ]); + } + + /** + * GET /review/releases/{release} — the release with its listing preview, the review + * requirements, and the listing's suspension when it is suspended. + */ + public function release(Request $request, string $release): Response + { + $this->authorizeStaff($request); + $model = Release::findByIdOrFail($release); + $extension = $model->extension; + $suspension = $extension?->status === Extension::STATUS_SUSPENDED + ? $this->suspension($extension, Account::query()->where('public_id', $extension->meta['suspension']['by'] ?? null)->pluck('name', 'public_id')->all()) + : null; + + return Inertia::render('Review/Release', [ + 'release' => $this->api($request, "/v1/admin/releases/{$release}", [], ReviewController::class, 'show', ['release' => $release]), + 'reviewItems' => $this->api($request, '/v1/admin/review-items', [], ReviewController::class, 'reviewItems'), + 'suspension' => $suspension, + ]); + } + + protected function authorizeStaff(Request $request): void + { + $account = $request->user(); + + abort_unless($account instanceof Account && $account->isStaff(), 403, 'The review console is for Fleetbase staff.'); + } + + /** + * The staff member behind the latest `$action` of each listed release in `$status`, keyed + * by release id: `{id, name, at}`. + * + * @param list> $releases ReleaseDetailResource arrays + * @return array + */ + protected function latestReviewers(array $releases, string $status, string $action): array + { + $ids = collect($releases)->where('status', $status)->pluck('id')->filter()->values(); + if ($ids->isEmpty()) { + return []; + } + + return Release::query() + ->whereIn('public_id', $ids) + ->with(['reviews' => fn ($query) => $query->where('action', $action)->with('reviewer')]) + ->get() + ->mapWithKeys(function (Release $release) { + $review = $release->reviews->sortBy('id')->last(); + + return [$release->public_id => [ + 'id' => $review?->reviewer?->public_id, + 'name' => $review?->reviewer?->name, + 'at' => $review?->created_at?->toIso8601String(), + ]]; + }) + ->filter(fn (array $reviewer) => $reviewer['name'] !== null) + ->all(); + } + + /** + * A listing's suspension (`reason`, `at`, `by`) with the suspending account's name. + * + * @param array $names account public id => name + */ + protected function suspension(Extension $extension, array $names): ?array + { + $suspension = $extension->meta['suspension'] ?? null; + if (! is_array($suspension)) { + return null; + } + + return [ + 'reason' => $suspension['reason'] ?? null, + 'at' => $suspension['at'] ?? null, + 'by' => $suspension['by'] ?? null, + 'by_name' => $names[$suspension['by'] ?? ''] ?? null, + ]; + } + + /** + * @return array + */ + protected function counts(): array + { + return [ + 'waiting' => Release::query()->whereIn('status', self::TABS['waiting'])->count(), + 'publish' => Release::query()->where('status', Release::STATUS_APPROVED)->count(), + 'suspended' => Extension::query()->where('status', Extension::STATUS_SUSPENDED)->count(), + ]; + } +} diff --git a/app/Http/Controllers/Web/StudioPageController.php b/app/Http/Controllers/Web/StudioPageController.php new file mode 100644 index 0000000..53fdd86 --- /dev/null +++ b/app/Http/Controllers/Web/StudioPageController.php @@ -0,0 +1,135 @@ + $this->api($request, '/v1/publisher/extensions', [], ExtensionController::class, 'index'), + 'organization' => $request->query('org'), + ]); + } + + public function create(Request $request): Response + { + return Inertia::render('Studio/Create', [ + 'categories' => $this->categories($request), + ]); + } + + /** + * GET /studio/{slug} — the listing editor. + */ + public function editor(Request $request, string $slug): Response + { + return Inertia::render('Studio/Editor', $this->listing($request, $slug) + [ + 'categories' => $this->categories($request), + ]); + } + + /** + * GET /studio/{slug}/releases — upload and the release list. + */ + public function releases(Request $request, string $slug): Response + { + return Inertia::render('Studio/Releases', $this->listing($request, $slug)); + } + + /** + * GET /studio/{slug}/releases/{release} — one release with its checks; the page polls this + * (a partial reload of `release`) while the checks run. + */ + public function release(Request $request, string $slug, string $release): Response + { + $extension = $this->api($request, "/v1/publisher/extensions/{$slug}", [], ExtensionController::class, 'show', ['slug' => $slug]); + + return Inertia::render('Studio/Release', [ + 'extension' => $extension, + 'release' => function () use ($request, $release, $extension) { + $detail = $this->api($request, "/v1/publisher/releases/{$release}", [], ReleaseController::class, 'show', ['release' => $release]); + // A release of another of the publisher's listings is not found under this one. + abort_if(($detail['extension']['id'] ?? null) !== $extension['id'], 404); + + return $detail; + }, + ]); + } + + /** + * GET /studio/{slug}/review — submit the open release, or follow its review. + */ + public function review(Request $request, string $slug): Response + { + $listing = $this->listing($request, $slug); + $open = collect($listing['releases'])->first(fn (array $release) => in_array($release['status'], self::OPEN, true)); + + return Inertia::render('Studio/Review', $listing + [ + 'candidate' => $open + ? $this->api($request, "/v1/publisher/releases/{$open['id']}", [], ReleaseController::class, 'show', ['release' => $open['id']]) + : null, + ]); + } + + /** + * GET /studio/{slug}/history — every release's review timeline, newest first. + */ + public function history(Request $request, string $slug): Response + { + $listing = $this->listing($request, $slug); + $details = collect($listing['releases'])->take(20)->map( + fn (array $release) => $this->api($request, "/v1/publisher/releases/{$release['id']}", [], ReleaseController::class, 'show', ['release' => $release['id']]) + )->values()->all(); + + return Inertia::render('Studio/History', [ + 'extension' => $listing['extension'], + 'releases' => $details, + 'truncated' => count($listing['releases']) > 20, + ]); + } + + public function insights(): Response + { + return Inertia::render('Studio/Insights'); + } + + /** + * The listing (as its publisher sees it) and every release with its checks, newest first. + * + * @return array{extension: array, releases: list} + */ + protected function listing(Request $request, string $slug): array + { + return [ + 'extension' => $this->api($request, "/v1/publisher/extensions/{$slug}", [], ExtensionController::class, 'show', ['slug' => $slug]), + 'releases' => $this->api($request, "/v1/publisher/extensions/{$slug}/releases", [], ReleaseController::class, 'index', ['slug' => $slug]), + ]; + } + + protected function categories(Request $request): array + { + return $this->api($request, '/v1/catalog/categories', [], CatalogController::class, 'categories'); + } +} diff --git a/app/Http/Controllers/Web/WebController.php b/app/Http/Controllers/Web/WebController.php new file mode 100644 index 0000000..364ad13 --- /dev/null +++ b/app/Http/Controllers/Web/WebController.php @@ -0,0 +1,64 @@ + $parameters route parameters (slug, release…) + */ + protected function api(Request $request, string $uri, array $query, string $controller, string $method, array $parameters = []): array + { + $sub = Request::create($uri, 'GET', $query); + $sub->setUserResolver($request->getUserResolver()); + if ($request->hasSession()) { + $sub->setLaravelSession($request->session()); + } + + $previous = app('request'); + app()->instance('request', $sub); + + try { + $result = app()->call([app($controller), $method], ['request' => $sub] + $parameters); + + if ($result instanceof JsonResource) { + return $result->response($sub)->getData(true); + } + + if ($result instanceof JsonResponse) { + return $result->getData(true); + } + + return (array) $result; + } finally { + app()->instance('request', $previous); + } + } + + /** + * Only the query parameters a listing page passes through to the API. + * + * @param list $keys + */ + protected function only(Request $request, array $keys): array + { + return array_filter($request->only($keys), fn ($value) => $value !== null && $value !== ''); + } +} diff --git a/app/Http/Controllers/WebsiteController.php b/app/Http/Controllers/WebsiteController.php deleted file mode 100644 index 56010a7..0000000 --- a/app/Http/Controllers/WebsiteController.php +++ /dev/null @@ -1,46 +0,0 @@ -isMethod('GET') && ! $request->isMethod('HEAD')) { - throw new NotFoundHttpException; - } - - if ($request->is(...self::API_PATHS)) { - throw new NotFoundHttpException; - } - - // A built asset the web server did not serve itself (php artisan serve does). - $public = realpath(public_path()); - $file = realpath(public_path(ltrim($request->path(), '/'))); - if ($request->path() !== '/' && $file !== false && $public !== false && str_starts_with($file, $public.DIRECTORY_SEPARATOR) && is_file($file) && basename($file) !== 'index.php') { - return response()->file($file); - } - - $index = public_path('index.html'); - if (! is_file($index)) { - return response()->error('The website has not been built. Run npm run build in web/.', 503); - } - - return response()->file($index, [ - 'Content-Type' => 'text/html; charset=UTF-8', - 'Cache-Control' => 'no-cache', - ]); - } -} diff --git a/app/Http/Middleware/HandleInertiaRequests.php b/app/Http/Middleware/HandleInertiaRequests.php new file mode 100644 index 0000000..0f6827a --- /dev/null +++ b/app/Http/Middleware/HandleInertiaRequests.php @@ -0,0 +1,59 @@ + + */ + public function share(Request $request): array + { + /** @var Account|null $account */ + $account = Auth::guard('web')->user(); + + return [ + ...parent::share($request), + 'auth' => [ + 'account' => $account ? (new AccountResource($account->loadMissing('organizations')))->resolve($request) : null, + ], + 'registry' => [ + 'host' => parse_url((string) (config('registry.public_url') ?: $request->getSchemeAndHttpHost()), PHP_URL_HOST) ?: $request->getHost(), + 'url' => rtrim((string) (config('registry.public_url') ?: $request->getSchemeAndHttpHost()), '/'), + ], + 'console' => $this->consoleVersion($request), + 'flash' => fn () => $request->session()->get('flash'), + ]; + } + + /** + * The console version from `?console=` (a console linking here sends it) or the cookie. + */ + protected function consoleVersion(Request $request): ?string + { + $version = $request->query('console') ?: $request->cookie(self::CONSOLE_COOKIE); + + return is_string($version) && preg_match('/^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/', $version) ? $version : null; + } +} diff --git a/bootstrap/app.php b/bootstrap/app.php index 27ceb8b..25beb6a 100644 --- a/bootstrap/app.php +++ b/bootstrap/app.php @@ -3,6 +3,7 @@ use App\Http\Middleware\EnsurePackageLicence; use App\Http\Middleware\EnsureStaff; use App\Http\Middleware\EnsureTokenScope; +use App\Http\Middleware\HandleInertiaRequests; use App\Http\Middleware\StatefulSession; use Illuminate\Auth\AuthenticationException; use Illuminate\Cookie\Middleware\EncryptCookies; @@ -12,6 +13,7 @@ use Illuminate\Foundation\Configuration\Middleware; use Illuminate\Http\Request; use Illuminate\Validation\ValidationException; +use Inertia\Inertia; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; @@ -34,6 +36,11 @@ // The website's cookie session must start before `auth:token,web` resolves the caller. $middleware->prependToPriorityList(EncryptCookies::class, StatefulSession::class); + + // Website pages render with Inertia. The console version and theme cookies are written + // by the page's script, so they are not encrypted. + $middleware->web(append: [HandleInertiaRequests::class]); + $middleware->encryptCookies(except: [HandleInertiaRequests::CONSOLE_COOKIE]); }) ->withExceptions(function (Exceptions $exceptions): void { $isPackage = fn (Request $request) => $request->is('npm', 'npm/*', 'composer', 'composer/*'); @@ -77,4 +84,27 @@ return response()->error($message, $e->getStatusCode())->withHeaders($e->getHeaders()); } }); + + // Website pages: errors render as the Inertia error page (not in debug mode for 500s, so + // the stack trace stays visible locally). An expired CSRF token sends the visitor back. + $exceptions->respond(function (Response $response, Throwable $e, Request $request) use ($isApi) { + if ($isApi($request) || $request->is('up')) { + return $response; + } + + $status = $response->getStatusCode(); + if ($status === 419) { + return back()->with('flash', ['type' => 'error', 'message' => 'The page expired. Please try again.']); + } + + if (in_array($status, [403, 404], true) || (in_array($status, [500, 503], true) && ! app()->hasDebugModeEnabled())) { + return Inertia::render('Error', [ + 'status' => $status, + // A 403 shows its reason (abort(403, '…')), not the framework's generic one. + 'message' => $status === 403 && ! in_array($e->getMessage(), ['', 'This action is unauthorized.'], true) ? $e->getMessage() : null, + ])->toResponse($request)->setStatusCode($status); + } + + return $response; + }); })->create(); diff --git a/composer.json b/composer.json index 037ee9f..70675dc 100644 --- a/composer.json +++ b/composer.json @@ -12,6 +12,7 @@ "require": { "php": "^8.2", "composer/semver": "^3.4", + "inertiajs/inertia-laravel": "^3.0", "laravel/framework": "^12.0", "laravel/tinker": "^2.10.1", "league/flysystem-aws-s3-v3": "^3.0", diff --git a/composer.lock b/composer.lock index 175f6be..9f48747 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": "4528b9e54e35558451623ac714d98c4f", + "content-hash": "01a5a44b360e697ea7dc5ee99d032426", "packages": [ { "name": "aws/aws-crt-php", @@ -1289,6 +1289,78 @@ ], "time": "2026-08-24T09:15:32+00:00" }, + { + "name": "inertiajs/inertia-laravel", + "version": "v3.5.2", + "source": { + "type": "git", + "url": "https://github.com/inertiajs/inertia-laravel.git", + "reference": "4e91cf45cad5ef97862d1ede40fe41616d4e4f91" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/inertiajs/inertia-laravel/zipball/4e91cf45cad5ef97862d1ede40fe41616d4e4f91", + "reference": "4e91cf45cad5ef97862d1ede40fe41616d4e4f91", + "shasum": "" + }, + "require": { + "ext-json": "*", + "laravel/framework": "^11.35|^12.0|^13.0", + "php": "^8.2.0", + "symfony/console": "^7.0|^8.0" + }, + "conflict": { + "laravel/boost": "<2.5.0" + }, + "require-dev": { + "guzzlehttp/guzzle": "^7.15.2|^8.0", + "larastan/larastan": "^3.0", + "laravel/pint": "^1.16", + "mockery/mockery": "^1.3.3", + "orchestra/testbench": "^9.2|^10.0|^11.0", + "phpunit/phpunit": "^11.5|^12.0" + }, + "suggest": { + "ext-pcntl": "Recommended when running the Inertia SSR server via the `inertia:start-ssr` artisan command." + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Inertia\\ServiceProvider" + ] + } + }, + "autoload": { + "files": [ + "./helpers.php" + ], + "psr-4": { + "Inertia\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jonathan Reinink", + "email": "jonathan@reinink.ca", + "homepage": "https://reinink.ca" + } + ], + "description": "The Laravel adapter for Inertia.js.", + "keywords": [ + "inertia", + "laravel" + ], + "support": { + "issues": "https://github.com/inertiajs/inertia-laravel/issues", + "source": "https://github.com/inertiajs/inertia-laravel/tree/v3.5.2" + }, + "time": "2026-10-21T19:41:00+00:00" + }, { "name": "laravel/framework", "version": "v12.70.0", diff --git a/e2e/smoke.spec.js b/e2e/smoke.spec.js new file mode 100644 index 0000000..981ae56 --- /dev/null +++ b/e2e/smoke.spec.js @@ -0,0 +1,114 @@ +// Smoke test against a seeded local Registry Service (scripts/e2e-server.sh): browse the catalog, +// sign in as a publisher, create a token, then review, approve and publish as staff. +import { test, expect } from '@playwright/test'; +import { readFileSync } from 'node:fs'; + +const seed = () => JSON.parse(readFileSync(new URL('../storage/e2e/seed.json', import.meta.url), 'utf8')); + +async function signIn(page, { email, password }) { + await page.goto('/sign-in'); + await page.getByLabel('Email').fill(email); + await page.getByLabel('Password').fill(password); + await page.locator('[data-sign-in-submit]').click(); + await expect(page.locator('[data-account-menu]')).toBeVisible(); +} + +async function signOut(page) { + await page.locator('[data-account-menu]').click(); + await page.locator('[data-sign-out]').click(); + await expect(page.locator('[data-sign-in]')).toBeVisible(); +} + +test('public catalog: search, detail and install instructions', async ({ page }) => { + const { extensions } = seed(); + await page.goto('/'); + await expect(page.locator('[data-extension]').first()).toBeVisible(); + + const slug = extensions[0]; + await page.locator(`[data-extension="${slug}"]`).click(); + await expect(page).toHaveURL(new RegExp(`/extensions/${slug}$`)); + await expect(page.locator('[data-extension-name]')).toBeVisible(); + await expect(page.locator('[data-versions]')).toBeVisible(); + + await page.getByRole('tab', { name: 'CLI' }).click(); + await expect(page.locator('[data-install="cli"]')).toContainText(`flb install ${slug}`); + + await page.goto('/no/such/page'); + await expect(page.locator('[data-error-status="404"]')).toBeVisible(); +}); + +test('publisher: Studio, a listing and a token shown once', async ({ page }) => { + const { publisher } = seed(); + await signIn(page, publisher); + await expect(page).toHaveURL(/\/studio$/); + + await page.goto('/account/tokens'); + await expect(page.getByRole('heading', { name: 'Tokens', level: 1 })).toBeVisible(); + // With no tokens yet the form is already open. + if (!(await page.locator('[data-new-token-form]').isVisible())) { + await page.locator('[data-new-token]').click(); + } + await page.locator('[data-new-token-form]').getByLabel('Name').fill('Playwright smoke'); + await page.locator('[data-create-token]').click(); + const value = page.locator('[data-token-value]'); + await expect(value).toHaveText(/^flbp_[A-Za-z0-9]{40}$/); + const plaintext = await value.textContent(); + await page.locator('[data-token-stored]').check(); + await page.locator('[data-token-done]').click(); + await expect(page.locator('[data-token]').filter({ hasText: 'Playwright smoke' })).toBeVisible(); + await expect(page.locator('body')).not.toContainText(plaintext); + + // The session cookie is HttpOnly: scripts on the page never see it. + const cookies = await page.context().cookies(); + expect(cookies.find((cookie) => /[-_]session$/.test(cookie.name))?.httpOnly).toBe(true); + await signOut(page); +}); + +test('staff: review queue, approve and publish, then it is in the catalog', async ({ page }) => { + const { staff, queued_release: releaseId } = seed(); + await signIn(page, staff); + + await page.goto('/review'); + await page.locator('[data-queue-row]').first().click(); + await expect(page).toHaveURL(new RegExp(`/review/releases/${releaseId}$`)); + await expect(page.locator('[data-check]')).toHaveCount(8); + const name = (await page.locator('h1.h2').textContent()).replace(/\s*v?\d+\.\d+\.\d+.*$/, '').trim(); + + if (await page.locator('[data-claim]').isVisible()) { + await page.locator('[data-claim]').click(); + await expect(page.locator('[data-claim]')).toHaveCount(0); + } + for (const code of ['R1', 'R2', 'R3', 'R4', 'R5', 'R6']) { + // R1 and R4 may already be passed from the automated checks; clicking again would clear them. + const item = page.locator(`[data-decision] [data-item="${code}"]`); + if ((await item.getAttribute('data-mark')) !== 'pass') { + await item.getByRole('button', { name: 'Pass' }).click(); + } + } + await page.locator('[data-decision] [data-approve]').click(); + await page.locator('[data-confirm-approve]').click(); + // Approving with "publish as soon as it's approved" publishes in the same request. + await expect(page.locator('[data-decided]')).toContainText('Live', { timeout: 20_000 }); + + await page.goto(`/?q=${encodeURIComponent(name)}`); + await expect(page.locator('[data-extension]').filter({ hasText: name })).toBeVisible(); +}); + +test('theme toggle switches between light and dark and is remembered', async ({ page }) => { + await page.goto('/'); + const theme = () => page.evaluate(() => document.documentElement.dataset.theme); + const before = await theme(); + await page.locator('[data-theme-toggle]').click(); + const after = await theme(); + expect(after).not.toBe(before); + expect(['light', 'dark']).toContain(after); + await page.reload(); + expect(await theme()).toBe(after); +}); + +test('Discover works on a phone @phone', async ({ page }) => { + await page.goto('/'); + await expect(page.locator('[data-extension]').first()).toBeVisible(); + const overflow = await page.evaluate(() => document.documentElement.scrollWidth - document.documentElement.clientWidth); + expect(overflow).toBeLessThanOrEqual(0); +}); diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..c2740b2 --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,26 @@ +import js from '@eslint/js'; +import globals from 'globals'; +import reactHooks from 'eslint-plugin-react-hooks'; + +export default [ + { ignores: ['public/**', 'vendor/**', 'bootstrap/ssr/**', 'node_modules/**', 'legacy/**'] }, + js.configs.recommended, + { + files: ['resources/js/**/*.{js,jsx}'], + plugins: { 'react-hooks': reactHooks }, + languageOptions: { + ecmaVersion: 'latest', + sourceType: 'module', + parserOptions: { ecmaFeatures: { jsx: true } }, + globals: { ...globals.browser, ...globals.node }, + }, + rules: { + 'react-hooks/rules-of-hooks': 'error', + 'react-hooks/exhaustive-deps': 'warn', + }, + }, + { + files: ['resources/js/**/*.test.{js,jsx}', 'resources/js/test/**'], + languageOptions: { globals: { ...globals.browser, ...globals.node, vi: 'readonly', describe: 'readonly', it: 'readonly', expect: 'readonly', beforeEach: 'readonly', afterEach: 'readonly' } }, + }, +]; diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..dacb2b0 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,4301 @@ +{ + "name": "laughing-blackburn-72efbc", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "dependencies": { + "@inertiajs/react": "^3.9.1", + "axios": "^1.20.0", + "dompurify": "^3.4.17", + "react": "^19.3.1", + "react-dom": "^19.3.1" + }, + "devDependencies": { + "@eslint/js": "^10.0.1", + "@playwright/test": "^1.64.1", + "@tailwindcss/vite": "^4.3.3", + "@testing-library/jest-dom": "^7.0.1", + "@testing-library/react": "^16.3.3", + "@testing-library/user-event": "^14.6.8", + "@vitejs/plugin-react": "^6.1.2", + "eslint": "^10.13.0", + "eslint-plugin-react-hooks": "^7.1.1", + "globals": "^17.13.0", + "jsdom": "^30.1.3", + "laravel-vite-plugin": "^3.2.0", + "tailwindcss": "^4.3.3", + "vite": "^8.3.5", + "vitest": "^5.0.5" + } + }, + "node_modules/@adobe/css-tools": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.5.0.tgz", + "integrity": "sha512-6OzddxPio9UiWTCemp4N8cYLV2ZN1ncRnV1cVGtve7dhPOtRkleRyx32GQCYSwDYgaHU3USMm84tNsvKzRCa1Q==", + "dev": true + }, + "node_modules/@asamuzakjp/css-color": { + "version": "7.1.7", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-7.1.7.tgz", + "integrity": "sha512-Q7ioeear07HwixdIitIn9KS1LZi97CEDFCSjPNm9v+y8KNbsu9q4Lrd4zUQW4faED+E7qxvfeVnbNRW46wFKuw==", + "dev": true, + "dependencies": { + "@csstools/css-calc": "^3.4.3", + "@csstools/css-color-parser": "^4.2.6", + "@csstools/css-parser-algorithms": "^4.0.2", + "@csstools/css-tokenizer": "^4.0.2", + "lru-cache": "^11.5.3" + }, + "engines": { + "node": "^22.22.2 || ^24.15.0 || >=26.0.0" + } + }, + "node_modules/@asamuzakjp/css-color/node_modules/lru-cache": { + "version": "11.5.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.3.tgz", + "integrity": "sha512-U4N8FgzmWxc8k1VH8Kr6lQg18U7Fjvby6wXHVRX/ZZ7IwWbRMgrRbP0Wrb5q5NVinryp4SQampHKdvtecItxUg==", + "dev": true, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@asamuzakjp/dom-selector": { + "version": "10.0.4", + "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-10.0.4.tgz", + "integrity": "sha512-A6f1nKy99I6tyoK82U6kBHvQQZmdHmTmPzJYPY/0eaOW0r8/qPmEOwjGHs+hL/vCgOwQTupu4LT1XtXNUuDruA==", + "dev": true, + "dependencies": { + "bidi-js": "^1.1.0", + "css-tree": "^3.2.1", + "is-potential-custom-element-name": "^1.0.1", + "lru-cache": "^11.5.3" + }, + "engines": { + "node": "^22.22.2 || ^24.15.0 || >=26.0.0" + } + }, + "node_modules/@asamuzakjp/dom-selector/node_modules/lru-cache": { + "version": "11.5.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.3.tgz", + "integrity": "sha512-U4N8FgzmWxc8k1VH8Kr6lQg18U7Fjvby6wXHVRX/ZZ7IwWbRMgrRbP0Wrb5q5NVinryp4SQampHKdvtecItxUg==", + "dev": true, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.8.tgz", + "integrity": "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.29.8", + "@babel/types": "^7.29.8", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", + "dev": true, + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", + "dev": true, + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.10", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.10.tgz", + "integrity": "sha512-EMCx3AGZ3WfAl6ht7AyQoPm2Lh+BGYEMdc4Q6Hf7uSCDOZFuLq44qNwL+iaim8VlyYhRAo4X43+weadK8ZB4Yg==", + "dev": true, + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.8" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.9", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.9.tgz", + "integrity": "sha512-CjXrNHTnvqBVqHgdBysY3vk2T8tpJHb5/RMeHJBTyVa9xgugCB0CJTx/3oO8RV2QRQP391RWpB7D6hLjm8V9uA==", + "dev": true, + "dependencies": { + "@babel/types": "^7.29.8" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.29.10", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.10.tgz", + "integrity": "sha512-NpugTzbKgGrNe45Ui32cN8vCFFMxVtP/9rtfwv0nP5FRDAjvYBxJcLVpYA9mr1jxP6cnIT7x9LUzuDTbWYYNRA==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.10", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.10.tgz", + "integrity": "sha512-B3lYAs2nkKskbgQ1nfy8Ma9ao9Q1rntTAQHrzzyDuj+TSO+X3le6PfWwqDvodsb+OumE10eMFfuMCTQSQ5iuIQ==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.8", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.9", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.8", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz", + "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", + "dev": true, + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bramus/specificity": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/@bramus/specificity/-/specificity-2.4.3.tgz", + "integrity": "sha512-0GEz8qJWokkUVYQYRukmkSHIFNoBH8QTreeHrAFbvz1XFYc3Y+puhMs8PQ5x8GNugiC2l5jvDBXKLbBIByeSBw==", + "dev": true, + "dependencies": { + "css-tree": "^3.2.1" + }, + "bin": { + "specificity": "bin/cli.js" + } + }, + "node_modules/@cacheable/memory": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@cacheable/memory/-/memory-2.2.0.tgz", + "integrity": "sha512-CTLKqLItRCEixEAewD3/j9DB3/o96gpTPD4eJ1v+DGOlxZRZncRQkGYqqnAGCscYd6RNeXfGeiuCphsPtqyIfQ==", + "dev": true, + "dependencies": { + "@cacheable/utils": "^2.5.0", + "@keyv/bigmap": "^1.3.1", + "hookified": "^1.15.1", + "keyv": "^5.6.0" + } + }, + "node_modules/@cacheable/utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@cacheable/utils/-/utils-2.5.0.tgz", + "integrity": "sha512-buipgOVDkkPXNR5+xBpDw7Zk2n1EvU7qBJCNUcL7rhQ//kfpOXPAvQ511Os0vpLYJ1pZnvudNytkQt2hst3wqA==", + "dev": true, + "dependencies": { + "hashery": "^1.5.1", + "keyv": "^5.6.0" + } + }, + "node_modules/@csstools/color-helpers": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.1.2.tgz", + "integrity": "sha512-grhRy3OKmniaAEKXMjua5z/EODX0MSqBGjunw8+j/3HQjOnahs2AGhvEOIYVUWcU6ScApbhLhVrQTX8XqrMrow==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/@csstools/css-calc": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.4.3.tgz", + "integrity": "sha512-iex20d8CHVkyvg6B7UKV7uHnI2Bqo9g+EFfT9E0y+GvTvhZ/DwONJ+9aKb1dlqm0ZiGsL5RXjp0fCoJYnkeDjA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.2", + "@csstools/css-tokenizer": "^4.0.2" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "4.2.6", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.2.6.tgz", + "integrity": "sha512-iiPQ3iRWwnJkeEn6RIu6SJPr7hYrLz6XZ9s/QZl+2/LI5KQVjpl2fdmDSZKuD4xP6GMmMPgHFFXg6k1Wkz0Trg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "dependencies": { + "@csstools/color-helpers": "^6.1.2", + "@csstools/css-calc": "^3.4.3" + }, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.2", + "@csstools/css-tokenizer": "^4.0.2" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.2.tgz", + "integrity": "sha512-40cSKyMvK+tq4qz6Awrlye2WGuOKt3FwPgtGg6KTfbHOWNw+Rk1rzbAtZnZ6IBhsY491HLRnDXwoyBAijmmILA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^4.0.2" + } + }, + "node_modules/@csstools/css-syntax-patches-for-csstree": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.15.tgz", + "integrity": "sha512-J0u7HkVl2nzSlhsiTOp4AmwcUQ3D+mGEEKfBy/7To5/y7F2OHwyLrXfrhR0SMgr4p5Lo+eaMVSeai24zUcBIxA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "peerDependencies": { + "css-tree": "^3.2.1" + }, + "peerDependenciesMeta": { + "css-tree": { + "optional": true + } + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.2.tgz", + "integrity": "sha512-OoKoR0f76dCY666JlcbhmVTs2drYj1GUXZTYTcbUgJjh9Nv41aFfZ21bPQTERm5+L5cBDo466NltB2lplS5GBw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.10.1.tgz", + "integrity": "sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==", + "dev": true, + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.23.5", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.5.tgz", + "integrity": "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==", + "dev": true, + "dependencies": { + "@eslint/object-schema": "^3.0.5", + "debug": "^4.3.1", + "minimatch": "^10.2.4" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.7.0.tgz", + "integrity": "sha512-DObd/KKUsU+FaFv4PLxSRenpXfQWmPXXP3pPZ6/K1PCrMu2vQpMDMuQe/BqYeoLcz8ro0bVDF1RxOJgfVEdhUw==", + "dev": true, + "dependencies": { + "@eslint/core": "^1.2.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/core": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.2.1.tgz", + "integrity": "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/js": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-10.0.1.tgz", + "integrity": "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==", + "dev": true, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "eslint": "^10.0.0" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/@eslint/object-schema": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.5.tgz", + "integrity": "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==", + "dev": true, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.3.tgz", + "integrity": "sha512-IkO+/KEUvwbVpiURZg+P7zF74z5Jxe0UgJxVni+RtoHQ6IZieXaO02kmadomap/q+l6bc/jdPGGqTjhuZnuz1Q==", + "dev": true, + "dependencies": { + "@eslint/core": "^1.2.1", + "levn": "^0.4.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@exodus/bytes": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.16.0.tgz", + "integrity": "sha512-IcpW84uEn3N7ETtNZMlxKhfl6Pec8rUNGOTBtWbK1FKhJxIFAptZyVrvVRVBimAJxJCgc3PxepxkdWWG4DVzfA==", + "dev": true, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "@noble/hashes": "^1.8.0 || ^2.0.0" + }, + "peerDependenciesMeta": { + "@noble/hashes": { + "optional": true + } + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", + "dev": true, + "dependencies": { + "@humanfs/types": "^0.15.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", + "dev": true, + "dependencies": { + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "dev": true, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@inertiajs/core": { + "version": "3.9.1", + "resolved": "https://registry.npmjs.org/@inertiajs/core/-/core-3.9.1.tgz", + "integrity": "sha512-ScLQZ1/KXlvmqCEJMEZABSK5updJZIli1kaG/coWXpWMmpSLsazSJkR7esITYp7X5vTgrOGNIrcRmdMp/GQ0NA==", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.31", + "es-toolkit": "^1.33.0", + "laravel-precognition": "^2.1.0" + }, + "peerDependencies": { + "axios": "^1.15.2" + }, + "peerDependenciesMeta": { + "axios": { + "optional": true + } + } + }, + "node_modules/@inertiajs/react": { + "version": "3.9.1", + "resolved": "https://registry.npmjs.org/@inertiajs/react/-/react-3.9.1.tgz", + "integrity": "sha512-fRWElF5XIm1j01ecQCEehx0KPLEYVA6fCdWvig7XUhwhmSQG6EIyNum3eG8ZEfYuY7mWhNyqae/MSRIv+bAsjw==", + "dependencies": { + "@inertiajs/core": "3.9.1", + "es-toolkit": "^1.33.0", + "laravel-precognition": "^2.1.0" + }, + "peerDependencies": { + "react": "^19.0.0", + "react-dom": "^19.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz", + "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@keyv/bigmap": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@keyv/bigmap/-/bigmap-1.3.1.tgz", + "integrity": "sha512-WbzE9sdmQtKy8vrNPa9BRnwZh5UF4s1KTmSK0KUVLo3eff5BlQNNWDnFOouNpKfPKDnms9xynJjsMYjMaT/aFQ==", + "dev": true, + "dependencies": { + "hashery": "^1.4.0", + "hookified": "^1.15.0" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "keyv": "^5.6.0" + } + }, + "node_modules/@keyv/serialize": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@keyv/serialize/-/serialize-1.1.1.tgz", + "integrity": "sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA==", + "dev": true + }, + "node_modules/@oxc-project/types": { + "version": "0.155.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.155.0.tgz", + "integrity": "sha512-SeE8JPwW1xs1tHeNycjUtUBdhorRa8o4jlvjwk49R7/Z291i+4DQa7JJIgOm/TaYQZx+QrPQbVJ/9c8dMIZN8Q==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/oxc-project" + } + }, + "node_modules/@playwright/test": { + "version": "1.64.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.64.1.tgz", + "integrity": "sha512-IaYGml/nSOiZWROfPtuIL7neuu9qDOZ3TNvNljUIrhDMxiGTgPjzj/g9p9RxpS2tSepPDRY+UwAaEeiKtUvIAg==", + "dev": true, + "dependencies": { + "playwright": "1.64.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@playwright/test/node_modules/playwright": { + "version": "1.64.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.64.1.tgz", + "integrity": "sha512-HCrVhHr5QQuwYoswEUGTb7gGp522yflbv/ST8nF4sFWGuLjQCt17Q/EubpGGSNKYn1tsTCPZxqHhHCfVURu7UA==", + "dev": true, + "dependencies": { + "playwright-core": "1.64.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@rolldown/binding-android-arm-eabi": { + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.15.tgz", + "integrity": "sha512-aLcgK9gIJ3YRvrrtfSBbRa27F5WrpiTaPulnFpsHPxpUHgFlSgnU4854MHwoRGEBhfRJDWCS2WQ7lIjskE9Olw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.15.tgz", + "integrity": "sha512-xD7DdZCrbThKuxNg7ooQmvp07ys4J/pnSEMTG3/OdOWl6daC67m/Q4fL78sRHMxTH/N4fGUbYhcPM0MF5WFHkg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.15.tgz", + "integrity": "sha512-m2WuQ6r0B4OTYxyPpNLtXaMRbRDUbG8YQV2Q3KTdSSK8Th6KHCnC8jVCJt8bvYeSB5WivkkNbc85mKadaSdOxA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.15.tgz", + "integrity": "sha512-5RtnsHM36nodM9WbMy+zip0LGS/G+y5wro5RHrpOlr4ac24+1zSi9kiV2MP1myZeZ/ifEHfulQcWjnAXzc2C2A==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.15.tgz", + "integrity": "sha512-g3l09pyFqhu+135JKkbkwiKGQmwU5hUMUPJ4VdBADvuqR8d33L6H578rh8m7rtxbdp+VmX/+nzI7nvhSZlzpUQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.15.tgz", + "integrity": "sha512-nIe3VugI97nGQtspPxDqA4sXMUpeiocKIP/CqpY/u/vgbT+alX+cVxu1C/oVBPKg4feNTmYovE91v4DgXvXNwQ==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.15.tgz", + "integrity": "sha512-iTBjXx+B5QSvgI89lJfmqh4wTh8NabR1YZIIN3hNx4zSG0EVfVqkOvLulNpG4251pbd6/IAmZ/LefVk1Y4rPhw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.15.tgz", + "integrity": "sha512-uN26XOnVR7rqQupG3V9oAG2EGGatWjcJHOeDOHUFCh4TBWmP2tHTFoVN13aYbwfm0aHqJAEmcwhAu6sooIjfqQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.15.tgz", + "integrity": "sha512-9nWkcdd+H+XhaA3fQIwpHnKIrfx/PQ6rO0pivr78Og07tXlUlctBBJoEYLPfhTmCR/QxEVdn6uPjHV5SymPhdw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.15.tgz", + "integrity": "sha512-Ggh0ciFjLwIusLSLz7LZYEaGdxOAUJXI+Qu5JCbhPJJHEt8vP+IjRATTJheiJsdzzTYgUiHUiw3X1Ea3ZpMBdA==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.15.tgz", + "integrity": "sha512-FHg60rS6DBePAYZ9X9F67x+JObFdmgn6tupA1C/a3B+CufFmTD4mX+e8M75tCiAO3foR/6xwVBI0KQiI7sn/rw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.15.tgz", + "integrity": "sha512-/cvV/7v5VGz/moiz0XKRj2dE8DzsKyhIu1KpOvJGPjRywg3yscON889UGvAe8zfiJLHup7VLjBduoGWi3HusWQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.15.tgz", + "integrity": "sha512-MyzC5Hrv/sm+Z7TSGKP/j8001RO2xQm1zOpCNVL4j0cY/VhBZviludA2YFQ7i4lH6ZAXxQm0lcMdBzM+99mE7w==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.15.tgz", + "integrity": "sha512-+PgYiS+AH5omy6L45ZLxybasauJaj3M8buoT83D+nzPmCayXlxUV9hydhciCKVyIg0B/v0BtXPDE1uGqNwCPpg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.15.tgz", + "integrity": "sha512-HFZd2QNQYn1JbIi+Hpd7H+r/lkWxcfIu28nirZ6eCn2LafuvmExMlb/P96Sfx5tf7pPArw39IdhmCF8vmV+NhQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true + }, + "node_modules/@tailwindcss/node": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.3.tgz", + "integrity": "sha512-/T8IKEsf9VTU6tLjgC7+sv2mOPtQxzE2jMw7u4Tt40Tx+QSZxpzh95/H6cMKoja9XuW7iMdLJYBB0o9G1CaAgg==", + "dev": true, + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "enhanced-resolve": "^5.24.1", + "jiti": "^2.7.0", + "lightningcss": "1.32.0", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.3.3" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.3.3.tgz", + "integrity": "sha512-krXjAikiaFSPaK/FkAQT5UTx3VormQaiZ5hBFlJZ9UFQGB/rwg1MZIhHAG9smMQRTdyJxP6Qt5MwMtdyU5FWrA==", + "dev": true, + "engines": { + "node": ">= 20" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.3.3", + "@tailwindcss/oxide-darwin-arm64": "4.3.3", + "@tailwindcss/oxide-darwin-x64": "4.3.3", + "@tailwindcss/oxide-freebsd-x64": "4.3.3", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.3", + "@tailwindcss/oxide-linux-arm64-gnu": "4.3.3", + "@tailwindcss/oxide-linux-arm64-musl": "4.3.3", + "@tailwindcss/oxide-linux-x64-gnu": "4.3.3", + "@tailwindcss/oxide-linux-x64-musl": "4.3.3", + "@tailwindcss/oxide-wasm32-wasi": "4.3.3", + "@tailwindcss/oxide-win32-arm64-msvc": "4.3.3", + "@tailwindcss/oxide-win32-x64-msvc": "4.3.3" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.3.3.tgz", + "integrity": "sha512-Y85A2gmPSkl5Ve5qR86GL4HT509cFqQh1aes9p3sSkyTPwt0Pppf3GkwGe4JPACcRYjgJIEhQgM6dBClnr0NYw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.3.tgz", + "integrity": "sha512-BiaWatpBcERQFDlOjRDpIVXuFK5PJez5SA4JMg6VYZdBYU+qKfV/vqjcIs+IYmtitf1xYQZTwXvU/8y4lfZUGw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.3.3.tgz", + "integrity": "sha512-fAeUqfV5ndhxRwai8cXGzdLvul9utWOmeTkv69unv4ZXixjn61Z+p9lCWdwOwA3TYboG3BwdVuN/RDjhBRl0mw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.3.3.tgz", + "integrity": "sha512-iyf5bV6+wnAlflVeEy7R25dupxTNECZN5QMI0qNT6eT+EgaGdZcKhGkr5SdoaWiLJ3spLqIY9VCeSGrwmtg4kw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.3.3.tgz", + "integrity": "sha512-aAYUprJAJQWWbRrPvtjdroZ56Md+JM8pMiopS6xGEwDfLhqj+2ver2p4nU4Mb3CRqcMmNBjo8KkUgcxhkzVQGQ==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.3.3.tgz", + "integrity": "sha512-nDxldcEENOxZRzC2uu9jrutZdAAQtb+8WWDCSnWL1zvBk1+FN+x6MtDViPB5AJMfttVCUhehGWus3XBPgatM/w==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.3.3.tgz", + "integrity": "sha512-Md44bD6veX/PC5iyF8cDVnw4HBIANZepRZZ7a8DQOvkfo5WUBwcp6iAuCUz23u+4SUkhJlD3eL7hNdW8ezd/kA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.3.3.tgz", + "integrity": "sha512-tx7us1muwOKAKWao2v/GaafFeQboE6aj88vC6ziN2NCGcRm8gWUhwjzg+YdVB1e4boAtdtma4L43onunI6NS4w==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.3.3.tgz", + "integrity": "sha512-SJxX60smvHgasZoBy11dX6YRjXJFovwWBoedhbQPOBzgFWBHGB+TVPWB9BxzR7TTxU8FQZAI2AyiNCMzFm8Img==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.3.3.tgz", + "integrity": "sha512-jx1+rPhY/5Ympkktd656HBWEBLxP7dH06losBLjjf5vgCODXvi9KhtftWcMIwTFIDqBr7cRnQkdLnAG+IOlGvQ==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "dev": true, + "optional": true, + "dependencies": { + "@emnapi/core": "^1.11.1", + "@emnapi/runtime": "^1.11.1", + "@emnapi/wasi-threads": "^1.2.2", + "@napi-rs/wasm-runtime": "^1.1.4", + "@tybys/wasm-util": "^0.10.2", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/core": { + "version": "1.11.1", + "integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==", + "dev": true, + "inBundle": true, + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.2", + "tslib": "^2.4.0" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/runtime": { + "version": "1.11.1", + "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==", + "dev": true, + "inBundle": true, + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/wasi-threads": { + "version": "1.2.2", + "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==", + "dev": true, + "inBundle": true, + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.4", + "integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==", + "dev": true, + "inBundle": true, + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@tybys/wasm-util": { + "version": "0.10.2", + "integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==", + "dev": true, + "inBundle": true, + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/tslib": { + "version": "2.8.1", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "inBundle": true, + "optional": true + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.3.tgz", + "integrity": "sha512-3rc292Ca2ceK6Ulcc/bAVnTs/3nDtoPhyEKlgPv+yQJQi/JS/AMJlqzxvlDacL1nekbrcf6bTqp/jV4qgnPxNQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.3.3.tgz", + "integrity": "sha512-yJ0pwIVc/nYeGoV02WtsN8KYyLQv7kyI2wDnkezyJlGGjkd4QLwDGAwl47YpPJeuI0M0ObaXGSPjvWDPeTPggw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/vite": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.3.3.tgz", + "integrity": "sha512-yYU8cogLeSh/ms2jh8Fj7jaba/EWa7Ja6GoUqYZaraEuCI5YS6ms6ObZgjjedm+jm6XZjdNRWBpPP6Z86oOxcw==", + "dev": true, + "dependencies": { + "@tailwindcss/node": "4.3.3", + "@tailwindcss/oxide": "4.3.3", + "tailwindcss": "4.3.3" + }, + "peerDependencies": { + "vite": "^5.2.0 || ^6 || ^7 || ^8" + } + }, + "node_modules/@testing-library/dom": { + "version": "10.4.2", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.2.tgz", + "integrity": "sha512-yzr2S9HyAIdhz2/6qHgbs665Q7PKVcDF05vsOlHPxG1mo36gKVesdYVeDLnXgfjJ03CrKRk08knc6+E/9m8v2Q==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.3.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "picocolors": "1.1.1", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@testing-library/dom/node_modules/aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/@testing-library/dom/node_modules/dom-accessibility-api": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@testing-library/jest-dom": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-7.0.1.tgz", + "integrity": "sha512-oMDTC3oA+6CXSO2JZnvOI7CA6oVub6kij5ggk9ohwye5slmkwxYDXcPOVxgMw/RQlticjtO0C1RZkR97HgrWMw==", + "dev": true, + "dependencies": { + "@adobe/css-tools": "^4.4.0", + "aria-query": "^5.0.0", + "css.escape": "^1.5.1", + "dom-accessibility-api": "^0.6.3", + "picocolors": "^1.1.1", + "redent": "^3.0.0" + }, + "engines": { + "node": ">=22", + "npm": ">=6", + "yarn": ">=1" + }, + "peerDependencies": { + "@testing-library/dom": ">=10 <11", + "vitest": ">= 0.32" + }, + "peerDependenciesMeta": { + "vitest": { + "optional": true + } + } + }, + "node_modules/@testing-library/react": { + "version": "16.3.3", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.3.tgz", + "integrity": "sha512-Uo193NgQbPMz6lrrhtRQQFcMC6Re/ELLFbbuVL30WDlZxlpZf9/lMHTAVxPRLw1q1iu9OJmR1c2BLiENRstdBg==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@testing-library/dom": "^10.0.0", + "@types/react": "^18.0.0 || ^19.0.0", + "@types/react-dom": "^18.0.0 || ^19.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@testing-library/user-event": { + "version": "14.6.8", + "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.8.tgz", + "integrity": "sha512-vgSSqnANMnkcYR/vUNDbwS7aM3OErRLNUG8vCVVlj4oc6cKffUrccX7UAS5AmJQQNZTSibi9z5GvvRZ0aYgUCQ==", + "dev": true, + "engines": { + "node": ">=12", + "npm": ">=6" + }, + "peerDependencies": { + "@testing-library/dom": ">=7.21.4" + } + }, + "node_modules/@types/aria-query": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", + "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true + }, + "node_modules/@types/esrecurse": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", + "integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==", + "dev": true + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true + }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "optional": true + }, + "node_modules/@vitejs/plugin-react": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.1.2.tgz", + "integrity": "sha512-fhJNGUdscQa4HkI0xFxwonWyhnVReCPDbXI7FH7jmzpnVCrFQI7PRrhWecO263610vOrs9tr2Y4xcf8uAdFxbw==", + "dev": true, + "dependencies": { + "@rolldown/pluginutils": "^1.0.1" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", + "babel-plugin-react-compiler": "^1.0.0", + "oxc-transform-react": "^0.152.0", + "vite": "^8.0.0" + }, + "peerDependenciesMeta": { + "@rolldown/plugin-babel": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + }, + "oxc-transform-react": { + "optional": true + } + } + }, + "node_modules/@vitest/mocker": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-5.0.5.tgz", + "integrity": "sha512-xLL3lkDegJZw+MGoGaCCsFayDZthGgpAtr3Vziyfc4p7xH89K7KRPcRBBh/kEYm0Aqkt5DtTmkYpBtRV/FrHZg==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "0.3.31", + "@vitest/spy": "5.0.5", + "estree-walker": "^3.0.3", + "magic-string": "^1.4.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9 || ^3.0.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/mocker/node_modules/magic-string": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-1.4.3.tgz", + "integrity": "sha512-z12OxmaPGE0F4xlpGdjuAUckipLpQlTAuAmyGhNoD7ODpYUzvDfvtxUbjM/jwznyP178oju/KDdyi220wNJ0RQ==", + "dev": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.6.0" + } + }, + "node_modules/@vitest/spy": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-5.0.5.tgz", + "integrity": "sha512-lwJRExuH417257Q5wv9QYugN9IMJkoxcrKrYxGPQkBzl7wlNpTUxD/5yNX4ESKVUIShKPgkGchYPFMWOOk0wzw==", + "dev": true, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/acorn": { + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.19.0.tgz", + "integrity": "sha512-oJlA3XiRm7Cyk6qFD2Jn8ak9B7jSy0qy00ADO3+8dpT0LSjFihQYv4C02LFCSYJV3Q37xYwwRy5m+IpIiUzqWw==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/aria-query": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", + "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "node_modules/axios": { + "version": "1.20.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.20.0.tgz", + "integrity": "sha512-r8aOh8j9cGKpgQAqpzrUHnSIc6a59Y3Xf/cv8sy1DrHCkZHzQGEuoq1tARk6qSyDdtQGSDgpb9kFlruzPvrgwg==", + "dependencies": { + "follow-redirects": "^1.16.0", + "form-data": "^4.0.6", + "https-proxy-agent": "^5.0.1", + "proxy-from-env": "^2.1.0" + } + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.11.31", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.31.tgz", + "integrity": "sha512-b0k7wXlWjpuL/Uxb/pYlpjY0ej5BjAmurtPUk3eRGuloI4yjK32m8TiKUQ81l1ptcLjKS/KH2+uqIolZti1x4Q==", + "dev": true, + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/bidi-js": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.1.0.tgz", + "integrity": "sha512-fX1Onk0tdVPC7obPWB5EbJ1z7NVhLq4m2xZLq2YXBkxzMXIGRpNMU88n0EPgWseKl12J7zXs7qrDxPK4sRs2fg==", + "dev": true, + "dependencies": { + "require-from-string": "^2.0.2" + } + }, + "node_modules/brace-expansion": { + "version": "5.0.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.12.tgz", + "integrity": "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==", + "dev": true, + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/browserslist": { + "version": "4.29.4", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.29.4.tgz", + "integrity": "sha512-2TcbbiwXEq+N0kD36tykpCfyyQigUXmuwNQbnPp4Xk4PX9JOqVIKjJlIDjpH7uY6tmRGoAd+apeT1p60T8wsdA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "baseline-browser-mapping": "^2.11.29", + "caniuse-lite": "^1.0.30001816", + "electron-to-chromium": "^1.5.459", + "node-releases": "^2.0.59", + "update-browserslist-db": "^1.3.4" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/cacheable": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/cacheable/-/cacheable-2.5.0.tgz", + "integrity": "sha512-60cyAOytib/OzBw1JNSoSV/boK1AtHryDIjvVBk7XbN4ugfkM3+Sry7fEjNgPMGgOjuaZPAp8ruZ0Cxafwyq9g==", + "dev": true, + "dependencies": { + "@cacheable/memory": "^2.2.0", + "@cacheable/utils": "^2.5.0", + "hookified": "^1.15.0", + "keyv": "^5.6.0", + "qified": "^0.10.1" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001819", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001819.tgz", + "integrity": "sha512-LFUqIIAwpzsIuevnKnsR9qnStBcgzPShTX61lBwN9uFkkf8o8zLiebEjbW8AYtZiF+1YKDWfdt/pWDtMuG8Xwg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ] + }, + "node_modules/chai": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.3.0.tgz", + "integrity": "sha512-XWAtwJ6OHO+tj0EKCs0Y2UamnyOxseZWltU4x2U2wh8g4AigdjwvtUjvLP2tqkA/avxHEtzxNaqGq/YGNwckKg==", + "dev": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/css-tree": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz", + "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==", + "dev": true, + "dependencies": { + "mdn-data": "2.27.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, + "node_modules/css.escape": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", + "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", + "dev": true + }, + "node_modules/data-urls": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-8.0.0.tgz", + "integrity": "sha512-JQUCCK+/CAGbGGx618PlFZMXH81Jtj7t73CHv9dd/zVWvxgGT2zLFRjnKb+Ng46GIwcy+f7f88qd4duqllK+WQ==", + "dev": true, + "dependencies": { + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^17.1.2" + }, + "engines": { + "node": "^22.14.0 || >=24.0.0" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "dev": true + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/dom-accessibility-api": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", + "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", + "dev": true + }, + "node_modules/dompurify": { + "version": "3.4.17", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.17.tgz", + "integrity": "sha512-B/pSlYVHQ58kqISD/yDjuJ1yfzI34m1gF4lAod2bwSRhiN4K5Cl3H8Af7hqB2PmTL911tdEudnnXn5FnhRoz0w==", + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.463", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.463.tgz", + "integrity": "sha512-nVWNU6ltFjopvOdD44J4zoBkgO0M+WZiFKKFV7EjebOH9z0KNphk6bCEjplXtJofL21ARIuuwWOCsFkBVklm6w==", + "dev": true + }, + "node_modules/enhanced-resolve": { + "version": "5.26.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.26.0.tgz", + "integrity": "sha512-9vhedylFonb2YGogzUKX6+Ja72gOJbN1QHAqdrvqLwhdl/QWbKopzoUC9EbQNVsAns/bx/4uyqalrQAoy1IByw==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/entities": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-8.1.0.tgz", + "integrity": "sha512-kxL7msIffSuh9aaFAMD7rxAIuTRMAHMeBtgHW2yUdWw732ZNh4MehkF2gdjvtdmikkaIP9bFDDJOPlsvm7avrA==", + "dev": true, + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.2.tgz", + "integrity": "sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==", + "dev": true + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-toolkit": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.52.0.tgz", + "integrity": "sha512-XTNEJQh1tY1ZJVcf6ayP/2n4ZPyaHlW2FWs7xvw5ddPuhUVjLD3olQVQS7kf58JbAB48iL0uL/jerTrjtV3lDA==" + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "10.13.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.13.0.tgz", + "integrity": "sha512-UohDjD4zSzEwhf58M3O7vc/ycc8O1I7dNlhtD0WxE8Px8EHNJKtUwzBqrzeGhisRuv2ag4sAYswszCJRgKXYUg==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.2", + "@eslint/config-array": "^0.23.5", + "@eslint/config-helpers": "^0.7.0", + "@eslint/core": "^1.2.1", + "@eslint/plugin-kit": "^0.7.3", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^9.1.2", + "eslint-visitor-keys": "^5.0.1", + "espree": "^11.2.0", + "esquery": "^1.7.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "11.1.5 || >11.1.6 <12", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "minimatch": "^10.2.5", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.1.1.tgz", + "integrity": "sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g==", + "dev": true, + "dependencies": { + "@babel/core": "^7.24.4", + "@babel/parser": "^7.24.4", + "hermes-parser": "^0.25.1", + "zod": "^3.25.0 || ^4.0.0", + "zod-validation-error": "^3.5.0 || ^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0" + } + }, + "node_modules/eslint-scope": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz", + "integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==", + "dev": true, + "dependencies": { + "@types/esrecurse": "^4.3.1", + "@types/estree": "^1.0.8", + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz", + "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==", + "dev": true, + "dependencies": { + "acorn": "^8.16.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^5.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expect-type": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", + "dev": true, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/file-entry-cache": { + "version": "11.1.5", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-11.1.5.tgz", + "integrity": "sha512-+PFTHITI08JIGhnNpGNI8T8inUpgZfk3GNEqfT9R2zZV2iFXg3CvqzSl/uEhs7TSGujYRELEANyDvS8Fj7+S7Q==", + "dev": true, + "dependencies": { + "flat-cache": "^6.1.23" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "6.1.23", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-6.1.23.tgz", + "integrity": "sha512-f++BY9pTk+983xK1FLzlLpmM0i0z+jHmx3QESGkURMXujQZz1k5wzwX6hjnQ8goaD0B+sYnDK1yZ6MTyZfUaqA==", + "dev": true, + "dependencies": { + "cacheable": "^2.5.0", + "flatted": "^3.4.2", + "hookified": "^1.15.0" + } + }, + "node_modules/flatted": { + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.4.tgz", + "integrity": "sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==", + "dev": true + }, + "node_modules/follow-redirects": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.1.tgz", + "integrity": "sha512-FNvFGzoMLWmE6Yj9spb/zjd7yiNCHiAW9/Tg9CXrQ8wuu32HtlJOwWO11OJafl5FfY3DxTdQ0vj42zU1kvv5jg==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", + "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.4", + "mime-types": "^2.1.35" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "17.13.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-17.13.0.tgz", + "integrity": "sha512-RwMTC61u7hrG3ZJMkZQOK4JLJrKS8QtoTii63EmWvFXHqycY9FObBJQCbsLxSO8kjKhWE5kV1GC+Vb1g3RI0Zg==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hashery": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/hashery/-/hashery-1.5.1.tgz", + "integrity": "sha512-iZyKG96/JwPz1N55vj2Ie2vXbhu440zfUfJvSwEqEbeLluk7NnapfGqa7LH0mOsnDxTF85Mx8/dyR6HfqcbmbQ==", + "dev": true, + "dependencies": { + "hookified": "^1.15.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hermes-estree": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", + "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", + "dev": true + }, + "node_modules/hermes-parser": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz", + "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", + "dev": true, + "dependencies": { + "hermes-estree": "0.25.1" + } + }, + "node_modules/hookified": { + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/hookified/-/hookified-1.15.1.tgz", + "integrity": "sha512-MvG/clsADq1GPM2KGo2nyfaWVyn9naPiXrqIe4jYjXNZQt238kWyOGrsyc/DmRAQ+Re6yeo6yX/yoNCG5KAEVg==", + "dev": true + }, + "node_modules/html-encoding-sniffer": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-7.0.0.tgz", + "integrity": "sha512-UikN5yr7xsCDAq87Or5or0PAlD3HJJOKVzM05az588WnpDJ4Ux7a2A53Qi6gofGg2/EtvF/H4hCi/TXfCW4Y6w==", + "dev": true, + "dependencies": { + "@exodus/bytes": "^1.15.1" + }, + "engines": { + "node": "^22.13.0 || >=24.0.0" + } + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "node_modules/jiti": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", + "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", + "dev": true, + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, + "node_modules/jsdom": { + "version": "30.1.3", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-30.1.3.tgz", + "integrity": "sha512-hQMUPvRgN7KxsfIytDQji7bimuJeRQa6Qo5+iuiZQk5CafeRxLIA10/cG5YCBFNpD9RO3oqIiN4CO1Y44kxqXg==", + "dev": true, + "dependencies": { + "@asamuzakjp/css-color": "^7.1.3", + "@asamuzakjp/dom-selector": "^10.0.3", + "@bramus/specificity": "^2.4.2", + "@csstools/css-syntax-patches-for-csstree": "^1.1.15", + "@exodus/bytes": "^1.16.0", + "css-tree": "^3.2.1", + "data-urls": "^8.0.0", + "decimal.js": "^10.6.0", + "html-encoding-sniffer": "^7.0.0", + "is-potential-custom-element-name": "^1.0.1", + "lru-cache": "^11.5.3", + "parse5": "^8.0.1", + "saxes": "^6.0.0", + "tough-cookie": "^6.0.2", + "undici": "^8.12.1", + "w3c-xmlserializer": "^6.0.0", + "webidl-conversions": "^8.0.1", + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^17.2.0", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": "^22.22.2 || ^24.15.0 || >=26.0.0" + }, + "peerDependencies": { + "canvas": "^3.2.3" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jsdom/node_modules/lru-cache": { + "version": "11.5.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.3.tgz", + "integrity": "sha512-U4N8FgzmWxc8k1VH8Kr6lQg18U7Fjvby6wXHVRX/ZZ7IwWbRMgrRbP0Wrb5q5NVinryp4SQampHKdvtecItxUg==", + "dev": true, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/keyv": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.6.0.tgz", + "integrity": "sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw==", + "dev": true, + "dependencies": { + "@keyv/serialize": "^1.1.1" + } + }, + "node_modules/laravel-precognition": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/laravel-precognition/-/laravel-precognition-2.1.1.tgz", + "integrity": "sha512-Wg05+puJ5qiNsE8rdO5JmSiqQ2xcEf7442AIZiDhZg1JyPOePv5SSCu0fO7z7LqmJcPMxNNuOIcZd6UatEmYug==", + "dependencies": { + "es-toolkit": "^1.32.0" + }, + "peerDependencies": { + "axios": "^1.4.0" + }, + "peerDependenciesMeta": { + "axios": { + "optional": true + } + } + }, + "node_modules/laravel-vite-plugin": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/laravel-vite-plugin/-/laravel-vite-plugin-3.2.0.tgz", + "integrity": "sha512-xSxY9Gzeb/eancd8WeK09piAFP+a6i5QIBqNCKNv9L0Eq6wziwzSem7F1GvMSrtjMh5F/QVKFxn8t9naGOA66A==", + "dev": true, + "dependencies": { + "picocolors": "^1.0.0", + "tinyglobby": "^0.2.12", + "vite-plugin-full-reload": "^1.1.0" + }, + "bin": { + "clean-orphaned-assets": "bin/clean.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "fontaine": "^0.8.0", + "vite": "^8.0.0" + }, + "peerDependenciesMeta": { + "fontaine": { + "optional": true + } + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "lz-string": "bin/bin.js" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mdn-data": { + "version": "2.27.1", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz", + "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==", + "dev": true + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/minimatch": { + "version": "10.2.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", + "dev": true, + "dependencies": { + "brace-expansion": "^5.0.8" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/nanoid": { + "version": "3.3.20", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.20.tgz", + "integrity": "sha512-uKdg2G3GNCKQn9byYOpxbGqrT2fGO5KRt5J/8b3pok8rT6qxGWF6hxMyJiEYtAf+FVyYuD9hRaDqX5uPFYJ4ZQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true + }, + "node_modules/node-releases": { + "version": "2.0.60", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.60.tgz", + "integrity": "sha512-OmO8+GM1JUZqXET720VrJCwkjUCXmCMQm0Z3UDDRbfNLHS4NEAvY60j5jiFv0tkKw2PNNlqOLhahVmwJIOLDIQ==", + "dev": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/obug": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/obug/-/obug-3.0.0.tgz", + "integrity": "sha512-5vvB5+W7ePv+p3uqxi+RcW1XAzLW0/hxt3/4X4Lc4qHudzOhmBiBwOY6DRob4WnanAEGvNcLjF+KNOufrUoEQw==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parse5": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.1.tgz", + "integrity": "sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==", + "dev": true, + "dependencies": { + "entities": "^8.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true + }, + "node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/playwright-core": { + "version": "1.64.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.64.1.tgz", + "integrity": "sha512-BC4s8y6SEVA6Q1rSxZTto8hI1KUI3xXlHGZgvGqP4IqPdI/a6FX9H29IDNz7vKo9l21b6LfYosScPL6M3xxyDQ==", + "dev": true, + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/postcss": { + "version": "8.5.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.31.tgz", + "integrity": "sha512-YUbWYCgErwnMAVkjUnYa+KlMBzGpHwiIt/DE7f0G33AuX2LH9UcTk6KgWxd3zRU+pN3u/C+Xl3EnMLM4Hea7Sg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.20", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/pretty-format/node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/proxy-from-env": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", + "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", + "engines": { + "node": ">=10" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/qified": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/qified/-/qified-0.10.1.tgz", + "integrity": "sha512-+Owyggi9IxT1ePKGafcI87ubSmxol6smwJ+RAHDQlx9+9cPwFWDiKFFCPuWhr9ignlGpZ9vDQLw67N4dcTVFEA==", + "dev": true, + "dependencies": { + "hookified": "^2.1.1" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/qified/node_modules/hookified": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/hookified/-/hookified-2.2.0.tgz", + "integrity": "sha512-p/LgFzRN5FeoD3DLS6bkUapeye6E4SI6yJs6KetENd18S+FBthqYq2amJUWpt5z0EQwwHemidjY5OqJGEKm5uA==", + "dev": true + }, + "node_modules/react": { + "version": "19.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-19.3.1.tgz", + "integrity": "sha512-QNhWdt7nrgVWWQG58QuGsAR3jv3Hbe4mb4vz7w2csnHavciZJgda1CPNaldF6eVxv90ud3jUXpKlHL69gO38yw==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.3.1.tgz", + "integrity": "sha512-Y0bARz53lhEC1NMCASs0/j+faB/zqt3R8hpvg1e8+61ZVJ4UL0UdC4cBbXfZMcIuhrfgeILTLe+ZeBCavQPtKQ==", + "dependencies": { + "scheduler": "^0.28.0" + }, + "peerDependencies": { + "react": "^19.3.1" + } + }, + "node_modules/redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/rolldown": { + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.15.tgz", + "integrity": "sha512-6odND+fel5zVw3I4Yc2e69gvguDnX0CAAGiNDZSq4ik5WUcaLrkD2GLuog0XcODbor31hZ5ayEMgVDno+KPV5g==", + "dev": true, + "dependencies": { + "@oxc-project/types": "=0.155.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm-eabi": "1.2.15", + "@rolldown/binding-android-arm64": "1.2.15", + "@rolldown/binding-darwin-arm64": "1.2.15", + "@rolldown/binding-darwin-x64": "1.2.15", + "@rolldown/binding-freebsd-x64": "1.2.15", + "@rolldown/binding-linux-arm-gnueabihf": "1.2.15", + "@rolldown/binding-linux-arm64-gnu": "1.2.15", + "@rolldown/binding-linux-arm64-musl": "1.2.15", + "@rolldown/binding-linux-ppc64-gnu": "1.2.15", + "@rolldown/binding-linux-s390x-gnu": "1.2.15", + "@rolldown/binding-linux-x64-gnu": "1.2.15", + "@rolldown/binding-linux-x64-musl": "1.2.15", + "@rolldown/binding-openharmony-arm64": "1.2.15", + "@rolldown/binding-win32-arm64-msvc": "1.2.15", + "@rolldown/binding-win32-x64-msvc": "1.2.15" + } + }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, + "node_modules/scheduler": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.28.0.tgz", + "integrity": "sha512-juorfCmIkIw8tT+p5BXSm6PJjQF/ycEYmKyzURCIt/RaZIhL+PulbQ9Yu2z1HdOJDdqDTlxA1+xKBmHXJsczAw==" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map-js": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.2.tgz", + "integrity": "sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/std-env": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.3.0.tgz", + "integrity": "sha512-OtU/EgQ1kIm5KwqQpBC6ZEMXrZRui11w8zgfTWp8cdO9B8OaPsbA8bTHO2P+HNo1VlUTGMVBwPhydu6poeXiag==", + "dev": true + }, + "node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tailwindcss": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.3.tgz", + "integrity": "sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ==", + "dev": true + }, + "node_modules/tapable": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", + "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", + "dev": true, + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/tinybench": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-6.2.2.tgz", + "integrity": "sha512-nIZ2N2dFpjBsg2hgitKu7mA+FvsIJuGJOwysRngmVRRmWgR/UyPjwJj8c/fT/dhm8EkHnF5EOogRf+0Z2qrimA==", + "dev": true, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/tinyexec": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.3.1.tgz", + "integrity": "sha512-GCvB3aoys96IuDFBMcTB46JOR6mdMtAToqwiW8JlWhsoh1mhHi/xn9ss/Dg7N555GiJyEt2qzoG/NHCwM6h1EA==", + "dev": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tldts": { + "version": "7.4.19", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.4.19.tgz", + "integrity": "sha512-48dH6aSpQnpbzDUc4vIQZ1VjB8tkK+e8c1yx05/SZK+rkssgjEOlxDd/yU8/a4vGYQ9vRgJr9dwMq9TJ0b3X2Q==", + "dev": true, + "dependencies": { + "tldts-core": "^7.4.19" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "7.4.19", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.4.19.tgz", + "integrity": "sha512-goNXV6Ho9TIOfpJZanTd6CKG5DyYgYAL+k+22T0SLh8l6+KHIfBUPFuePuSZv05UttIaaLmI9tb7TmGIdK5AkQ==", + "dev": true + }, + "node_modules/tough-cookie": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.2.tgz", + "integrity": "sha512-exgYmnmL/sJpR3upZfXG5PoatXQii55xAiXGXzY+sROLZ/Y+SLcp9PgJNI9Vz37HpQ74WvDcLT8eqm+kV3FzrA==", + "dev": true, + "dependencies": { + "tldts": "^7.0.5" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tr46": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-7.0.0.tgz", + "integrity": "sha512-BcDSDW+SIGWVOns1Zm6Yg91q9AEj7eaID0QDY9oIqJD1A8dscNwMcl9Sms2DeuWCNov1X0/oXanHGYQBSLwZsg==", + "dev": true, + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": "^22.14.0 || >=24.0.0" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/undici": { + "version": "8.12.3", + "resolved": "https://registry.npmjs.org/undici/-/undici-8.12.3.tgz", + "integrity": "sha512-+fdndUTwBP86cVjihTp21hZHo7d6oj1c/47XPA3lL8mM0XHs+C4EkcjmgRsFnFk2f/UONGDy9ItBItmXsLOQHw==", + "dev": true, + "engines": { + "node": ">=22.19.0" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.4.tgz", + "integrity": "sha512-t8wonIOdQyOjtPd2VktlBu6QjTVTks6v8bSi7kd0r2Vll8TK1JYDZtRRdbmCHOAMphkZgrzK+L+vXK4pyZ8t4A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/vite": { + "version": "8.3.5", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.3.5.tgz", + "integrity": "sha512-z1YpjieJ9g0ebTmQpLBv/oq9E3F9EIlOe+7LahW8SrE3LTiG3udBRUXQOlYXP9b+IC2q2phHeVxj9xGlw34OEQ==", + "dev": true, + "dependencies": { + "lightningcss": "^1.33.0", + "picomatch": "^4.0.7", + "postcss": "^8.5.30", + "rolldown": "~1.2.14", + "tinyglobby": "^0.2.17" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.7.1 || ^0.8.0", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite-plugin-full-reload": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/vite-plugin-full-reload/-/vite-plugin-full-reload-1.2.0.tgz", + "integrity": "sha512-kz18NW79x0IHbxRSHm0jttP4zoO9P9gXh+n6UTwlNKnviTTEpOlum6oS9SmecrTtSr+muHEn5TUuC75UovQzcA==", + "dev": true, + "dependencies": { + "picocolors": "^1.0.0", + "picomatch": "^2.3.1" + } + }, + "node_modules/vite-plugin-full-reload/node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/vite/node_modules/lightningcss": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", + "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", + "dev": true, + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.33.0", + "lightningcss-darwin-arm64": "1.33.0", + "lightningcss-darwin-x64": "1.33.0", + "lightningcss-freebsd-x64": "1.33.0", + "lightningcss-linux-arm-gnueabihf": "1.33.0", + "lightningcss-linux-arm64-gnu": "1.33.0", + "lightningcss-linux-arm64-musl": "1.33.0", + "lightningcss-linux-x64-gnu": "1.33.0", + "lightningcss-linux-x64-musl": "1.33.0", + "lightningcss-win32-arm64-msvc": "1.33.0", + "lightningcss-win32-x64-msvc": "1.33.0" + } + }, + "node_modules/vite/node_modules/lightningcss-android-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", + "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-darwin-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", + "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-darwin-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", + "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-freebsd-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", + "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", + "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", + "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-linux-arm64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", + "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-linux-x64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", + "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-linux-x64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", + "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", + "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-win32-x64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", + "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vitest": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-5.0.5.tgz", + "integrity": "sha512-cPODOMF8YnkiiYtDdHo6n4wWmBrOBBbOIYH2d33M4l3CdNn72h+qMmO6uXffUN6A5AZFXNND/d+krS9mgkXRfg==", + "dev": true, + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/mocker": "5.0.5", + "chai": "^6.3.0", + "es-module-lexer": "^2.3.2", + "expect-type": "^1.4.0", + "magic-string": "^1.4.3", + "obug": "^3.0.0", + "picomatch": "^4.0.7", + "std-env": "^4.3.0", + "tinybench": "^6.2.2", + "tinyexec": "^1.3.1", + "tinyglobby": "^0.2.17", + "why-is-node-running": "3.2.1" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^22.12.0 || ^24.0.0 || >=26.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "5.0.5", + "@vitest/browser-preview": "5.0.5", + "@vitest/browser-webdriverio": "^5.0.0-beta.5 || >=5.0.0", + "@vitest/coverage-istanbul": "5.0.5", + "@vitest/coverage-v8": "5.0.5", + "@vitest/ui": "5.0.5", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.4.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "vite": { + "optional": false + } + } + }, + "node_modules/vitest/node_modules/magic-string": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-1.4.3.tgz", + "integrity": "sha512-z12OxmaPGE0F4xlpGdjuAUckipLpQlTAuAmyGhNoD7ODpYUzvDfvtxUbjM/jwznyP178oju/KDdyi220wNJ0RQ==", + "dev": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.6.0" + } + }, + "node_modules/w3c-xmlserializer": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-6.0.0.tgz", + "integrity": "sha512-4Nsy8K5Tr6SPDH9jhKJOHf7ChDrc1zufZTVSF7x72hwuEXBqxqk9G6cK+K2NRUtB3iELRJqjXb4JPDMBjMTl2Q==", + "dev": true, + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": "^22.22.2 || ^24.15.0 || >=26.0.0" + } + }, + "node_modules/webidl-conversions": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz", + "integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==", + "dev": true, + "engines": { + "node": ">=20" + } + }, + "node_modules/whatwg-mimetype": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz", + "integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==", + "dev": true, + "engines": { + "node": ">=20" + } + }, + "node_modules/whatwg-url": { + "version": "17.3.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-17.3.0.tgz", + "integrity": "sha512-KwOAJrpf+GTgBvcBQJcP38sMF0DsQKdwtG+TxM2pW44P6E5k0hDbWrDmyuJDIF0Uef0aQq0ZoJs+5oI+k8ddhg==", + "dev": true, + "dependencies": { + "@exodus/bytes": "^1.16.0", + "tr46": "^7.0.0", + "webidl-conversions": "^8.0.1" + }, + "engines": { + "node": "^22.14.0 || >=24.0.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/why-is-node-running": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-3.2.1.tgz", + "integrity": "sha512-Tb2FUhB4vUsGQlfSquQLYkApkuPAFQXGFzxWKHHumVz2dK+X1RUm/HnID4+TfIGYJ1kTcwOaCk/buYCEJr6YjQ==", + "dev": true, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=20.11" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "4.6.7", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.6.7.tgz", + "integrity": "sha512-g8W2D2kU6a8untc053fX1uVBKzM7yqAxW/3zwk83+6RyF2+SKOuV+c0wBDhUwzV1PaGapmZH+rmcUzPf+3CGwg==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-validation-error": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz", + "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==", + "dev": true, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..f30f52f --- /dev/null +++ b/package.json @@ -0,0 +1,37 @@ +{ + "$schema": "https://json.schemastore.org/package.json", + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build && vite build --ssr", + "build:client": "vite build", + "test": "vitest run", + "test:e2e": "playwright test", + "lint": "eslint resources/js" + }, + "dependencies": { + "@inertiajs/react": "^3.9.1", + "axios": "^1.20.0", + "dompurify": "^3.4.17", + "react": "^19.3.1", + "react-dom": "^19.3.1" + }, + "devDependencies": { + "@eslint/js": "^10.0.1", + "@playwright/test": "^1.64.1", + "@tailwindcss/vite": "^4.3.3", + "@testing-library/jest-dom": "^7.0.1", + "@testing-library/react": "^16.3.3", + "@testing-library/user-event": "^14.6.8", + "@vitejs/plugin-react": "^6.1.2", + "eslint": "^10.13.0", + "eslint-plugin-react-hooks": "^7.1.1", + "globals": "^17.13.0", + "jsdom": "^30.1.3", + "laravel-vite-plugin": "^3.2.0", + "tailwindcss": "^4.3.3", + "vite": "^8.3.5", + "vitest": "^5.0.5" + } +} diff --git a/playwright.config.js b/playwright.config.js new file mode 100644 index 0000000..01fa6a4 --- /dev/null +++ b/playwright.config.js @@ -0,0 +1,27 @@ +import { defineConfig, devices } from '@playwright/test'; + +const port = Number(process.env.E2E_PORT || 8123); + +export default defineConfig({ + testDir: './e2e', + timeout: 60_000, + retries: process.env.CI ? 1 : 0, + workers: 1, + reporter: process.env.CI ? [['list'], ['html', { open: 'never' }]] : 'list', + use: { + baseURL: `http://127.0.0.1:${port}`, + trace: 'retain-on-failure', + screenshot: 'only-on-failure', + }, + projects: [ + { name: 'desktop', use: { ...devices['Desktop Chrome'] } }, + { name: 'phone', use: { ...devices['Pixel 7'] }, grep: /@phone/ }, + ], + webServer: { + command: './scripts/e2e-server.sh', + url: `http://127.0.0.1:${port}/up`, + reuseExistingServer: !process.env.CI, + timeout: 120_000, + env: { E2E_PORT: String(port) }, + }, +}); diff --git a/resources/css/app.css b/resources/css/app.css new file mode 100644 index 0000000..f6f9b90 --- /dev/null +++ b/resources/css/app.css @@ -0,0 +1,67 @@ +@import 'tailwindcss'; + +/* Pages and components are scanned for utility classes. */ +@source '../js'; +@source '../views'; + +/* `dark:` follows the document's data-theme (the header toggle, else the OS preference). */ +@custom-variant dark (&:where([data-theme='dark'], [data-theme='dark'] *)); + +/* The design system's tokens as Tailwind colours, radii and fonts, e.g. bg-surface, text-muted. */ +@theme inline { + --font-sans: var(--font); + --font-mono: var(--mono); + --color-bg: var(--bg); + --color-bg-tint: var(--bg-tint); + --color-surface: var(--surface); + --color-surface-2: var(--surface-2); + --color-raised: var(--raised); + --color-line: var(--line); + --color-line-strong: var(--line-strong); + --color-ink: var(--ink); + --color-ink-2: var(--ink-2); + --color-muted: var(--muted); + --color-accent: var(--accent); + --color-accent-soft: var(--accent-soft); + --color-accent-line: var(--accent-line); + --color-ok: var(--ok); + --color-wait: var(--wait); + --color-you: var(--you); + --color-fail: var(--fail); + --radius-card: var(--r-lg); +} + +/* The Waypoints component classes (.btn, .card, .st, .route…) sit in the components layer so a + utility on the same element wins. */ +@import './registry.css' layer(components); + +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { transition: none !important; animation: none !important; } +} + +.theme-fade, .theme-fade * { + transition: background-color 200ms, border-color 200ms, color 200ms; +} + +@keyframes spin { to { transform: rotate(360deg); } } +.spin { animation: spin 1.2s linear infinite; } + +.sr-only { + position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0; +} + +/* Publisher-written rich text */ +.prose p { margin: 0 0 12px; } +.prose p:last-child { margin-bottom: 0; } +.prose ul, .prose ol { margin: 0 0 12px; padding-left: 22px; } +.prose li { margin: 4px 0; } +.prose h1, .prose h2, .prose h3 { margin: 16px 0 8px; font-size: 16px; font-weight: 600; color: var(--ink); } +.prose strong { color: var(--ink); } +.prose a { text-decoration: underline; } +.prose code { padding: 1px 5px; border-radius: 5px; background: var(--bg-tint); } + +/* At phone width a page's side rail (install, readiness, decision) comes first when it carries + the page's main action. */ +@media (max-width: 760px) { + .rail-first { order: -1; } +} diff --git a/resources/css/registry.css b/resources/css/registry.css new file mode 100644 index 0000000..c599c81 --- /dev/null +++ b/resources/css/registry.css @@ -0,0 +1,333 @@ +/* Fleetbase Extensions Registry: the "Waypoints" design system (from the design canvas). + Tokens per theme on ; Tailwind exposes them as colours in app.css. */ + +:root { + --font: 'Inter', ui-sans-serif, system-ui, -apple-system, 'Segoe UI', sans-serif; + --mono: 'JetBrains Mono', ui-monospace, 'SFMono-Regular', Menlo, monospace; + + --blue-100: #e6f0fb; --blue-200: #bad5f5; --blue-300: #8dbbef; --blue-400: #61a0e8; --blue-500: #3485e2; + --blue-600: #1c6cc7; --blue-700: #16539a; --blue-800: #103b6d; --blue-900: #092341; + --navy-100: #0d2f57; --navy-200: #092340; --navy-300: #06172a; --navy-400: #030b14; + + --r-sm: 6px; --r-md: 10px; --r-lg: 16px; --r-xl: 24px; --r-full: 999px; + +} + +body { + margin: 0; + font-family: var(--font); + font-size: 15px; + line-height: 1.55; + color: var(--ink); + background: var(--bg); + min-height: 100vh; + -webkit-font-smoothing: antialiased; + font-feature-settings: 'cv11', 'ss01'; +} + +:root, [data-theme='light'] { + --bg: #f5f8fc; --bg-tint: #edf3fa; --surface: #ffffff; --surface-2: #f8fafd; --raised: #ffffff; + --line: #e1e8f1; --line-strong: #c8d5e5; + --ink: #07182b; --ink-2: #33445b; --muted: #56677d; + --accent: #1c6cc7; --accent-hover: #16539a; --accent-ink: #ffffff; --accent-soft: #e6f0fb; --accent-line: #bad5f5; + --brand: #3485e2; --focus: #3485e2; + --ok: #0f6b3a; --ok-bg: #e2f4e9; --ok-line: #a9dcbc; + --wait: #16539a; --wait-bg: #e6f0fb; --wait-line: #bad5f5; + --you: #8a4a00; --you-bg: #fff0d9; --you-line: #f4c98a; + --fail: #b42318; --fail-bg: #fde9e7; --fail-line: #f4b4ad; + --idle: #4a5a70; --idle-bg: #edf1f6; --idle-line: #d3dce8; + --code-bg: #0b1d33; --code-ink: #e6f0fb; --code-muted: #8dbbef; + --e1: 0 1px 2px rgba(7, 24, 43, 0.06); + --e2: 0 1px 2px rgba(7, 24, 43, 0.04), 0 10px 28px -12px rgba(7, 24, 43, 0.16); + --e3: 0 30px 80px -20px rgba(7, 24, 43, 0.35); + --scrim: rgba(6, 23, 42, 0.45); + --route: #bad5f5; +} + +[data-theme='dark'] { + --bg: #06172a; --bg-tint: #071c33; --surface: #092340; --surface-2: #0b2848; --raised: #0d2f57; + --line: #143558; --line-strong: #1e4a78; + --ink: #eaf2fc; --ink-2: #c3d4ea; --muted: #93abc8; + --accent: #61a0e8; --accent-hover: #8dbbef; --accent-ink: #030b14; --accent-soft: #0d2f57; --accent-line: #1e4a78; + --brand: #3485e2; --focus: #8dbbef; + --ok: #74d6a0; --ok-bg: #0b3326; --ok-line: #1a5c41; + --wait: #8dbbef; --wait-bg: #0d2f57; --wait-line: #1e4a78; + --you: #f6bd6a; --you-bg: #3a2709; --you-line: #6b4a17; + --fail: #ff958a; --fail-bg: #3e1616; --fail-line: #7a2a24; + --idle: #a6b7cd; --idle-bg: #10304f; --idle-line: #1e4a78; + --code-bg: #030b14; --code-ink: #e6f0fb; --code-muted: #8dbbef; + --e1: 0 0 0 1px rgba(255, 255, 255, 0.02); + --e2: 0 0 0 1px rgba(255, 255, 255, 0.03), 0 14px 32px -14px rgba(0, 0, 0, 0.6); + --e3: 0 40px 90px -20px rgba(0, 0, 0, 0.8); + --scrim: rgba(3, 11, 20, 0.72); + --route: #1e4a78; +} + +*, *::before, *::after { box-sizing: border-box; } +a { color: var(--accent); text-decoration: none; } +a:hover { color: var(--accent-hover); text-decoration: underline; text-underline-offset: 3px; } +:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; border-radius: 6px; } +h1, h2, h3, h4, p { margin: 0; } +svg { flex-shrink: 0; } +code, kbd, .mono { font-family: var(--mono); font-size: 0.88em; } + +/* Type */ +.display { font-size: 56px; line-height: 1.04; letter-spacing: -0.035em; font-weight: 650; } +.h1 { font-size: 40px; line-height: 1.1; letter-spacing: -0.03em; font-weight: 650; } +.h2 { font-size: 28px; line-height: 1.2; letter-spacing: -0.02em; font-weight: 650; } +.h3 { font-size: 20px; line-height: 1.3; letter-spacing: -0.01em; font-weight: 600; } +.h4 { font-size: 16px; line-height: 1.4; font-weight: 600; } +.lead { font-size: 18px; line-height: 1.55; color: var(--ink-2); } +.small { font-size: 13px; } +.xs { font-size: 12px; } +.muted { color: var(--muted); } +.ink2 { color: var(--ink-2); } +.eyebrow { font-size: 12px; font-weight: 600; letter-spacing: 0.08em; text-transform: uppercase; color: var(--muted); } +.num { font-variant-numeric: tabular-nums; } + +/* Layout */ +.wrap { width: 100%; max-width: 1240px; margin: 0 auto; padding: 0 40px; } +.row { display: flex; align-items: center; gap: 12px; } +.col { display: flex; flex-direction: column; gap: 12px; } +.between { justify-content: space-between; } +.wrapflex { flex-wrap: wrap; } +.grow { flex: 1 1 auto; min-width: 0; } +.grid2 { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 16px; } +.grid3 { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 16px; } +.grid4 { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 16px; } +.split { display: flex; gap: 32px; align-items: flex-start; flex-wrap: wrap; } +.split > .main { flex: 999 1 620px; min-width: 0; display: flex; flex-direction: column; gap: 24px; } +.split > .rail { flex: 1 1 320px; min-width: 0; display: flex; flex-direction: column; gap: 16px; } +.stack-lg { display: flex; flex-direction: column; gap: 32px; } +.stack { display: flex; flex-direction: column; gap: 16px; } +.hr { height: 1px; background: var(--line); border: 0; margin: 0; } + +/* Top bar */ +.top { position: relative; z-index: 2; border-bottom: 1px solid var(--line); background: color-mix(in srgb, var(--bg) 88%, transparent); } +.top .wrap { display: flex; align-items: center; gap: 20px; height: 64px; } +.logo { display: flex; align-items: center; gap: 10px; font-weight: 650; letter-spacing: -0.01em; color: var(--ink) !important; text-decoration: none !important; } +.logo-mark { width: 28px; height: 28px; border-radius: 8px; background: var(--brand); display: grid; place-items: center; color: #fff; } +.logo .tag { font-weight: 500; color: var(--muted); } +.nav { display: flex; gap: 4px; } +.nav a { color: var(--ink-2); padding: 10px 12px; border-radius: 8px; font-size: 14px; font-weight: 500; min-height: 44px; display: inline-flex; align-items: center; } +.nav a:hover { background: var(--accent-soft); color: var(--ink); text-decoration: none; } +.nav a[aria-current='page'] { color: var(--ink); background: var(--accent-soft); } +.kbar { flex: 1 1 auto; max-width: 420px; margin-left: auto; display: flex; align-items: center; gap: 10px; height: 40px; padding: 0 12px; border: 1px solid var(--line); border-radius: 10px; background: var(--surface); color: var(--muted); font-size: 14px; cursor: pointer; font-family: inherit; } +.kbar:hover { border-color: var(--line-strong); } +.kbd { display: inline-flex; align-items: center; gap: 2px; padding: 1px 6px; border: 1px solid var(--line); border-bottom-width: 2px; border-radius: 6px; font-family: var(--mono); font-size: 11px; color: var(--muted); background: var(--surface-2); } +.compat { display: inline-flex; align-items: center; gap: 8px; height: 40px; padding: 0 12px; border-radius: var(--r-full); border: 1px solid var(--accent-line); background: var(--accent-soft); color: var(--ink); font-size: 13px; font-weight: 500; font-family: inherit; cursor: pointer; } +.avatar { width: 36px; height: 36px; border-radius: 50%; background: linear-gradient(135deg, var(--blue-500), var(--blue-800)); color: #fff; display: grid; place-items: center; font-size: 13px; font-weight: 600; flex-shrink: 0; } + +/* Buttons */ +.btn { display: inline-flex; align-items: center; justify-content: center; gap: 8px; min-height: 44px; padding: 0 18px; border-radius: 10px; border: 1px solid var(--line-strong); background: var(--surface); color: var(--ink); font: 500 14px/1 var(--font); cursor: pointer; white-space: nowrap; text-decoration: none !important; } +.btn:hover { border-color: var(--accent); color: var(--ink); } +.btn-primary { background: var(--accent); border-color: var(--accent); color: var(--accent-ink) !important; } +.btn-primary:hover { background: var(--accent-hover); border-color: var(--accent-hover); color: var(--accent-ink); } +.btn-quiet { background: transparent; border-color: transparent; color: var(--ink-2); } +.btn-quiet:hover { background: var(--accent-soft); border-color: transparent; } +.btn-danger { background: var(--surface); border-color: var(--fail-line); color: var(--fail) !important; } +.btn-danger-solid { background: var(--fail); border-color: var(--fail); color: var(--surface) !important; } +.btn-sm { min-height: 36px; padding: 0 12px; font-size: 13px; border-radius: 8px; } +.btn-lg { min-height: 52px; padding: 0 24px; font-size: 16px; border-radius: 12px; } +.btn[disabled], .btn[aria-disabled='true'] { opacity: 0.45; cursor: not-allowed; } +.icon-btn { width: 44px; height: 44px; display: inline-grid; place-items: center; border-radius: 10px; border: 1px solid transparent; background: transparent; color: var(--ink-2); cursor: pointer; } +.icon-btn:hover { background: var(--accent-soft); color: var(--ink); } + +/* Form */ +.field { display: flex; flex-direction: column; gap: 6px; } +.label { font-size: 13px; font-weight: 600; color: var(--ink); display: flex; align-items: center; gap: 8px; } +.label .opt { font-weight: 400; color: var(--muted); } +.input, .select, .textarea { width: 100%; min-height: 44px; padding: 10px 12px; border: 1px solid var(--line-strong); border-radius: 10px; background: var(--surface); color: var(--ink); font: 400 15px/1.4 var(--font); } +.textarea { min-height: 96px; resize: vertical; } +.input::placeholder, .textarea::placeholder { color: var(--muted); } +.input:focus, .select:focus, .textarea:focus { outline: 2px solid var(--focus); outline-offset: 0; border-color: transparent; } +.input.is-error { border-color: var(--fail); } +.help { font-size: 13px; color: var(--muted); } +.err { font-size: 13px; color: var(--fail); display: flex; gap: 6px; align-items: center; } +.check { display: flex; gap: 10px; align-items: flex-start; font-size: 14px; } +.check input { width: 18px; height: 18px; margin-top: 2px; accent-color: var(--accent); } + +/* Chips and segmented controls */ +.chip { display: inline-flex; align-items: center; gap: 8px; min-height: 40px; padding: 0 14px; border-radius: var(--r-full); border: 1px solid var(--line); background: var(--surface); color: var(--ink-2); font: 500 14px/1 var(--font); cursor: pointer; white-space: nowrap; } +.chip:hover { border-color: var(--line-strong); color: var(--ink); } +.chip[aria-pressed='true'] { border-color: var(--accent); background: var(--accent-soft); color: var(--ink); } +.seg { display: inline-flex; padding: 4px; gap: 4px; border-radius: 12px; background: var(--bg-tint); border: 1px solid var(--line); } +.seg button, .seg a { min-height: 36px; padding: 0 14px; border: 0; border-radius: 8px; background: transparent; color: var(--ink-2); font: 500 14px/1 var(--font); cursor: pointer; display: inline-flex; align-items: center; gap: 8px; } +.seg [aria-selected='true'] { background: var(--surface); color: var(--ink); box-shadow: var(--e1), 0 0 0 1px var(--line); } + +/* Status: icon + word, never colour alone */ +.st { display: inline-flex; align-items: center; gap: 6px; height: 26px; padding: 0 10px; border-radius: var(--r-full); font-size: 12.5px; font-weight: 600; border: 1px solid; white-space: nowrap; } +.st svg { width: 14px; height: 14px; } +.st-ok { color: var(--ok); background: var(--ok-bg); border-color: var(--ok-line); } +.st-wait { color: var(--wait); background: var(--wait-bg); border-color: var(--wait-line); } +.st-you { color: var(--you); background: var(--you-bg); border-color: var(--you-line); } +.st-fail { color: var(--fail); background: var(--fail-bg); border-color: var(--fail-line); } +.st-idle { color: var(--idle); background: var(--idle-bg); border-color: var(--idle-line); } + +/* Trust marks: each means one thing */ +.trust { display: inline-flex; align-items: center; gap: 6px; font-size: 13px; font-weight: 500; color: var(--ink-2); } +.trust svg { color: var(--accent); } + +/* Cards */ +.card { background: var(--surface); border: 1px solid var(--line); border-radius: var(--r-lg); box-shadow: var(--e1); } +.card-pad { padding: 24px; } +.card-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 18px 24px; border-bottom: 1px solid var(--line); } +.card-body { padding: 20px 24px; } +.ext-card { display: flex; flex-direction: column; gap: 14px; padding: 20px; background: var(--surface); border: 1px solid var(--line); border-radius: var(--r-lg); box-shadow: var(--e1); color: var(--ink) !important; text-decoration: none !important; transition: border-color 120ms, box-shadow 120ms; } +.ext-card:hover { border-color: var(--accent-line); box-shadow: var(--e2); } +.ext-icon { width: 48px; height: 48px; border-radius: 12px; display: grid; place-items: center; color: #fff; font-weight: 650; font-size: 16px; flex-shrink: 0; } +.ext-icon.lg { width: 72px; height: 72px; border-radius: 18px; font-size: 24px; } +.ext-icon.xl { width: 96px; height: 96px; border-radius: 24px; font-size: 32px; } +.ext-icon.sm { width: 36px; height: 36px; border-radius: 10px; font-size: 13px; } +.i-blue { background: linear-gradient(140deg, #3485e2, #16539a); } +.i-teal { background: linear-gradient(140deg, #1f9e8f, #0f5d63); } +.i-amber { background: linear-gradient(140deg, #e59a2f, #a35a0c); } +.i-violet { background: linear-gradient(140deg, #6d6ae8, #3b33a8); } +.i-slate { background: linear-gradient(140deg, #4a6584, #1f3550); } +.i-rose { background: linear-gradient(140deg, #d9577a, #8f2346); } +.price { font-weight: 650; font-size: 14px; } +.price-free { color: var(--ok); } +.tagline { display: flex; flex-wrap: wrap; gap: 6px; } +.tag { display: inline-flex; align-items: center; height: 24px; padding: 0 9px; border-radius: 6px; background: var(--bg-tint); color: var(--ink-2); font-size: 12px; font-weight: 500; } + +/* Code / copy */ +.cmd { display: flex; align-items: center; gap: 12px; padding: 0 6px 0 16px; min-height: 52px; border-radius: 12px; background: var(--code-bg); color: var(--code-ink); font-family: var(--mono); font-size: 13.5px; } +.cmd .p { color: var(--code-muted); user-select: none; } +.cmd code { flex: 1 1 auto; min-width: 0; overflow-x: auto; white-space: nowrap; font-size: 13.5px; } +.copy { min-height: 40px; padding: 0 12px; border-radius: 8px; border: 1px solid rgba(141, 187, 239, 0.3); background: rgba(141, 187, 239, 0.08); color: var(--code-ink); font: 500 13px/1 var(--font); display: inline-flex; align-items: center; gap: 6px; cursor: pointer; } +.copy.done { color: #74d6a0; border-color: rgba(116, 214, 160, 0.4); } +.pre { margin: 0; padding: 16px; border-radius: 12px; background: var(--code-bg); color: var(--code-ink); font-family: var(--mono); font-size: 12.5px; line-height: 1.6; overflow: auto; } + +/* Check rows (release checks) */ +.checks { display: flex; flex-direction: column; } +.ck { display: grid; grid-template-columns: 28px minmax(0, 1fr) auto; gap: 12px; align-items: start; padding: 14px 0; border-top: 1px solid var(--line); } +.ck:first-child { border-top: 0; } +.ck-dot { width: 28px; height: 28px; border-radius: 50%; display: grid; place-items: center; border: 1px solid; } +.ck-dot svg { width: 15px; height: 15px; } +.ck.ok .ck-dot { color: var(--ok); background: var(--ok-bg); border-color: var(--ok-line); } +.ck.fail .ck-dot { color: var(--fail); background: var(--fail-bg); border-color: var(--fail-line); } +.ck.warn .ck-dot { color: var(--you); background: var(--you-bg); border-color: var(--you-line); } +.ck.run .ck-dot { color: var(--wait); background: var(--wait-bg); border-color: var(--wait-line); border-style: dashed; } +.ck.wait .ck-dot { color: var(--muted); background: transparent; border-color: var(--line-strong); border-style: dashed; } +.ck-title { font-weight: 600; font-size: 14.5px; } +.ck-desc { font-size: 13px; color: var(--muted); } +.ck-msg { margin-top: 8px; padding: 10px 12px; border-radius: 8px; font-family: var(--mono); font-size: 12.5px; line-height: 1.5; } +.ck.fail .ck-msg { background: var(--fail-bg); color: var(--fail); } +.ck.warn .ck-msg { background: var(--you-bg); color: var(--you); } +.ck-state { font-size: 12.5px; font-weight: 600; padding-top: 4px; } + +/* Waypoint route: the release pipeline */ +.route { display: flex; align-items: flex-start; } +.stop { position: relative; flex: 1 1 0; display: flex; flex-direction: column; align-items: center; gap: 8px; text-align: center; min-width: 0; } +.stop::before { content: ''; position: absolute; top: 13px; left: -50%; right: 50%; height: 2px; background: var(--route); z-index: 0; } +.stop:first-child::before { display: none; } +.stop.done::before, .stop.now::before { background: var(--accent); } +.stop .dot { position: relative; z-index: 1; width: 28px; height: 28px; border-radius: 50%; display: grid; place-items: center; background: var(--surface); border: 2px solid var(--route); color: var(--muted); } +.stop .dot svg { width: 14px; height: 14px; } +.stop.done .dot { background: var(--accent); border-color: var(--accent); color: var(--accent-ink); } +.stop.now .dot { border-color: var(--accent); color: var(--accent); box-shadow: 0 0 0 5px var(--accent-soft); } +.stop.you .dot { border-color: var(--you); color: var(--you); box-shadow: 0 0 0 5px var(--you-bg); } +.stop.fail .dot { border-color: var(--fail); background: var(--fail); color: var(--surface); } +.stop .lbl { font-size: 12.5px; font-weight: 600; color: var(--ink-2); } +.stop .sub { font-size: 12px; color: var(--muted); margin-top: -6px; } +.route.mini .stop .dot { width: 14px; height: 14px; border-width: 2px; } +.route.mini .stop::before { top: 6px; } +.route.mini .stop .dot svg { display: none; } + +/* Timeline */ +.tl { display: flex; flex-direction: column; } +.tl-item { position: relative; display: grid; grid-template-columns: 32px minmax(0, 1fr); gap: 14px; padding-bottom: 22px; } +.tl-item::before { content: ''; position: absolute; left: 15px; top: 32px; bottom: 0; width: 2px; background: var(--line); } +.tl-item:last-child::before { display: none; } +.tl-dot { width: 32px; height: 32px; border-radius: 50%; display: grid; place-items: center; border: 1px solid var(--line); background: var(--surface-2); color: var(--muted); } +.tl-dot svg { width: 15px; height: 15px; } +.tl-dot.ok { color: var(--ok); background: var(--ok-bg); border-color: var(--ok-line); } +.tl-dot.fail { color: var(--fail); background: var(--fail-bg); border-color: var(--fail-line); } +.tl-dot.wait { color: var(--wait); background: var(--wait-bg); border-color: var(--wait-line); } +.tl-title { font-weight: 600; font-size: 14.5px; } +.tl-meta { font-size: 13px; color: var(--muted); } +.quote { margin-top: 8px; padding: 12px 14px; border-radius: 10px; background: var(--surface-2); border: 1px solid var(--line); font-size: 14px; color: var(--ink-2); } + +/* Callouts, toasts, dialogs, empty */ +.note { display: flex; gap: 12px; align-items: flex-start; padding: 14px 16px; border-radius: 12px; border: 1px solid; font-size: 14px; } +.note svg { margin-top: 2px; } +.note-info { background: var(--wait-bg); border-color: var(--wait-line); color: var(--ink); } +.note-info svg { color: var(--wait); } +.note-you { background: var(--you-bg); border-color: var(--you-line); color: var(--ink); } +.note-you svg { color: var(--you); } +.note-fail { background: var(--fail-bg); border-color: var(--fail-line); color: var(--ink); } +.note-fail svg { color: var(--fail); } +.note-ok { background: var(--ok-bg); border-color: var(--ok-line); color: var(--ink); } +.note-ok svg { color: var(--ok); } +.toast { display: flex; align-items: center; gap: 12px; padding: 12px 14px 12px 16px; border-radius: 12px; background: var(--ink); color: var(--bg); box-shadow: var(--e3); font-size: 14px; } +.toast a { color: var(--blue-300); } +[data-theme='dark'] .toast { background: var(--raised); color: var(--ink); border: 1px solid var(--line-strong); } +.dialog { width: 100%; max-width: 520px; background: var(--raised); border: 1px solid var(--line); border-radius: var(--r-xl); box-shadow: var(--e3); } +.dialog-head { padding: 24px 24px 0; display: flex; gap: 12px; align-items: flex-start; justify-content: space-between; } +.dialog-body { padding: 16px 24px 8px; display: flex; flex-direction: column; gap: 16px; } +.dialog-foot { padding: 16px 24px 24px; display: flex; gap: 12px; justify-content: flex-end; flex-wrap: wrap; } +.scrim { background: var(--scrim); } +.empty { display: flex; flex-direction: column; align-items: center; text-align: center; gap: 12px; padding: 56px 24px; } +.empty-glyph { width: 56px; height: 56px; border-radius: 16px; display: grid; place-items: center; background: var(--accent-soft); color: var(--accent); } +.skel { background: linear-gradient(90deg, var(--bg-tint), var(--line), var(--bg-tint)); border-radius: 8px; } + +/* Lists and tables, used sparingly */ +.list { display: flex; flex-direction: column; } +.li { display: flex; align-items: center; gap: 16px; padding: 16px 0; border-top: 1px solid var(--line); } +.li:first-child { border-top: 0; } +.tbl-wrap { overflow-x: auto; } +.tbl { width: 100%; border-collapse: collapse; font-size: 14px; } +.tbl th { text-align: left; font-size: 12px; font-weight: 600; color: var(--muted); padding: 10px 16px; border-bottom: 1px solid var(--line); white-space: nowrap; } +.tbl td { padding: 14px 16px; border-bottom: 1px solid var(--line); vertical-align: middle; } +.tbl tr:last-child td { border-bottom: 0; } + +/* Section anchor nav (replaces tabs in the editor) */ +.anchors { display: flex; flex-direction: column; gap: 2px; } +.anchors a { display: flex; align-items: center; justify-content: space-between; gap: 8px; min-height: 40px; padding: 0 12px; border-radius: 8px; color: var(--ink-2); font-size: 14px; font-weight: 500; } +.anchors a:hover { background: var(--accent-soft); text-decoration: none; color: var(--ink); } +.anchors a[aria-current='true'] { background: var(--accent-soft); color: var(--ink); } +.count { min-width: 22px; height: 22px; padding: 0 6px; border-radius: 999px; display: inline-grid; place-items: center; font-size: 12px; font-weight: 600; background: var(--you-bg); color: var(--you); border: 1px solid var(--you-line); } +.count.ok { background: var(--ok-bg); color: var(--ok); border-color: var(--ok-line); } + +/* Progress */ +.meter { height: 8px; border-radius: 999px; background: var(--bg-tint); overflow: hidden; border: 1px solid var(--line); } +.meter > span { display: block; height: 100%; background: var(--accent); border-radius: 999px; } + +/* Hero */ +.hero { position: relative; overflow: hidden; border-bottom: 1px solid var(--line); background: radial-gradient(1200px 400px at 85% -10%, var(--accent-soft), transparent 60%), var(--bg); } +.searchbox { display: flex; align-items: center; gap: 12px; height: 64px; padding: 0 10px 0 20px; border-radius: 18px; border: 1px solid var(--line-strong); background: var(--surface); box-shadow: var(--e2); } +.searchbox input { flex: 1 1 auto; min-width: 0; border: 0; outline: 0; background: transparent; color: var(--ink); font: 400 18px/1 var(--font); height: 100%; } +.searchbox input::placeholder { color: var(--muted); } +.intent { display: flex; align-items: center; gap: 12px; padding: 16px; border-radius: 14px; border: 1px solid var(--line); background: var(--surface); color: var(--ink) !important; text-decoration: none !important; min-height: 72px; } +.intent:hover { border-color: var(--accent-line); box-shadow: var(--e2); } +.intent .ig { width: 40px; height: 40px; border-radius: 10px; display: grid; place-items: center; background: var(--accent-soft); color: var(--accent); } + +/* Shown only at phone width */ +.show-sm { display: none !important; } + +/* Responsive: container queries so artboards reflow at their own width */ +@media (max-width: 1100px) { + .grid4 { grid-template-columns: repeat(2, minmax(0, 1fr)); } +} +@media (max-width: 760px) { + .wrap { padding: 0 16px; } + .display { font-size: 36px; } + .h1 { font-size: 28px; } + .h2 { font-size: 22px; } + .lead { font-size: 16px; } + .grid2, .grid3, .grid4 { grid-template-columns: minmax(0, 1fr); } + .nav, .hide-sm { display: none !important; } + .show-sm { display: flex !important; } + .kbar { max-width: none; } + .kbar .kbd, .kbar .kbar-text { display: none; } + .kbar { flex: 0 0 44px; height: 44px; padding: 0; justify-content: center; } + .top .wrap { gap: 8px; } + .card-head, .card-body { padding-left: 16px; padding-right: 16px; } + .card-pad { padding: 16px; } + .searchbox { height: 56px; } + .searchbox input { font-size: 16px; } + .stop .lbl { font-size: 11px; } + .stop .sub { display: none; } + .split { gap: 20px; } + .dialog { border-radius: 20px 20px 0 0; max-width: none; } +} diff --git a/resources/js/app.jsx b/resources/js/app.jsx new file mode 100644 index 0000000..ada4b02 --- /dev/null +++ b/resources/js/app.jsx @@ -0,0 +1,17 @@ +import '../css/app.css'; +import { createInertiaApp } from '@inertiajs/react'; +import { createRoot, hydrateRoot } from 'react-dom/client'; +import { resolvePage, pageTitle } from './lib/pages'; + +createInertiaApp({ + title: pageTitle, + resolve: resolvePage, + progress: { color: '#3485e2', delay: 150 }, + setup({ el, App, props }) { + if (el.hasChildNodes()) { + hydrateRoot(el, ); + return; + } + createRoot(el).render(); + }, +}); diff --git a/resources/js/components/Checks.jsx b/resources/js/components/Checks.jsx new file mode 100644 index 0000000..d545164 --- /dev/null +++ b/resources/js/components/Checks.jsx @@ -0,0 +1,80 @@ +import Icon from './Icon'; +import { CHECKS } from '../lib/statuses'; + +const LOOK = { + ok: { icon: 'check', word: 'Passed', color: 'var(--ok)' }, + fail: { icon: 'x', word: 'Failed', color: 'var(--fail)' }, + warn: { icon: 'alert', word: 'Warning', color: 'var(--you)' }, + run: { icon: 'spinner', word: 'Running', color: 'var(--wait)' }, + wait: { icon: null, word: 'Waiting', color: 'var(--muted)' }, +}; + +/** The rows for a release's checks, in the service's order. Unreported checks run or wait. */ +export function checkRows(release) { + const reported = new Map((release?.checks ?? []).map((check) => [check.check, check])); + const running = release?.status === 'checking'; + let firstPending = true; + return Object.entries(CHECKS).map(([key, meta]) => { + const check = reported.get(key); + let state = 'wait'; + if (check) { + state = check.passed ? (check.warnings?.length ? 'warn' : 'ok') : 'fail'; + } else if (running && firstPending) { + state = 'run'; + firstPending = false; + } + return { key, ...meta, check, state }; + }); +} + +export function checksSummary(release) { + const rows = checkRows(release); + const failed = rows.filter((row) => row.state === 'fail').length; + const warned = rows.filter((row) => row.state === 'warn').length; + const done = rows.filter((row) => row.check).length; + if (release?.status === 'checking') { + return { tone: 'wait', label: `${done} of ${rows.length} done` }; + } + if (failed) { + return { tone: 'fail', label: `${failed} failed${warned ? `, ${warned} ${warned === 1 ? 'warning' : 'warnings'}` : ''}` }; + } + if (!done) { + return { tone: 'idle', label: 'Not run yet' }; + } + return { tone: 'ok', label: warned ? `All passed, ${warned} ${warned === 1 ? 'warning' : 'warnings'}` : 'All passed' }; +} + +/** The eight check rows with their messages (errors always, warnings when `showWarnings`). */ +export default function Checks({ release, showWarnings = true }) { + return ( +
+ {checkRows(release).map((row) => { + const look = LOOK[row.state]; + return ( +
+ {look.icon && } +
+
{row.label}
+
{row.check?.summary || row.description}
+ {row.check?.errors?.map((message, index) => ( +
+ {message} +
+ ))} + {showWarnings && + row.check?.passed && + row.check?.warnings?.map((message, index) => ( +
+ {message} +
+ ))} +
+ + {look.word} + +
+ ); + })} +
+ ); +} diff --git a/resources/js/components/CommandPalette.jsx b/resources/js/components/CommandPalette.jsx new file mode 100644 index 0000000..06e061a --- /dev/null +++ b/resources/js/components/CommandPalette.jsx @@ -0,0 +1,192 @@ +import { useEffect, useMemo, useRef, useState } from 'react'; +import { createPortal } from 'react-dom'; +import { router, usePage } from '@inertiajs/react'; +import Icon from './Icon'; +import ExtensionIcon from './ExtensionIcon'; +import { api } from '../lib/api'; +import { priceLabel } from '../lib/format'; + +/** + * ⌘K / Ctrl K from any page: searches extensions, publishers and categories as you type, and + * for signed-in people doubles as navigation. ↑↓ move, Enter opens, ⌘/Ctrl Enter copies an + * extension's install command. + */ +export default function CommandPalette({ open, onClose }) { + const { auth } = usePage().props; + const account = auth?.account; + const [query, setQuery] = useState(''); + const [extensions, setExtensions] = useState([]); + const [categories, setCategories] = useState([]); + const [active, setActive] = useState(0); + const [copied, setCopied] = useState(null); + const input = useRef(null); + + useEffect(() => { + if (!open) { + return; + } + setQuery(''); + setActive(0); + input.current?.focus(); + if (!categories.length) { + api.get('catalog/categories').then(setCategories).catch(() => {}); + } + }, [open]); // eslint-disable-line react-hooks/exhaustive-deps + + useEffect(() => { + if (!open) { + return undefined; + } + const q = query.trim(); + const timer = setTimeout(() => { + api.get('catalog/extensions', { q: q || undefined, limit: 5 }) + .then((result) => setExtensions(result.data ?? [])) + .catch(() => setExtensions([])); + }, 120); + return () => clearTimeout(timer); + }, [query, open]); + + const items = useMemo(() => { + const q = query.trim().toLowerCase(); + const list = extensions.map((extension) => ({ group: 'Extensions', key: `x-${extension.slug}`, extension, href: `/extensions/${extension.slug}`, title: extension.name, meta: `${extension.publisher?.name ?? ''} · ${priceLabel(extension.price)}` })); + + const publishers = new Map(); + extensions.forEach((extension) => extension.publisher && publishers.set(extension.publisher.slug, extension.publisher)); + [...publishers.values()].forEach((publisher) => list.push({ group: 'Publishers', key: `p-${publisher.slug}`, href: `/publishers/${publisher.slug}`, title: publisher.name, meta: publisher.is_verified ? 'Verified publisher' : 'Publisher', icon: 'building' })); + + categories + .filter((category) => q && category.name.toLowerCase().includes(q)) + .slice(0, 3) + .forEach((category) => list.push({ group: 'Categories', key: `c-${category.slug}`, href: `/?category=${category.slug}`, title: category.name, meta: `${category.extensions_count ?? 0} extensions`, icon: 'warehouse' })); + + const goto = account + ? [ + ['My extensions', '/studio', 'box'], + ['New extension', '/studio/new', 'plus'], + ['Tokens', '/account/tokens', 'key'], + ['Organizations', '/account/organizations', 'building'], + ...(account.is_staff ? [['Review queue', '/review', 'gavel']] : []), + ] + : [ + ['Sign in', '/sign-in', 'user'], + ['Create an account', '/register', 'plus'], + ]; + goto.filter(([title]) => !q || title.toLowerCase().includes(q)).forEach(([title, href, icon]) => list.push({ group: 'Go to', key: `g-${href}`, href, title, icon })); + return list; + }, [extensions, categories, query, account]); + + useEffect(() => setActive((index) => Math.min(index, Math.max(items.length - 1, 0))), [items.length]); + + if (!open) { + return null; + } + + function choose(item) { + if (!item) { + return; + } + onClose(); + router.visit(item.href); + } + + async function copyInstall(item) { + if (!item?.extension) { + return; + } + try { + await navigator.clipboard.writeText(`flb install ${item.extension.slug}`); + setCopied(item.key); + } catch { + // Clipboard refused; Enter still opens it. + } + } + + function onKeyDown(event) { + if (event.key === 'Escape') { + event.preventDefault(); + onClose(); + } else if (event.key === 'ArrowDown') { + event.preventDefault(); + setActive((index) => (index + 1) % Math.max(items.length, 1)); + } else if (event.key === 'ArrowUp') { + event.preventDefault(); + setActive((index) => (index - 1 + items.length) % Math.max(items.length, 1)); + } else if (event.key === 'Enter') { + event.preventDefault(); + if (event.metaKey || event.ctrlKey) { + copyInstall(items[active]); + } else { + choose(items[active]); + } + } + } + + let lastGroup = null; + return createPortal( +
event.target === event.currentTarget && onClose()}> +
+
+ + { + setQuery(event.target.value); + setActive(0); + }} + style={{ border: 0, outline: 0, background: 'transparent', color: 'var(--ink)', font: '400 18px/1 var(--font)', minHeight: 36 }} + /> + Esc +
+
    + {items.length === 0 &&
  • No matches. Try a broader term.
  • } + {items.map((item, index) => { + const header = item.group !== lastGroup ? item.group : null; + lastGroup = item.group; + return ( +
  • + {header &&
    {header}
    } +
    setActive(index)} + onClick={() => choose(item)} + className="row" + style={{ gap: 12, minHeight: 52, padding: '0 12px', borderRadius: 10, cursor: 'pointer', background: index === active ? 'var(--accent-soft)' : 'transparent', outline: index === active ? '1px solid var(--accent-line)' : 'none' }} + > + {item.extension ? ( + + ) : ( + + )} + + {item.title} + {item.meta && {item.meta}} + + {copied === item.key ? Install command copied : index === active && ↵} +
    +
  • + ); + })} +
+
+ + ↑ + ↓ move ↵ open ⌘ ↵ copy install command + +
+
+
, + document.body + ); +} diff --git a/resources/js/components/CopyCommand.jsx b/resources/js/components/CopyCommand.jsx new file mode 100644 index 0000000..0feafc0 --- /dev/null +++ b/resources/js/components/CopyCommand.jsx @@ -0,0 +1,36 @@ +import { useEffect, useRef, useState } from 'react'; +import Icon from './Icon'; + +/** A command (or any value) with a copy button; the label reads "Copied" for 1.5 s. */ +export default function CopyCommand({ value, prompt = '$', label = 'command', className = '' }) { + const [copied, setCopied] = useState(false); + const timer = useRef(); + + useEffect(() => () => clearTimeout(timer.current), []); + + async function copy() { + try { + await navigator.clipboard.writeText(value); + setCopied(true); + clearTimeout(timer.current); + timer.current = setTimeout(() => setCopied(false), 1500); + } catch { + // Clipboard access can be refused; the value stays selectable. + } + } + + return ( +
+ {prompt && ( + + )} + {value} + +
+ ); +} diff --git a/resources/js/components/Dialog.jsx b/resources/js/components/Dialog.jsx new file mode 100644 index 0000000..e0b3df5 --- /dev/null +++ b/resources/js/components/Dialog.jsx @@ -0,0 +1,82 @@ +import { useEffect, useRef } from 'react'; +import { createPortal } from 'react-dom'; +import Icon from './Icon'; + +/** + * A modal dialog: focus moves in on open and returns on close, Escape and the close button + * close it (a click outside does not, so nothing typed is lost), Tab stays inside. + */ +export default function Dialog({ open, title, onClose, children, footer, size = 520, labelledBy = 'dialog-title', dismissible = true }) { + const ref = useRef(null); + const previous = useRef(null); + // The latest onClose and dismissible, read by the key handler: the effect below runs only when + // the dialog opens or closes, so focus never jumps back to the first field while typing. + const latest = useRef({ onClose, dismissible }); + useEffect(() => { + latest.current = { onClose, dismissible }; + }); + + useEffect(() => { + if (!open) { + return undefined; + } + previous.current = document.activeElement; + const node = ref.current; + const focusable = () => [...node.querySelectorAll('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])')].filter((el) => !el.disabled && el.getAttribute('aria-hidden') !== 'true'); + (node.querySelector('[data-autofocus]') ?? focusable()[0] ?? node).focus(); + + function onKey(event) { + if (event.key === 'Escape' && latest.current.dismissible) { + event.stopPropagation(); + latest.current.onClose?.(); + } + if (event.key === 'Tab') { + const items = focusable(); + if (!items.length) { + return; + } + const first = items[0]; + const last = items[items.length - 1]; + if (event.shiftKey && document.activeElement === first) { + event.preventDefault(); + last.focus(); + } else if (!event.shiftKey && document.activeElement === last) { + event.preventDefault(); + first.focus(); + } + } + } + node.addEventListener('keydown', onKey); + const overflow = document.body.style.overflow; + document.body.style.overflow = 'hidden'; + return () => { + node.removeEventListener('keydown', onKey); + document.body.style.overflow = overflow; + previous.current?.focus?.(); + }; + }, [open]); + + if (!open) { + return null; + } + + return createPortal( +
+
+
+

+ {title} +

+ {dismissible && ( + + )} +
+
{children}
+ {footer &&
{footer}
} +
+
, + document.body + ); +} diff --git a/resources/js/components/Dialog.test.jsx b/resources/js/components/Dialog.test.jsx new file mode 100644 index 0000000..ba1e13d --- /dev/null +++ b/resources/js/components/Dialog.test.jsx @@ -0,0 +1,33 @@ +import { expect, test, vi } from 'vitest'; +import { useState } from 'react'; +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import Dialog from './Dialog'; + +function Form() { + const [value, setValue] = useState(''); + return ( + {}}> + + setValue(event.target.value)} /> + + ); +} + +test('keeps focus in the field being typed in when the parent re-renders', async () => { + render(
); + const second = screen.getByLabelText('Second'); + await userEvent.click(second); + await userEvent.type(second, 'abc'); + expect(second).toHaveValue('abc'); + expect(second).toHaveFocus(); +}); + +test('Escape closes with the latest onClose and not when not dismissible', async () => { + const onClose = vi.fn(); + const { rerender } = render( {}} dismissible={false}>); + await userEvent.keyboard('{Escape}'); + rerender(); + await userEvent.keyboard('{Escape}'); + expect(onClose).toHaveBeenCalledTimes(1); +}); diff --git a/resources/js/components/ExtensionCard.jsx b/resources/js/components/ExtensionCard.jsx new file mode 100644 index 0000000..1ae9e14 --- /dev/null +++ b/resources/js/components/ExtensionCard.jsx @@ -0,0 +1,38 @@ +import { Link } from '@inertiajs/react'; +import ExtensionIcon from './ExtensionIcon'; +import { Verified, WorksWith } from './Trust'; +import { isPaid, priceLabel, shortDate } from '../lib/format'; + +/** A catalog card. The whole card is one link to the extension page. */ +export default function ExtensionCard({ extension, console: consoleVersion }) { + const paid = isPaid(extension.price); + return ( + +
+ +
+
{extension.name}
+
{extension.publisher?.name}
+
+ {priceLabel(extension.price)} +
+

+ {extension.subtitle} +

+
+ {consoleVersion && extension.compatible === true && } + {extension.publisher?.is_verified && } + {extension.hosting === 'self_hosted' && Self-hosted only} + {extension.price?.on_sale && On sale} +
+
+ + {extension.latest_version ? `v${extension.latest_version}` : 'No release'} + {extension.category ? ` · ${extension.category.name}` : ''} + + {extension.installs_count} installs +
+ Updated {shortDate(extension.updated_at)} + + ); +} diff --git a/resources/js/components/ExtensionIcon.jsx b/resources/js/components/ExtensionIcon.jsx new file mode 100644 index 0000000..009f0a6 --- /dev/null +++ b/resources/js/components/ExtensionIcon.jsx @@ -0,0 +1,11 @@ +import { iconTone, initials } from '../lib/format'; + +/** The listing's icon image, else its initials on a stable gradient. */ +export default function ExtensionIcon({ extension, size = '', className = '' }) { + const url = extension?.icon_url; + return ( + + ); +} diff --git a/resources/js/components/Icon.jsx b/resources/js/components/Icon.jsx new file mode 100644 index 0000000..7ade68a --- /dev/null +++ b/resources/js/components/Icon.jsx @@ -0,0 +1,82 @@ +/** + * Stroke icons from the design (24 px grid, currentColor). Decorative unless `label` is given. + */ +const PATHS = { + search: 'M11 4a7 7 0 1 0 0 14 7 7 0 0 0 0-14zM20 20l-3.5-3.5', + moon: 'M20 14.5A8 8 0 0 1 9.5 4 7 7 0 1 0 20 14.5z', + sun: 'M12 4V2M12 22v-2M4 12H2M22 12h-2M5.6 5.6 4.2 4.2M19.8 19.8l-1.4-1.4M5.6 18.4l-1.4 1.4M19.8 4.2l-1.4 1.4M12 7a5 5 0 1 0 0 10 5 5 0 0 0 0-10z', + user: 'M12 4a4 4 0 1 0 0 8 4 4 0 0 0 0-8zM4 21a8 8 0 0 1 16 0', + check: 'M5 12l5 5L20 7', + x: 'M7 7l10 10M17 7L7 17', + close: 'M6 6l12 12M18 6L6 18', + plus: 'M12 5v14M5 12h14', + minus: 'M8 12h8', + arrowUp: 'M12 19V6M6 11l6-6 6 6', + arrowLeft: 'M15 6l-6 6 6 6', + arrowRight: 'M9 6l6 6-6 6', + chevronDown: 'M6 9l6 6 6-6', + clock: 'M12 3a9 9 0 1 0 0 18 9 9 0 0 0 0-18zM12 7v5l3 2', + eye: 'M2 12s4-7 10-7 10 7 10 7-4 7-10 7S2 12 2 12zM12 9a3 3 0 1 0 0 6 3 3 0 0 0 0-6z', + alert: 'M12 8v5M12 16h.01M12 3a9 9 0 1 0 0 18 9 9 0 0 0 0-18z', + warning: 'M12 9v4M12 17h.01M10.3 3.9L2 18a2 2 0 0 0 1.7 3h16.6a2 2 0 0 0 1.7-3L13.7 3.9a2 2 0 0 0-3.4 0z', + info: 'M12 3a9 9 0 1 0 0 18 9 9 0 0 0 0-18zM12 11v5M12 8h.01', + ban: 'M12 4a8 8 0 1 0 0 16 8 8 0 0 0 0-16zM6.5 6.5l11 11', + pencil: 'M4 20h4L19 9l-4-4L4 16z', + copy: 'M9 9h11v11H9zM5 15V5a1 1 0 0 1 1-1h10', + download: 'M12 4v12M6 11l6 6 6-6M4 20h16', + upload: 'M12 19V6M6 11l6-6 6 6M4 20h16', + trash: 'M4 7h16M10 11v6M14 11v6M6 7l1 13h10l1-13M9 7V4h6v3', + send: 'M22 2L11 13M22 2l-7 20-4-9-9-4z', + globe: 'M12 3a9 9 0 1 0 0 18 9 9 0 0 0 0-18zM3 12h18M12 3a14 14 0 0 1 0 18M12 3a14 14 0 0 0 0 18', + shield: 'M12 3l7 3v5c0 4.5-3 8.3-7 10-4-1.7-7-5.5-7-10V6zM9 12l2 2 4-4', + reviewed: 'M11 5a6 6 0 1 0 0 12 6 6 0 0 0 0-12zM20 20l-4.3-4.3M8.5 11l1.8 1.8 3.2-3.3', + monitor: 'M3 4h18v14H3zM8 21h8M9 11l2 2 4-4', + terminal: 'M4 5h16v14H4zM8 10l3 2-3 2M13 15h3', + box: 'M4 7l8-4 8 4-8 4zM4 7v10l8 4 8-4V7M12 11v10', + file: 'M14 3H7a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V8zM14 3v5h5', + key: 'M8 11a4 4 0 1 0 0 8 4 4 0 0 0 0-8zM11 12l9-9M17 6l3 3M14 9l2 2', + mail: 'M3 5h18v14H3zM3 7l9 6 9-6', + lock: 'M5 11h14v10H5zM8 11V7a4 4 0 0 1 8 0v4', + image: 'M3 3h18v18H3zM8 14l3-3 5 5M9 7a1.5 1.5 0 1 0 0 3 1.5 1.5 0 0 0 0-3z', + gavel: 'M12 3l8 4v5c0 5-3.5 8-8 9-4.5-1-8-4-8-9V7z', + building: 'M4 21V5h10v16M14 9h6v12M8 9h2M8 13h2M8 17h2', + wallet: 'M4 6h16v12H4zM4 10h16M16 14h2', + chart: 'M4 20V10M10 20V4M16 20v-7M22 20H2', + truck: 'M5 17h14l-1.5-6h-11zM7 17v2M17 17v2M8 11l1-4h6l1 4', + warehouse: 'M3 10l9-6 9 6v10H3zM8 20v-6h8v6', + route: 'M6 19a2 2 0 1 0 0-4 2 2 0 0 0 0 4zM18 9a2 2 0 1 0 0-4 2 2 0 0 0 0 4zM8 17h7a3 3 0 0 0 0-6H9a3 3 0 0 1 0-6h7', + card: 'M4 6h16v12H4zM4 10h16M8 15h3', + ledger: 'M5 4h14v16H5zM9 8h6M9 12h6M9 16h3', + chat: 'M4 5h16v10H8l-4 4z', + logo: 'M8.5 18H14a4 4 0 0 0 0-8h-4a4 4 0 0 1 0-8h5.5M6 15.5a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5zM18 3.5a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5z', + menu: 'M4 7h16M4 12h16M4 17h16', + external: 'M14 4h6v6M20 4l-9 9M18 14v6H4V6h6', + spinner: 'M12 4a8 8 0 1 0 8 8', +}; + +export default function Icon({ name, size = 18, strokeWidth = 2, label, className = '', spin = false }) { + const d = PATHS[name]; + if (!d) { + return null; + } + return ( + + + + ); +} + +export const ICONS = Object.keys(PATHS); diff --git a/resources/js/components/Route.jsx b/resources/js/components/Route.jsx new file mode 100644 index 0000000..52b77f8 --- /dev/null +++ b/resources/js/components/Route.jsx @@ -0,0 +1,65 @@ +import Icon from './Icon'; + +const STATE_ICON = { done: 'check', now: 'eye', you: 'alert', fail: 'x' }; + +/** + * The waypoint route: a release's stops (uploaded, checks, submitted, review, live). Each stop is + * `{ label, sub?, state: 'done'|'now'|'you'|'fail'|'todo', icon? }`. `mini` draws dots only. + */ +export default function Route({ stops, mini = false, label, className = '' }) { + return ( +
    + {stops.map((stop, index) => ( +
  1. + {!mini && (stop.icon || STATE_ICON[stop.state]) && } + {!mini || stop.label ? {stop.label} : null} + {!mini && stop.sub && {stop.sub}} + {srState(stop.state)} +
  2. + ))} +
+ ); +} + +function srState(state) { + return { done: ', done', now: ', in progress', you: ', your move', fail: ', failed', todo: ', not yet' }[state] ?? ''; +} + +/** + * The route for a release, from its status: uploaded → checks → submitted → review → live. + */ +export function releaseStops(release, { submittedAt } = {}) { + const status = release?.status; + const order = ['uploaded', 'checks', 'submitted', 'review', 'live']; + const reached = { + checking: 1, + failed_checks: 1, + uploaded: 2, + submitted: 3, + in_review: 3, + approved: 4, + rejected: 3, + published: 5, + yanked: 5, + }[status] ?? 0; + const stops = order.map((key, index) => ({ key, state: index < reached ? 'done' : 'todo' })); + const set = (key, state, sub) => Object.assign(stops.find((s) => s.key === key), { state, sub }); + + set('uploaded', 'done'); + if (status === 'checking') set('checks', 'now', 'running'); + if (status === 'failed_checks') set('checks', 'fail', 'failed'); + if (['uploaded', 'submitted', 'in_review', 'approved', 'rejected', 'published', 'yanked'].includes(status)) set('checks', 'done', 'passed'); + if (status === 'uploaded') set('submitted', 'you', 'your move'); + if (['submitted', 'in_review', 'approved', 'rejected', 'published', 'yanked'].includes(status)) set('submitted', 'done', submittedAt); + if (status === 'submitted') set('review', 'now', 'waiting'); + if (status === 'in_review') set('review', 'now', 'in review'); + if (status === 'rejected') set('review', 'fail', 'changes requested'); + if (status === 'approved') { + set('review', 'done', 'approved'); + set('live', 'now', 'publishing'); + } + if (status === 'published') set('live', 'done'); + + const labels = { uploaded: 'Uploaded', checks: 'Checks', submitted: 'Submitted', review: 'Review', live: 'Live' }; + return stops.map((stop) => ({ ...stop, label: labels[stop.key], icon: stop.state === 'now' && stop.key === 'checks' ? 'spinner' : undefined })); +} diff --git a/resources/js/components/Route.test.jsx b/resources/js/components/Route.test.jsx new file mode 100644 index 0000000..638c591 --- /dev/null +++ b/resources/js/components/Route.test.jsx @@ -0,0 +1,26 @@ +import { render, screen } from '@testing-library/react'; +import Route, { releaseStops } from './Route'; + +describe('releaseStops', () => { + it('marks the review stop as current while a release is in review', () => { + const stops = releaseStops({ status: 'in_review' }); + expect(stops.map((s) => [s.key, s.state])).toEqual([ + ['uploaded', 'done'], + ['checks', 'done'], + ['submitted', 'done'], + ['review', 'now'], + ['live', 'todo'], + ]); + }); + + it('fails the checks stop when checks failed and asks the publisher to submit when they passed', () => { + expect(releaseStops({ status: 'failed_checks' }).find((s) => s.key === 'checks').state).toBe('fail'); + expect(releaseStops({ status: 'uploaded' }).find((s) => s.key === 'submitted').state).toBe('you'); + }); + + it('renders each stop with a state for screen readers', () => { + render(); + expect(screen.getByRole('list', { name: 'Release route' })).toBeInTheDocument(); + expect(screen.getByText(', failed')).toBeInTheDocument(); + }); +}); diff --git a/resources/js/components/Status.jsx b/resources/js/components/Status.jsx new file mode 100644 index 0000000..537f40e --- /dev/null +++ b/resources/js/components/Status.jsx @@ -0,0 +1,30 @@ +import Icon from './Icon'; +import { listingStatus, releaseStatus } from '../lib/statuses'; + +/** A status chip: icon plus word, never colour alone. */ +export function StatusChip({ tone = 'idle', icon, children, className = '', ...rest }) { + return ( + + {icon && } + {children} + + ); +} + +export function ReleaseStatus({ status, ...rest }) { + const s = releaseStatus(status); + return ( + + {s.label} + + ); +} + +export function ListingStatus({ status, ...rest }) { + const s = listingStatus(status); + return ( + + {s.label} + + ); +} diff --git a/resources/js/components/Timeline.jsx b/resources/js/components/Timeline.jsx new file mode 100644 index 0000000..c5c2a38 --- /dev/null +++ b/resources/js/components/Timeline.jsx @@ -0,0 +1,75 @@ +import Icon from './Icon'; +import { dateTime, humanize } from '../lib/format'; + +const ACTIONS = { + upload: { icon: 'arrowUp', text: 'Uploaded' }, + check: { icon: 'spinner', text: 'Checks started', tone: 'wait' }, + pass_checks: { icon: 'check', text: 'Automated checks passed', tone: 'ok' }, + fail_checks: { icon: 'x', text: 'Automated checks failed', tone: 'fail' }, + submit: { icon: 'send', text: 'Submitted for review', tone: 'wait' }, + claim: { icon: 'eye', text: 'Review started', tone: 'wait' }, + approve: { icon: 'check', text: 'Approved', tone: 'ok' }, + reject: { icon: 'x', text: 'Changes requested', tone: 'fail' }, + publish: { icon: 'globe', text: 'Published', tone: 'ok' }, + withdraw: { icon: 'arrowLeft', text: 'Withdrawn from review' }, + yank: { icon: 'minus', text: 'Yanked' }, +}; + +function who(entry) { + if (entry.by?.name) { + return entry.by.name; + } + return { system: 'Registry', staff: 'Fleetbase review team', publisher: 'Publisher' }[entry.actor] ?? null; +} + +/** + * Review timeline entries (`reviews` rows), newest first, with reasons and failed R-items. Entries + * arrive oldest first and several can share a second, so ties keep that order reversed. + */ +export default function Timeline({ entries = [], itemLabels = {} }) { + const sorted = entries + .map((entry, index) => ({ entry, index })) + .sort((a, b) => new Date(b.entry.created_at) - new Date(a.entry.created_at) || b.index - a.index) + .map(({ entry }) => entry); + + if (!sorted.length) { + return

Nothing yet.

; + } + + return ( +
    + {sorted.map((entry, index) => { + const action = ACTIONS[entry.action] ?? { icon: entry.icon ?? 'minus', text: entry.text ?? humanize(entry.action) }; + const failed = (entry.items ?? []).filter((item) => item.passed === false); + return ( +
  1. + + + +
    +
    + {action.text} + {entry.version && ` · v${entry.version}`} + {failed.length > 0 && · {failed.length} to fix} +
    +
    + {[who(entry), dateTime(entry.created_at)].filter(Boolean).join(' · ')} +
    + {entry.reason &&
    {entry.reason}
    } + {failed.length > 0 && ( +
      + {failed.map((item) => ( +
    • + {item.code} {itemLabels[item.code] ?? ''} + {item.note && : {item.note}} +
    • + ))} +
    + )} +
    +
  2. + ); + })} +
+ ); +} diff --git a/resources/js/components/Toasts.jsx b/resources/js/components/Toasts.jsx new file mode 100644 index 0000000..86933d1 --- /dev/null +++ b/resources/js/components/Toasts.jsx @@ -0,0 +1,51 @@ +import { createContext, useCallback, useContext, useEffect, useRef, useState } from 'react'; +import { usePage } from '@inertiajs/react'; +import Icon from './Icon'; + +const ToastContext = createContext({ show: () => {} }); + +/** Toasts: short confirmations after an action, announced politely. */ +export function ToastProvider({ children }) { + const [toasts, setToasts] = useState([]); + const id = useRef(0); + const { flash } = usePage().props; + + const dismiss = useCallback((key) => setToasts((list) => list.filter((toast) => toast.key !== key)), []); + + const show = useCallback( + (message, { type = 'ok', action } = {}) => { + const key = ++id.current; + setToasts((list) => [...list.slice(-2), { key, message, type, action }]); + setTimeout(() => dismiss(key), type === 'error' ? 8000 : 4500); + }, + [dismiss] + ); + + useEffect(() => { + if (flash?.message) { + show(flash.message, { type: flash.type ?? 'ok' }); + } + }, [flash, show]); + + return ( + + {children} +
+ {toasts.map((toast) => ( +
+ + {toast.message} + {toast.action} + +
+ ))} +
+
+ ); +} + +export function useToast() { + return useContext(ToastContext).show; +} diff --git a/resources/js/components/Trust.jsx b/resources/js/components/Trust.jsx new file mode 100644 index 0000000..f8fae7d --- /dev/null +++ b/resources/js/components/Trust.jsx @@ -0,0 +1,29 @@ +import Icon from './Icon'; + +/** The three trust marks; each means one fact the registry can prove. */ +export function Verified({ short = false, className = '' }) { + return ( + + + {short ? 'Verified' : 'Verified publisher'} + + ); +} + +export function Reviewed({ short = false, className = '' }) { + return ( + + + {short ? 'Reviewed' : 'Reviewed by Fleetbase'} + + ); +} + +export function WorksWith({ version, className = '' }) { + return ( + + + Works with {version} + + ); +} diff --git a/resources/js/components/Ui.jsx b/resources/js/components/Ui.jsx new file mode 100644 index 0000000..fc18760 --- /dev/null +++ b/resources/js/components/Ui.jsx @@ -0,0 +1,112 @@ +import { useEffect, useId, useState } from 'react'; +import DOMPurify from 'dompurify'; +import Icon from './Icon'; + +/** Empty, no-results and error states: what happened, and the one next step. */ +export function EmptyState({ icon = 'box', tone, title, children, actions }) { + const glyph = tone === 'fail' ? { background: 'var(--fail-bg)', color: 'var(--fail)' } : tone === 'you' ? { background: 'var(--you-bg)', color: 'var(--you)' } : undefined; + return ( +
+ +

{title}

+ {children &&
{children}
} + {actions &&
{actions}
} +
+ ); +} + +/** A callout: info (blue), you (amber), fail (red), ok (green). */ +export function Note({ tone = 'info', icon, children, className = '', ...rest }) { + const icons = { info: 'info', you: 'warning', fail: 'alert', ok: 'check' }; + return ( +
+ +
{children}
+
+ ); +} + +/** + * A labelled form field. The input gets the generated id through the render prop, so the label + * always names it: `{(id) => }`. + */ +export function Field({ label, optional, help, error, children, className = '' }) { + const id = useId(); + const helpId = `${id}-help`; + return ( +
+ + {children(id, { 'aria-describedby': help || error ? helpId : undefined, 'aria-invalid': error ? true : undefined })} + {error ? ( + + + {error} + + ) : ( + help && ( + + {help} + + ) + )} +
+ ); +} + +/** A segmented control: tabs (`role="tab"`) or radios (`role="radio"`). */ +export function Segmented({ options, value, onChange, label, role = 'tab', className = '' }) { + return ( +
+ {options.map((option) => ( + + ))} +
+ ); +} + +export function Skeleton({ width = '100%', height = 12, radius = 8, className = '' }) { + return