From ff16dd282c73e33e316944a261c33a03032c3c5b Mon Sep 17 00:00:00 2001 From: Naffah Abdulla Rasheed Date: Wed, 4 Nov 2026 14:13:33 +0500 Subject: [PATCH 01/10] Foundations: PHP 8.4, Laravel 12/13, Pint, CI matrix and Codecov Closes #1 Co-Authored-By: Claude Opus 5.5 --- .gitattributes | 4 + .github/coverage.svg | 20 - .github/workflows/run-tests.yml | 73 -- .github/workflows/tests.yml | 82 ++ .gitignore | 3 + .php_cs.dist.php | 35 - CHANGELOG.md | 7 +- LICENSE | 21 - LICENSE.md | 2 +- README.md | 9 +- clover.xml | 895 ------------------ composer.json | 41 +- config/translatable.php | 18 +- phpunit.xml.dist | 8 +- pint.json | 52 +- src/DbTranslatable/IsDbTranslatable.php | 22 +- src/Enums/Flags.php | 508 +++++----- src/JsonTranslatable/IsJsonTranslatable.php | 24 +- src/LanguageRegistrar.php | 4 +- src/Languages.php | 8 +- src/Middleware/LocaleMiddleware.php | 6 +- src/Models/Language.php | 12 +- src/Traits/IsTranslatable.php | 4 +- src/Traits/UrlGenerators/BaseUrlGenerator.php | 11 - .../DbTranslatedUrlGenerator.php | 2 +- .../JsonTranslatedUrlGenerator.php | 4 +- src/TranslatableServiceProvider.php | 4 +- src/helpers.php | 27 +- tests/Feature/IsJsonTranslatableCRUDTest.php | 56 +- .../TestSupport/Factories/ArticleFactory.php | 8 +- tests/TestSupport/Factories/AuthorFactory.php | 2 +- tests/TestSupport/Factories/PostFactory.php | 8 +- tests/TestSupport/Models/Article.php | 2 +- tests/TestSupport/Models/Post.php | 2 +- .../DbTranslatable/IsDbTranslatableTest.php | 236 ++--- tests/Unit/Facades/LanguageFacadeTest.php | 18 +- .../IsJsonTranslatableTest.php | 148 +-- tests/Unit/LocaleMiddlewareTest.php | 18 +- tests/Unit/Models/LanguageTest.php | 12 +- tests/Unit/Models/ModelTest.php | 2 +- .../TableCellTranslatedUrlGenerationTest.php | 34 +- 41 files changed, 730 insertions(+), 1722 deletions(-) delete mode 100644 .github/coverage.svg delete mode 100644 .github/workflows/run-tests.yml create mode 100644 .github/workflows/tests.yml delete mode 100644 .php_cs.dist.php delete mode 100644 LICENSE delete mode 100644 clover.xml diff --git a/.gitattributes b/.gitattributes index bb6265e..f577fdc 100644 --- a/.gitattributes +++ b/.gitattributes @@ -9,3 +9,7 @@ /.scrutinizer.yml export-ignore /tests export-ignore /.editorconfig export-ignore +/.github export-ignore +/pint.json export-ignore +/AGENTS.md export-ignore +/CLAUDE.md export-ignore diff --git a/.github/coverage.svg b/.github/coverage.svg deleted file mode 100644 index 8cb18f9..0000000 --- a/.github/coverage.svg +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - - - - - - - - - - coverage - - 80 % - - \ No newline at end of file diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml deleted file mode 100644 index 9fe8e22..0000000 --- a/.github/workflows/run-tests.yml +++ /dev/null @@ -1,73 +0,0 @@ -name: tests - -on: - pull_request: - branches: - - main - push: - branches: - - main - - dev - -jobs: - tests: - runs-on: ${{ matrix.os }} - - strategy: - fail-fast: true - matrix: - os: [ ubuntu-latest ] - php: [ 8.4, 8.3, 8.2 ] - laravel: [ 13.*, 12.*, 11.*, 10.* ] - stability: [ prefer-lowest, prefer-stable ] - include: - - laravel: 13.* - testbench: 11.* - carbon: ^3.8.4 - - laravel: 12.* - testbench: 10.* - carbon: 3.* - - laravel: 11.* - testbench: ^9.16 - carbon: ^2.63 - - laravel: 10.* - testbench: 8.* - carbon: ^2.63 - exclude: - - laravel: 13.* - php: 8.2 - - name: P${{ matrix.php }} - L${{ matrix.laravel }} - ${{ matrix.stability }} - ${{ matrix.os }} - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Install SQLite 3 - run: | - sudo apt-get update - sudo apt-get install sqlite3 -y - - - name: Setup PHP - uses: shivammathur/setup-php@v2 - with: - php-version: ${{ matrix.php }} - extensions: curl, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, iconv - coverage: ${{ startsWith(github.event.head_commit.message, 'coverage') && matrix.php == '8.4' && matrix.laravel == '13.*' && matrix.stability == 'prefer-stable' && 'xdebug' || 'none' }} - - - name: Install dependencies - run: | - composer require "laravel/framework:${{ matrix.laravel }}" "orchestra/testbench:${{ matrix.testbench }}" "nesbot/carbon:${{ matrix.carbon }}" --no-interaction --no-update - composer update --${{ matrix.stability }} --prefer-dist --no-interaction - - - name: Execute tests - run: | - vendor/bin/phpunit ${{ startsWith(github.event.head_commit.message, 'coverage') && matrix.php == '8.4' && matrix.laravel == '13.*' && matrix.stability == 'prefer-stable' && '--coverage-clover=clover.xml' || '' }} - - - name: Make code coverage badge - if: startsWith(github.event.head_commit.message, 'coverage') && matrix.php == '8.4' && matrix.laravel == '13.*' && matrix.stability == 'prefer-stable' - uses: timkrase/phpunit-coverage-badge@v1.2.1 - with: - coverage_badge_path: .github/coverage.svg - push_badge: true - repo_token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..4b5113b --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,82 @@ +name: tests + +on: + push: + branches: + - main + - next-gen + pull_request: + +# Unreleased javaabu/* dependencies come from VCS repositories; the job's own +# token keeps Composer within GitHub's API limits +env: + COMPOSER_AUTH: '{"github-oauth": {"github.com": "${{ github.token }}"}}' + +jobs: + tests: + runs-on: ubuntu-latest + + strategy: + fail-fast: false + matrix: + php: ['8.4', '8.5'] + laravel: ['12.*', '13.*'] + stability: [prefer-lowest, prefer-stable] + include: + - laravel: '12.*' + testbench: '10.*' + - laravel: '13.*' + testbench: '11.*' + + name: PHP ${{ matrix.php }} - Laravel ${{ matrix.laravel }} - ${{ matrix.stability }} + + env: + COVERAGE: ${{ matrix.php == '8.4' && matrix.laravel == '13.*' && matrix.stability == 'prefer-stable' }} + + steps: + - name: Checkout code + uses: actions/checkout@v5 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + extensions: curl, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, iconv, intl + coverage: ${{ env.COVERAGE == 'true' && 'xdebug' || 'none' }} + + - name: Install dependencies + run: | + composer require "laravel/framework:${{ matrix.laravel }}" "orchestra/testbench:${{ matrix.testbench }}" --dev --no-interaction --no-update + composer update --${{ matrix.stability }} --prefer-dist --no-interaction --no-progress + + - name: Execute tests + run: vendor/bin/phpunit ${{ env.COVERAGE == 'true' && '--coverage-clover=clover.xml' || '' }} + + - name: Upload coverage to Codecov + if: env.COVERAGE == 'true' + uses: codecov/codecov-action@v5 + with: + files: clover.xml + fail_ci_if_error: false + token: ${{ secrets.CODECOV_TOKEN }} + + pint: + runs-on: ubuntu-latest + + name: Pint + + steps: + - name: Checkout code + uses: actions/checkout@v5 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '8.4' + coverage: none + + - name: Install dependencies + run: composer update --prefer-dist --no-interaction --no-progress + + - name: Check code style + run: vendor/bin/pint --test diff --git a/.gitignore b/.gitignore index 7a3e63f..11a2aba 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,9 @@ build composer.lock vendor coverage +clover.xml +node_modules .phpunit.result.cache +.phpunit.cache .php-cs-fixer.cache .idea diff --git a/.php_cs.dist.php b/.php_cs.dist.php deleted file mode 100644 index 8648fdc..0000000 --- a/.php_cs.dist.php +++ /dev/null @@ -1,35 +0,0 @@ -in([ - __DIR__ . '/src', - __DIR__ . '/tests', - ]) - ->name('*.php') - ->notName('*.blade.php') - ->ignoreDotFiles(true) - ->ignoreVCS(true); - -return (new PhpCsFixer\Config()) - ->setRules([ - '@PSR12' => true, - 'array_syntax' => ['syntax' => 'short'], - 'ordered_imports' => ['sort_algorithm' => 'alpha'], - 'no_unused_imports' => true, - 'not_operator_with_successor_space' => true, - 'trailing_comma_in_multiline' => true, - 'phpdoc_scalar' => true, - 'unary_operator_spaces' => true, - 'binary_operator_spaces' => true, - 'blank_line_before_statement' => [ - 'statements' => ['break', 'continue', 'declare', 'return', 'throw', 'try'], - ], - 'phpdoc_single_line_var_spacing' => true, - 'phpdoc_var_without_name' => true, - 'method_argument_space' => [ - 'on_multiline' => 'ensure_fully_multiline', - 'keep_multiple_spaces_after_comma' => true, - ], - 'single_trait_insert_per_statement' => true, - ]) - ->setFinder($finder); diff --git a/CHANGELOG.md b/CHANGELOG.md index 8869e77..0768fe6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,8 @@ # Changelog -All notable changes will be documented in this file +All notable changes to `javaabu/translatable` are documented in this file. -## 1.0.0 - 202X-XX-XX +## 3.0.0 - Unreleased -- initial release +- Requires PHP 8.4 and Laravel 12 or 13 +- Javaabu codestyle enforced with Pint; CI runs PHP 8.4/8.5 × Laravel 12/13 × lowest/stable, with coverage on Codecov diff --git a/LICENSE b/LICENSE deleted file mode 100644 index e3e4a82..0000000 --- a/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2024 Javaabu - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/LICENSE.md b/LICENSE.md index 8834892..4104bba 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -18,4 +18,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. \ No newline at end of file +SOFTWARE. diff --git a/README.md b/README.md index 77a6ac4..9e6d65e 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,9 @@ # Translatable -[![Latest Version on Packagist](https://img.shields.io/packagist/v/javaabu/translatable.svg?style=flat-square)](https://packagist.org/packages/javaabu/translatable) -[![Test Status](../../actions/workflows/run-tests.yml/badge.svg)](../../actions/workflows/run-tests.yml) -[![Total Downloads](https://img.shields.io/packagist/dt/javaabu/translatable.svg?style=flat-square)](https://packagist.org/packages/javaabu/translatable) -![Code Coverage](./.github/coverage.svg) - +[![Tests](https://github.com/n4ff4h/translatable/actions/workflows/tests.yml/badge.svg?branch=next-gen)](https://github.com/n4ff4h/translatable/actions/workflows/tests.yml?query=branch%3Anext-gen) +[![codecov](https://codecov.io/gh/n4ff4h/translatable/branch/next-gen/graph/badge.svg)](https://codecov.io/gh/n4ff4h/translatable/tree/next-gen) +[![Latest Version on Packagist](https://img.shields.io/packagist/v/javaabu/translatable.svg)](https://packagist.org/packages/javaabu/translatable) +[![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE.md) Adds multi-lingual to Laravel models diff --git a/clover.xml b/clover.xml deleted file mode 100644 index 46506ab..0000000 --- a/clover.xml +++ /dev/null @@ -1,895 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/composer.json b/composer.json index 7e5e964..a868de4 100644 --- a/composer.json +++ b/composer.json @@ -1,12 +1,17 @@ { "name": "javaabu/translatable", - "description": "Adds multi-lingual to Laravel models", + "description": "Multi-lingual Eloquent models for Laravel, stored as rows per locale or as JSON, with Blade and Inertia React components", "homepage": "https://github.com/Javaabu/translatable", "license": "MIT", "keywords": [ "javaabu", "laravel", - "translatable" + "translatable", + "translations", + "multilingual", + "i18n", + "dhivehi", + "rtl" ], "authors": [ { @@ -26,17 +31,20 @@ } ], "require": { - "php": "^8.2", - "illuminate/support": "^9.0 || ^10.0 || ^11.0 || ^12.0 || ^13.0", - "javaabu/helpers": "^1.70" - }, - "suggest": { - "javaabu/forms": "For the little buttons and table cells that are provided." + "php": "^8.4", + "illuminate/database": "^12.0 || ^13.0", + "illuminate/support": "^12.0 || ^13.0", + "javaabu/helpers": "^1.74" }, "require-dev": { - "laravel/pint": "^1.14", - "orchestra/testbench": "^7.0 || ^8.0 || ^9.16 || ^10.0 || ^11.0", - "phpunit/phpunit": "^9.5 || ^10.5 || ^11.5.3 || ^12.5.8 || ^13.0.3" + "javaabu/boost-javaabu-guidelines": "^1.4", + "laravel/pint": "^1.25", + "orchestra/testbench": "^10.0 || ^11.0", + "phpunit/phpunit": "^11.5.3 || ^12.5.8" + }, + "suggest": { + "javaabu/forms": "Required for the Blade components (^2.0)", + "javaabu/manifest": "Ships the Inertia React components to your app (^1.0)" }, "autoload": { "psr-4": { @@ -51,9 +59,15 @@ "Javaabu\\Translatable\\Tests\\": "tests/" } }, + "scripts": { + "test": "vendor/bin/phpunit", + "test-coverage": "vendor/bin/phpunit --coverage-clover=clover.xml", + "lint": "vendor/bin/pint --test", + "format": "vendor/bin/pint" + }, "extra": { "branch-alias": { - "dev-main": "2.0-dev" + "dev-next-gen": "3.x-dev" }, "laravel": { "providers": [ @@ -61,6 +75,9 @@ ] } }, + "config": { + "sort-packages": true + }, "minimum-stability": "dev", "prefer-stable": true } diff --git a/config/translatable.php b/config/translatable.php index c4821cf..18cb51b 100644 --- a/config/translatable.php +++ b/config/translatable.php @@ -1,8 +1,10 @@ Javaabu\Translatable\Models\Language::class, + 'language_model' => Language::class, // Standard fields to be ignored for translation on all models 'fields_ignored_for_translation' => [ @@ -14,27 +16,27 @@ ], // Whether attr_dv should fall back to app locale if translations do not exist - 'lang_suffix_should_fallback' => false, + 'lang_suffix_should_fallback' => false, // Default locale before translations are added - 'default_locale' => 'en', + 'default_locale' => 'en', // Cache configuration for the Language Registrar - 'cache' => [ + 'cache' => [ 'expiration_time' => DateInterval::createFromDateString('24 hours'), - 'key' => 'translation.languages.cache', - 'driver' => 'default', + 'key' => 'translation.languages.cache', + 'driver' => 'default', ], 'styles' => [ 'table-cell-wrapper' => 'd-flex justify-content-center align-items-center', 'icons' => [ - 'add' => 'fas fa-plus', + 'add' => 'fas fa-plus', 'edit' => 'fas fa-edit', ], ], - 'default_portal' => 'web', + 'default_portal' => 'web', 'default_redirect_route' => 'home', ]; diff --git a/phpunit.xml.dist b/phpunit.xml.dist index ad2440d..b360c33 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,5 +1,6 @@ -src - - - - - diff --git a/pint.json b/pint.json index bdf97a2..44df8bf 100644 --- a/pint.json +++ b/pint.json @@ -1,60 +1,20 @@ { "preset": "laravel", "rules": { - "align_multiline_comment": true, - "array_indentation": true, - "array_syntax": true, - "blank_line_after_namespace": true, - "blank_line_after_opening_tag": true, - "combine_consecutive_issets": true, - "combine_consecutive_unsets": true, - "binary_operator_spaces": { - "operators": { - "=>" : "align" - } - }, "concat_space": { "spacing": "one" }, - "explicit_string_variable": true, - "fully_qualified_strict_types": true, "global_namespace_import": { "import_classes": true, - "import_constants": true, - "import_functions": true - }, - "is_null": true, - "lambda_not_used_import": true, - "logical_operators": true, - "mb_str_functions": true, - "method_chaining_indentation": true, - "modernize_strpos": true, - "new_with_braces": { - "anonymous_class": false + "import_constants": false, + "import_functions": false }, - "no_empty_comment": true, - "not_operator_with_space": true, - "ordered_traits": true, - "protected_to_private": true, - "simplified_if_return": true, - "strict_comparison": true, - "ternary_to_null_coalescing": true, - "trim_array_spaces": true, - "array_push": true, - "assign_null_coalescing_to_coalesce_equal": true, - "explicit_indirect_variable": true, - "method_argument_space": { - "on_multiline": "ensure_fully_multiline" - }, - "modernize_types_casting": true, - "no_superfluous_elseif": true, - "no_superfluous_phpdoc_tags": true, - "no_useless_else": true, - "phpdoc_trim": true, - "no_trailing_whitespace_in_comment": true, - "nullable_type_declaration_for_default_null_value": true, + "no_unused_imports": true, "ordered_imports": { "sort_algorithm": "alpha" + }, + "php_unit_method_casing": { + "case": "snake_case" } } } diff --git a/src/DbTranslatable/IsDbTranslatable.php b/src/DbTranslatable/IsDbTranslatable.php index 6bd0d6b..62501db 100644 --- a/src/DbTranslatable/IsDbTranslatable.php +++ b/src/DbTranslatable/IsDbTranslatable.php @@ -74,7 +74,7 @@ public function getTranslation(?string $locale = null): ?static $defaultTranslation = $this->isDefaultTranslation() ? $this : $this->defaultTranslation; // Return fast if there is no default translation - if ( ! $defaultTranslation) { + if (! $defaultTranslation) { return null; } @@ -95,12 +95,12 @@ public function translate(string $field, ?string $locale = null, bool $fallback } // If the locale is not allowed then return null - if ( ! $this->isAllowedTranslationLocale($locale)) { + if (! $this->isAllowedTranslationLocale($locale)) { return $fallback ? $this->getAttributeValue($field) : null; } // If the field is not in the translatable fields list then return null - if ( ! $this->isTranslatable($field)) { + if (! $this->isTranslatable($field)) { return $fallback ? $this->getAttributeValue($field) : null; } @@ -125,7 +125,7 @@ public function getDefaultTranslationLocale(): string public function clearTranslations(?string $locale = null): void { - if ( ! $locale) { + if (! $locale) { // nuke all except the main one $parent_id = $this->isDefaultTranslation() ? $this->id : $this->translatable_parent_id; self::query()->where('translatable_parent_id', $parent_id)->withTrashed()->forceDelete(); @@ -161,9 +161,9 @@ public function hasTranslation(?string $locale = null): bool } $translation = self::query()->where([ 'translatable_parent_id' => $this->translatable_parent_id, - 'lang' => $locale, + 'lang' => $locale, ])->orWhere([ - 'id' => $this->translatable_parent_id, + 'id' => $this->translatable_parent_id, 'lang' => $locale, ]); @@ -177,11 +177,11 @@ public function hasTranslation(?string $locale = null): bool */ public function addTranslation(string $locale, string $field, string $value): static { - if ( ! $this->isAllowedTranslationLocale($locale)) { + if (! $this->isAllowedTranslationLocale($locale)) { throw LanguageNotAllowedException::create($locale); } - if ( ! $this->isTranslatable($field)) { + if (! $this->isTranslatable($field)) { throw FieldNotAllowedException::create($field, $locale); } @@ -202,8 +202,8 @@ public function addTranslation(string $locale, string $field, string $value): st $newTranslation = $defaultTranslation->translations()->where('lang', $locale)->first(); // if there is none, make a new blank translation of this object - if ( ! $newTranslation) { - $newTranslation = new self(); + if (! $newTranslation) { + $newTranslation = new self; // copy all the attributes of the current object to the new translation // this ensures no columns are left null @@ -266,6 +266,6 @@ public function scopeRootTranslations(Builder $query): Builder */ public function canUpdateTranslatableParent(): bool { - return ( ! $this->isRootTranslation()) || ( ! $this->hasTranslation()); + return (! $this->isRootTranslation()) || (! $this->hasTranslation()); } } diff --git a/src/Enums/Flags.php b/src/Enums/Flags.php index 5c30773..1547cef 100644 --- a/src/Enums/Flags.php +++ b/src/Enums/Flags.php @@ -56,8 +56,8 @@ public static function listFlags(): array foreach (self::getFlags() as $code => $name) { $list[$code] = [ 'image' => self::getFlagUrl($code), - 'code' => $code, - 'name' => __(':name (:code)', ['name' => $name, 'code' => mb_strtoupper($code)]), + 'code' => $code, + 'name' => __(':name (:code)', ['name' => $name, 'code' => mb_strtoupper($code)]), ]; } @@ -70,261 +70,261 @@ public static function listFlags(): array * @var string[] */ protected static $flags = [ - 'ad' => 'Andorra', - 'ae' => 'United Arab Emirates', - 'af' => 'Afghanistan', - 'ag' => 'Antigua and Barbuda', - 'ai' => 'Anguilla', - 'al' => 'Albania', - 'am' => 'Armenia', - 'an' => 'Netherlands Antilles', - 'ao' => 'Angola', - 'aq' => 'Antarctica', - 'ar' => 'Argentina', - 'as' => 'American Samoa', - 'at' => 'Austria', - 'au' => 'Australia', - 'aw' => 'Aruba', - 'ax' => "\u00c5land Islands", - 'az' => 'Azerbaijan', - 'ba' => 'Bosnia and Herzegovina', - 'bb' => 'Barbados', - 'bd' => 'Bangladesh', - 'be' => 'Belgium', - 'bf' => 'Burkina Faso', - 'bg' => 'Bulgaria', - 'bh' => 'Bahrain', - 'bi' => 'Burundi', - 'bj' => 'Benin', - 'bl' => 'Saint Barthélemy', - 'bm' => 'Bermuda', - 'bn' => 'Brunei Darussalam', - 'bo' => 'Bolivia, Plurinational State of', - 'bq' => 'Caribbean Netherlands', - 'br' => 'Brazil', - 'bs' => 'Bahamas', - 'bt' => 'Bhutan', - 'bv' => 'Bouvet Island', - 'bw' => 'Botswana', - 'by' => 'Belarus', - 'bz' => 'Belize', - 'ca' => 'Canada', - 'cc' => 'Cocos (Keeling) Islands', - 'cd' => 'Congo, the Democratic Republic of the', - 'cf' => 'Central African Republic', - 'cg' => 'Congo', - 'ch' => 'Switzerland', - 'ci' => "C\u00f4te d'Ivoire", - 'ck' => 'Cook Islands', - 'cl' => 'Chile', - 'cm' => 'Cameroon', - 'cn' => 'China', - 'co' => 'Colombia', - 'cr' => 'Costa Rica', - 'cu' => 'Cuba', - 'cv' => 'Cape Verde', - 'cw' => "Cura\u00e7ao", - 'cx' => 'Christmas Island', - 'cy' => 'Cyprus', - 'cz' => 'Czech Republic', - 'de' => 'Germany', - 'dj' => 'Djibouti', - 'dk' => 'Denmark', - 'dm' => 'Dominica', - 'do' => 'Dominican Republic', - 'dz' => 'Algeria', - 'ec' => 'Ecuador', - 'ee' => 'Estonia', - 'eg' => 'Egypt', - 'eh' => 'Western Sahara', - 'er' => 'Eritrea', - 'es' => 'Spain', - 'et' => 'Ethiopia', - 'eu' => 'Europe', - 'fi' => 'Finland', - 'fj' => 'Fiji', - 'fk' => 'Falkland Islands (Malvinas)', - 'fm' => 'Micronesia, Federated States of', - 'fo' => 'Faroe Islands', - 'fr' => 'France', - 'ga' => 'Gabon', + 'ad' => 'Andorra', + 'ae' => 'United Arab Emirates', + 'af' => 'Afghanistan', + 'ag' => 'Antigua and Barbuda', + 'ai' => 'Anguilla', + 'al' => 'Albania', + 'am' => 'Armenia', + 'an' => 'Netherlands Antilles', + 'ao' => 'Angola', + 'aq' => 'Antarctica', + 'ar' => 'Argentina', + 'as' => 'American Samoa', + 'at' => 'Austria', + 'au' => 'Australia', + 'aw' => 'Aruba', + 'ax' => "\u00c5land Islands", + 'az' => 'Azerbaijan', + 'ba' => 'Bosnia and Herzegovina', + 'bb' => 'Barbados', + 'bd' => 'Bangladesh', + 'be' => 'Belgium', + 'bf' => 'Burkina Faso', + 'bg' => 'Bulgaria', + 'bh' => 'Bahrain', + 'bi' => 'Burundi', + 'bj' => 'Benin', + 'bl' => 'Saint Barthélemy', + 'bm' => 'Bermuda', + 'bn' => 'Brunei Darussalam', + 'bo' => 'Bolivia, Plurinational State of', + 'bq' => 'Caribbean Netherlands', + 'br' => 'Brazil', + 'bs' => 'Bahamas', + 'bt' => 'Bhutan', + 'bv' => 'Bouvet Island', + 'bw' => 'Botswana', + 'by' => 'Belarus', + 'bz' => 'Belize', + 'ca' => 'Canada', + 'cc' => 'Cocos (Keeling) Islands', + 'cd' => 'Congo, the Democratic Republic of the', + 'cf' => 'Central African Republic', + 'cg' => 'Congo', + 'ch' => 'Switzerland', + 'ci' => "C\u00f4te d'Ivoire", + 'ck' => 'Cook Islands', + 'cl' => 'Chile', + 'cm' => 'Cameroon', + 'cn' => 'China', + 'co' => 'Colombia', + 'cr' => 'Costa Rica', + 'cu' => 'Cuba', + 'cv' => 'Cape Verde', + 'cw' => "Cura\u00e7ao", + 'cx' => 'Christmas Island', + 'cy' => 'Cyprus', + 'cz' => 'Czech Republic', + 'de' => 'Germany', + 'dj' => 'Djibouti', + 'dk' => 'Denmark', + 'dm' => 'Dominica', + 'do' => 'Dominican Republic', + 'dz' => 'Algeria', + 'ec' => 'Ecuador', + 'ee' => 'Estonia', + 'eg' => 'Egypt', + 'eh' => 'Western Sahara', + 'er' => 'Eritrea', + 'es' => 'Spain', + 'et' => 'Ethiopia', + 'eu' => 'Europe', + 'fi' => 'Finland', + 'fj' => 'Fiji', + 'fk' => 'Falkland Islands (Malvinas)', + 'fm' => 'Micronesia, Federated States of', + 'fo' => 'Faroe Islands', + 'fr' => 'France', + 'ga' => 'Gabon', 'gb-eng' => 'England', 'gb-nir' => 'Northern Ireland', 'gb-sct' => 'Scotland', 'gb-wls' => 'Wales', - 'gb' => 'United Kingdom', - 'gd' => 'Grenada', - 'ge' => 'Georgia', - 'gf' => 'French Guiana', - 'gg' => 'Guernsey', - 'gh' => 'Ghana', - 'gi' => 'Gibraltar', - 'gl' => 'Greenland', - 'gm' => 'Gambia', - 'gn' => 'Guinea', - 'gp' => 'Guadeloupe', - 'gq' => 'Equatorial Guinea', - 'gr' => 'Greece', - 'gs' => 'South Georgia and the South Sandwich Islands', - 'gt' => 'Guatemala', - 'gu' => 'Guam', - 'gw' => 'Guinea-Bissau', - 'gy' => 'Guyana', - 'hk' => 'Hong Kong', - 'hm' => 'Heard Island and McDonald Islands', - 'hn' => 'Honduras', - 'hr' => 'Croatia', - 'ht' => 'Haiti', - 'hu' => 'Hungary', - 'id' => 'Indonesia', - 'ie' => 'Ireland', - 'il' => 'Israel', - 'im' => 'Isle of Man', - 'in' => 'India', - 'io' => 'British Indian Ocean Territory', - 'iq' => 'Iraq', - 'ir' => 'Iran, Islamic Republic of', - 'is' => 'Iceland', - 'it' => 'Italy', - 'je' => 'Jersey', - 'jm' => 'Jamaica', - 'jo' => 'Jordan', - 'jp' => 'Japan', - 'ke' => 'Kenya', - 'kg' => 'Kyrgyzstan', - 'kh' => 'Cambodia', - 'ki' => 'Kiribati', - 'km' => 'Comoros', - 'kn' => 'Saint Kitts and Nevis', - 'kp' => "Korea, Democratic People's Republic of", - 'kr' => 'Korea, Republic of', - 'kw' => 'Kuwait', - 'ky' => 'Cayman Islands', - 'kz' => 'Kazakhstan', - 'la' => "Lao People's Democratic Republic", - 'lb' => 'Lebanon', - 'lc' => 'Saint Lucia', - 'li' => 'Liechtenstein', - 'lk' => 'Sri Lanka', - 'lr' => 'Liberia', - 'ls' => 'Lesotho', - 'lt' => 'Lithuania', - 'lu' => 'Luxembourg', - 'lv' => 'Latvia', - 'ly' => 'Libya', - 'ma' => 'Morocco', - 'mc' => 'Monaco', - 'md' => 'Moldova, Republic of', - 'me' => 'Montenegro', - 'mf' => 'Saint Martin', - 'mg' => 'Madagascar', - 'mh' => 'Marshall Islands', - 'mk' => 'Macedonia, the former Yugoslav Republic of', - 'ml' => 'Mali', - 'mm' => 'Myanmar', - 'mn' => 'Mongolia', - 'mo' => 'Macao', - 'mp' => 'Northern Mariana Islands', - 'mq' => 'Martinique', - 'mr' => 'Mauritania', - 'ms' => 'Montserrat', - 'mt' => 'Malta', - 'mu' => 'Mauritius', - 'mv' => 'Maldives', - 'mw' => 'Malawi', - 'mx' => 'Mexico', - 'my' => 'Malaysia', - 'mz' => 'Mozambique', - 'na' => 'Namibia', - 'nc' => 'New Caledonia', - 'ne' => 'Niger', - 'nf' => 'Norfolk Island', - 'ng' => 'Nigeria', - 'ni' => 'Nicaragua', - 'nl' => 'Netherlands', - 'no' => 'Norway', - 'np' => 'Nepal', - 'nr' => 'Nauru', - 'nu' => 'Niue', - 'nz' => 'New Zealand', - 'om' => 'Oman', - 'pa' => 'Panama', - 'pe' => 'Peru', - 'pf' => 'French Polynesia', - 'pg' => 'Papua New Guinea', - 'ph' => 'Philippines', - 'pk' => 'Pakistan', - 'pl' => 'Poland', - 'pm' => 'Saint Pierre and Miquelon', - 'pn' => 'Pitcairn', - 'pr' => 'Puerto Rico', - 'ps' => 'Palestine', - 'pt' => 'Portugal', - 'pw' => 'Palau', - 'py' => 'Paraguay', - 'qa' => 'Qatar', - 're' => 'Réunion', - 'ro' => 'Romania', - 'rs' => 'Serbia', - 'ru' => 'Russian Federation', - 'rw' => 'Rwanda', - 'sa' => 'Saudi Arabia', - 'sb' => 'Solomon Islands', - 'sc' => 'Seychelles', - 'sd' => 'Sudan', - 'se' => 'Sweden', - 'sg' => 'Singapore', - 'sh' => 'Saint Helena, Ascension and Tristan da Cunha', - 'si' => 'Slovenia', - 'sj' => 'Svalbard and Jan Mayen Islands', - 'sk' => 'Slovakia', - 'sl' => 'Sierra Leone', - 'sm' => 'San Marino', - 'sn' => 'Senegal', - 'so' => 'Somalia', - 'sr' => 'Suriname', - 'ss' => 'South Sudan', - 'st' => 'Sao Tome and Principe', - 'sv' => 'El Salvador', - 'sx' => 'Sint Maarten (Dutch part)', - 'sy' => 'Syrian Arab Republic', - 'sz' => 'Swaziland', - 'tc' => 'Turks and Caicos Islands', - 'td' => 'Chad', - 'tf' => 'French Southern Territories', - 'tg' => 'Togo', - 'th' => 'Thailand', - 'tj' => 'Tajikistan', - 'tk' => 'Tokelau', - 'tl' => 'Timor-Leste', - 'tm' => 'Turkmenistan', - 'tn' => 'Tunisia', - 'to' => 'Tonga', - 'tr' => 'Turkey', - 'tt' => 'Trinidad and Tobago', - 'tv' => 'Tuvalu', - 'tw' => 'Taiwan', - 'tz' => 'Tanzania, United Republic of', - 'ua' => 'Ukraine', - 'ug' => 'Uganda', - 'um' => 'US Minor Outlying Islands', - 'us' => 'United States', - 'uy' => 'Uruguay', - 'uz' => 'Uzbekistan', - 'va' => 'Holy See (Vatican City State)', - 'vc' => 'Saint Vincent and the Grenadines', - 've' => 'Venezuela, Bolivarian Republic of', - 'vg' => 'Virgin Islands, British', - 'vi' => 'Virgin Islands, U.S.', - 'vn' => 'Viet Nam', - 'vu' => 'Vanuatu', - 'wf' => 'Wallis and Futuna Islands', - 'xk' => 'Kosovo', - 'ws' => 'Samoa', - 'ye' => 'Yemen', - 'yt' => 'Mayotte', - 'za' => 'South Africa', - 'zm' => 'Zambia', - 'zw' => 'Zimbabwe', + 'gb' => 'United Kingdom', + 'gd' => 'Grenada', + 'ge' => 'Georgia', + 'gf' => 'French Guiana', + 'gg' => 'Guernsey', + 'gh' => 'Ghana', + 'gi' => 'Gibraltar', + 'gl' => 'Greenland', + 'gm' => 'Gambia', + 'gn' => 'Guinea', + 'gp' => 'Guadeloupe', + 'gq' => 'Equatorial Guinea', + 'gr' => 'Greece', + 'gs' => 'South Georgia and the South Sandwich Islands', + 'gt' => 'Guatemala', + 'gu' => 'Guam', + 'gw' => 'Guinea-Bissau', + 'gy' => 'Guyana', + 'hk' => 'Hong Kong', + 'hm' => 'Heard Island and McDonald Islands', + 'hn' => 'Honduras', + 'hr' => 'Croatia', + 'ht' => 'Haiti', + 'hu' => 'Hungary', + 'id' => 'Indonesia', + 'ie' => 'Ireland', + 'il' => 'Israel', + 'im' => 'Isle of Man', + 'in' => 'India', + 'io' => 'British Indian Ocean Territory', + 'iq' => 'Iraq', + 'ir' => 'Iran, Islamic Republic of', + 'is' => 'Iceland', + 'it' => 'Italy', + 'je' => 'Jersey', + 'jm' => 'Jamaica', + 'jo' => 'Jordan', + 'jp' => 'Japan', + 'ke' => 'Kenya', + 'kg' => 'Kyrgyzstan', + 'kh' => 'Cambodia', + 'ki' => 'Kiribati', + 'km' => 'Comoros', + 'kn' => 'Saint Kitts and Nevis', + 'kp' => "Korea, Democratic People's Republic of", + 'kr' => 'Korea, Republic of', + 'kw' => 'Kuwait', + 'ky' => 'Cayman Islands', + 'kz' => 'Kazakhstan', + 'la' => "Lao People's Democratic Republic", + 'lb' => 'Lebanon', + 'lc' => 'Saint Lucia', + 'li' => 'Liechtenstein', + 'lk' => 'Sri Lanka', + 'lr' => 'Liberia', + 'ls' => 'Lesotho', + 'lt' => 'Lithuania', + 'lu' => 'Luxembourg', + 'lv' => 'Latvia', + 'ly' => 'Libya', + 'ma' => 'Morocco', + 'mc' => 'Monaco', + 'md' => 'Moldova, Republic of', + 'me' => 'Montenegro', + 'mf' => 'Saint Martin', + 'mg' => 'Madagascar', + 'mh' => 'Marshall Islands', + 'mk' => 'Macedonia, the former Yugoslav Republic of', + 'ml' => 'Mali', + 'mm' => 'Myanmar', + 'mn' => 'Mongolia', + 'mo' => 'Macao', + 'mp' => 'Northern Mariana Islands', + 'mq' => 'Martinique', + 'mr' => 'Mauritania', + 'ms' => 'Montserrat', + 'mt' => 'Malta', + 'mu' => 'Mauritius', + 'mv' => 'Maldives', + 'mw' => 'Malawi', + 'mx' => 'Mexico', + 'my' => 'Malaysia', + 'mz' => 'Mozambique', + 'na' => 'Namibia', + 'nc' => 'New Caledonia', + 'ne' => 'Niger', + 'nf' => 'Norfolk Island', + 'ng' => 'Nigeria', + 'ni' => 'Nicaragua', + 'nl' => 'Netherlands', + 'no' => 'Norway', + 'np' => 'Nepal', + 'nr' => 'Nauru', + 'nu' => 'Niue', + 'nz' => 'New Zealand', + 'om' => 'Oman', + 'pa' => 'Panama', + 'pe' => 'Peru', + 'pf' => 'French Polynesia', + 'pg' => 'Papua New Guinea', + 'ph' => 'Philippines', + 'pk' => 'Pakistan', + 'pl' => 'Poland', + 'pm' => 'Saint Pierre and Miquelon', + 'pn' => 'Pitcairn', + 'pr' => 'Puerto Rico', + 'ps' => 'Palestine', + 'pt' => 'Portugal', + 'pw' => 'Palau', + 'py' => 'Paraguay', + 'qa' => 'Qatar', + 're' => 'Réunion', + 'ro' => 'Romania', + 'rs' => 'Serbia', + 'ru' => 'Russian Federation', + 'rw' => 'Rwanda', + 'sa' => 'Saudi Arabia', + 'sb' => 'Solomon Islands', + 'sc' => 'Seychelles', + 'sd' => 'Sudan', + 'se' => 'Sweden', + 'sg' => 'Singapore', + 'sh' => 'Saint Helena, Ascension and Tristan da Cunha', + 'si' => 'Slovenia', + 'sj' => 'Svalbard and Jan Mayen Islands', + 'sk' => 'Slovakia', + 'sl' => 'Sierra Leone', + 'sm' => 'San Marino', + 'sn' => 'Senegal', + 'so' => 'Somalia', + 'sr' => 'Suriname', + 'ss' => 'South Sudan', + 'st' => 'Sao Tome and Principe', + 'sv' => 'El Salvador', + 'sx' => 'Sint Maarten (Dutch part)', + 'sy' => 'Syrian Arab Republic', + 'sz' => 'Swaziland', + 'tc' => 'Turks and Caicos Islands', + 'td' => 'Chad', + 'tf' => 'French Southern Territories', + 'tg' => 'Togo', + 'th' => 'Thailand', + 'tj' => 'Tajikistan', + 'tk' => 'Tokelau', + 'tl' => 'Timor-Leste', + 'tm' => 'Turkmenistan', + 'tn' => 'Tunisia', + 'to' => 'Tonga', + 'tr' => 'Turkey', + 'tt' => 'Trinidad and Tobago', + 'tv' => 'Tuvalu', + 'tw' => 'Taiwan', + 'tz' => 'Tanzania, United Republic of', + 'ua' => 'Ukraine', + 'ug' => 'Uganda', + 'um' => 'US Minor Outlying Islands', + 'us' => 'United States', + 'uy' => 'Uruguay', + 'uz' => 'Uzbekistan', + 'va' => 'Holy See (Vatican City State)', + 'vc' => 'Saint Vincent and the Grenadines', + 've' => 'Venezuela, Bolivarian Republic of', + 'vg' => 'Virgin Islands, British', + 'vi' => 'Virgin Islands, U.S.', + 'vn' => 'Viet Nam', + 'vu' => 'Vanuatu', + 'wf' => 'Wallis and Futuna Islands', + 'xk' => 'Kosovo', + 'ws' => 'Samoa', + 'ye' => 'Yemen', + 'yt' => 'Mayotte', + 'za' => 'South Africa', + 'zm' => 'Zambia', + 'zw' => 'Zimbabwe', ]; } diff --git a/src/JsonTranslatable/IsJsonTranslatable.php b/src/JsonTranslatable/IsJsonTranslatable.php index 05aa78a..ab1a492 100644 --- a/src/JsonTranslatable/IsJsonTranslatable.php +++ b/src/JsonTranslatable/IsJsonTranslatable.php @@ -54,12 +54,12 @@ public function translate(string $field, ?string $locale = null, bool $fallback } // If the locale is not allowed then return null - if ( ! $this->isAllowedTranslationLocale($locale)) { + if (! $this->isAllowedTranslationLocale($locale)) { return $fallback ? $this->getAttributeValue($field) : null; } // If the field is not in the translatable fields list then return null - if ( ! $this->isTranslatable($field)) { + if (! $this->isTranslatable($field)) { return $fallback ? $this->getAttributeValue($field) : null; } @@ -68,7 +68,7 @@ public function translate(string $field, ?string $locale = null, bool $fallback if (isset($translations[$locale]) && array_key_exists($field, $translations[$locale])) { $value = $translations[$locale][$field]; - if ( ! $value && $fallback) { + if (! $value && $fallback) { // If the value is empty, return the default value if fallback is true return $this->getAttributeValue($field); } @@ -77,7 +77,7 @@ public function translate(string $field, ?string $locale = null, bool $fallback } // Check if translations for that locale exists - if ( ! isset($translations[$locale])) { + if (! isset($translations[$locale])) { return $fallback ? $this->getAttributeValue($field) : null; } @@ -107,7 +107,7 @@ public function clearTranslations(?string $locale = null): void $translations = $this->getAttributeValue('translations'); - if ( ! isset($translations[$locale])) { + if (! isset($translations[$locale])) { return; } @@ -144,11 +144,11 @@ public function hasTranslation(?string $locale = null): bool */ public function addTranslation(string $locale, string $field, $value): static { - if ( ! $this->isAllowedTranslationLocale($locale)) { + if (! $this->isAllowedTranslationLocale($locale)) { throw LanguageNotAllowedException::create($locale); } - if ( ! $this->isTranslatable($field)) { + if (! $this->isTranslatable($field)) { throw FieldNotAllowedException::create($field, $locale); } @@ -170,11 +170,11 @@ public function addTranslation(string $locale, string $field, $value): static ); $this->translations = $translations; - if ( ! $this->isDefaultTranslationLocale($locale)) { + if (! $this->isDefaultTranslationLocale($locale)) { $this->setTranslationAttributeValue($field, $locale, $value); // if it's a new model and the default value is not set, set the default - if (( ! $this->exists) && ( ! parent::getAttribute($field))) { + if ((! $this->exists) && (! parent::getAttribute($field))) { $this->setAttributeInternal($field, $value); } } @@ -193,14 +193,14 @@ public function deleteTranslation(string $locale): void throw CannotDeletePrimaryTranslationException::create($locale); } - if ( ! array_key_exists($locale, $this->translations)) { + if (! array_key_exists($locale, $this->translations)) { return; } $this->skipTranslation = true; $translations = $this->translations ?? []; - if ( ! $this->isDefaultTranslationLocale($locale)) { + if (! $this->isDefaultTranslationLocale($locale)) { unset($translations[$locale]); } else { throw CannotDeletePrimaryTranslationException::create($locale); @@ -286,7 +286,7 @@ public function attributesToArray() protected function addTranslatableAttributesToArray(array $attributes): array { foreach ($this->getTranslatables() as $key) { - if ( ! isset($attributes[$key])) { + if (! isset($attributes[$key])) { continue; } diff --git a/src/LanguageRegistrar.php b/src/LanguageRegistrar.php index 6d720f5..a99605c 100644 --- a/src/LanguageRegistrar.php +++ b/src/LanguageRegistrar.php @@ -2,8 +2,6 @@ namespace Javaabu\Translatable; -use function array_key_exists; - use DateInterval; use Illuminate\Cache\CacheManager; use Illuminate\Contracts\Cache\Repository; @@ -63,7 +61,7 @@ protected function getCacheStoreFromConfig(): Repository } // if an undefined cache store is specified, fallback to 'array' which is Laravel's closest equiv to 'none' - if ( ! array_key_exists($cache_driver, config('cache.stores'))) { + if (! \array_key_exists($cache_driver, config('cache.stores'))) { $cache_driver = 'array'; } diff --git a/src/Languages.php b/src/Languages.php index 630892e..a6e70f2 100644 --- a/src/Languages.php +++ b/src/Languages.php @@ -21,11 +21,11 @@ public function allExceptCurrent(): Collection public function except($code): Collection { - if ( ! is_array($code)) { + if (! is_array($code)) { $code = [$code]; } - if ( ! $code) { + if (! $code) { return $this->all(); } @@ -39,7 +39,7 @@ public function except($code): Collection */ public function get(?string $code = null): ?Language { - if ( ! $code) { + if (! $code) { $code = $this->currentLanguageCode(); } @@ -144,7 +144,7 @@ public function isDefault(string|Language|null $code = null): bool $code = $code->code; } - if ( ! $code) { + if (! $code) { $code = $this->currentLanguageCode(); } diff --git a/src/Middleware/LocaleMiddleware.php b/src/Middleware/LocaleMiddleware.php index df37088..5c62b56 100644 --- a/src/Middleware/LocaleMiddleware.php +++ b/src/Middleware/LocaleMiddleware.php @@ -38,7 +38,7 @@ protected function getUserLocale(Request $request): Language|string|null return $locale; } - if ( ! is_api_request($request)) { + if (! is_api_request($request)) { // then try the session $locale = $this->getLocaleFromSession($request); if ($locale) { @@ -58,7 +58,7 @@ protected function getLocaleFromRequest(Request $request): ?Language ?: $request->input('language') ?: $request->query('lang'); - if ( ! $locale || ! TranslatableFacade::isAllowedTranslationLocale($locale)) { + if (! $locale || ! TranslatableFacade::isAllowedTranslationLocale($locale)) { return null; } @@ -91,7 +91,7 @@ protected function setUserLocale($locale, Request $request, bool $set_app_locale $locale = $locale->code; } - if ( ! is_api_request($request)) { + if (! is_api_request($request)) { $request->session()->put('language', $locale); } diff --git a/src/Models/Language.php b/src/Models/Language.php index 92df3a8..f0ed6ce 100644 --- a/src/Models/Language.php +++ b/src/Models/Language.php @@ -62,13 +62,13 @@ class Language extends Model implements AdminModel protected function casts(): array { return [ - 'name' => 'string', - 'code' => 'string', - 'locale' => 'string', + 'name' => 'string', + 'code' => 'string', + 'locale' => 'string', 'local_name' => 'string', - 'flag' => 'string', - 'is_rtl' => 'boolean', - 'active' => 'boolean', + 'flag' => 'string', + 'is_rtl' => 'boolean', + 'active' => 'boolean', ]; } diff --git a/src/Traits/IsTranslatable.php b/src/Traits/IsTranslatable.php index 302e851..ac05318 100644 --- a/src/Traits/IsTranslatable.php +++ b/src/Traits/IsTranslatable.php @@ -117,7 +117,7 @@ public function isTranslatable(string $field): bool */ public function addTranslations(string $locale, array $fields): static { - if ( ! $this->isAllowedTranslationLocale($locale)) { + if (! $this->isAllowedTranslationLocale($locale)) { throw LanguageNotAllowedException::create($locale); } @@ -137,7 +137,7 @@ public function getFieldAndLocale(string $key): array { $locale = Str::afterLast($key, '_'); - if (empty($locale) || ( ! $this->isAllowedTranslationLocale($locale))) { + if (empty($locale) || (! $this->isAllowedTranslationLocale($locale))) { return [$key, null]; } diff --git a/src/Traits/UrlGenerators/BaseUrlGenerator.php b/src/Traits/UrlGenerators/BaseUrlGenerator.php index 6773837..e76b94d 100644 --- a/src/Traits/UrlGenerators/BaseUrlGenerator.php +++ b/src/Traits/UrlGenerators/BaseUrlGenerator.php @@ -7,9 +7,6 @@ trait BaseUrlGenerator /** * Standardized model route name. * Can customize this on each model as well. - * - * @param string|null $portal - * @return string */ public function getModelRouteName(?string $portal = null): string { @@ -19,11 +16,6 @@ public function getModelRouteName(?string $portal = null): string /** * Override in model for nested params. * Useful for nested resources. - * - * @param string $action - * @param string|null $locale - * @param string|null $portal - * @return array */ public function getAdditionalRouteParams(string $action, ?string $locale, ?string $portal): array { @@ -35,9 +27,6 @@ public function getAdditionalRouteParams(string $action, ?string $locale, ?strin * $portal parameter to be used to customize the key based on portal if needed. * Default is just the ID. * Can be overridden in the model if needed. - * - * @param string|null $portal - * @return string */ public function routeKeyForPortal(?string $portal): string { diff --git a/src/Traits/UrlGenerators/DbTranslatedUrlGenerator.php b/src/Traits/UrlGenerators/DbTranslatedUrlGenerator.php index 4a77b16..2a53802 100644 --- a/src/Traits/UrlGenerators/DbTranslatedUrlGenerator.php +++ b/src/Traits/UrlGenerators/DbTranslatedUrlGenerator.php @@ -12,7 +12,7 @@ trait DbTranslatedUrlGenerator public function url(string $action = 'show', Language|string|null $locale = null, ?string $portal = null): string { - if (!$portal) { + if (! $portal) { $portal = Request::portal(); } diff --git a/src/Traits/UrlGenerators/JsonTranslatedUrlGenerator.php b/src/Traits/UrlGenerators/JsonTranslatedUrlGenerator.php index 8cb974a..358026b 100644 --- a/src/Traits/UrlGenerators/JsonTranslatedUrlGenerator.php +++ b/src/Traits/UrlGenerators/JsonTranslatedUrlGenerator.php @@ -11,7 +11,7 @@ trait JsonTranslatedUrlGenerator public function url(string $action = 'show', Language|string|null $locale = null, ?string $portal = null): string { - if ( ! $portal) { + if (! $portal) { $portal = Request::portal(); } @@ -21,7 +21,7 @@ public function url(string $action = 'show', Language|string|null $locale = null $params = $this->getAdditionalRouteParams($action, $locale, $portal); - if ( ! in_array($action, ['index', 'store', 'create', 'trash'])) { + if (! in_array($action, ['index', 'store', 'create', 'trash'])) { $params[] = $this->routeKeyForPortal($portal); } diff --git a/src/TranslatableServiceProvider.php b/src/TranslatableServiceProvider.php index 94112d2..a4e78dd 100644 --- a/src/TranslatableServiceProvider.php +++ b/src/TranslatableServiceProvider.php @@ -103,12 +103,12 @@ public function register(): void public function registerSingletons(): void { $this->app->singleton(Translatable::class, function () { - return new Translatable(); + return new Translatable; }); $this->app->alias(Translatable::class, 'translatable'); $this->app->singleton(Languages::class, function () { - return new Languages(); + return new Languages; }); $this->app->alias(Languages::class, 'languages'); } diff --git a/src/helpers.php b/src/helpers.php index a399556..40030ec 100644 --- a/src/helpers.php +++ b/src/helpers.php @@ -2,12 +2,13 @@ use Illuminate\Database\Eloquent\Model; use Illuminate\Support\Arr; +use Illuminate\Support\Facades\URL; use Javaabu\Translatable\Contracts\DbTranslatable; use Javaabu\Translatable\Facades\Languages; use Javaabu\Translatable\ModelAttribute; use Javaabu\Translatable\Models\Language; -if ( ! function_exists('translation_locale')) { +if (! function_exists('translation_locale')) { /** * One function helper to get the current locale. */ @@ -17,7 +18,7 @@ function translation_locale(): ?string } } -if ( ! function_exists('appGetLocale')) { +if (! function_exists('appGetLocale')) { /** * One function helper to get the current locale. */ @@ -27,7 +28,7 @@ function appGetLocale(): array|string|null } } -if ( ! function_exists('_d')) { +if (! function_exists('_d')) { /** * Translate the given message and use the default locale if locale not specified. */ @@ -39,7 +40,7 @@ function _d(?string $key = null, array $replace = [], ?string $locale = null): a } } -if ( ! function_exists('translate_url')) { +if (! function_exists('translate_url')) { /** * Generate a translatable url for the application. * @@ -47,7 +48,7 @@ function _d(?string $key = null, array $replace = [], ?string $locale = null): a */ function translate_url(?string $path = null, array $parameters = [], ?bool $secure = null, $locale = null): string { - if ( ! $locale) { + if (! $locale) { $locale = translation_locale(); } @@ -57,7 +58,7 @@ function translate_url(?string $path = null, array $parameters = [], ?bool $secu } } -if ( ! function_exists('translate_route')) { +if (! function_exists('translate_route')) { /** * Generate a translatable url using the route method. * @@ -69,7 +70,7 @@ function translate_route(string $name, mixed $parameters = [], bool $absolute = $locale = $locale->code; } - if ( ! $locale) { + if (! $locale) { $locale = translation_locale(); } @@ -80,7 +81,7 @@ function translate_route(string $name, mixed $parameters = [], bool $absolute = } } -if ( ! function_exists('translate_action')) { +if (! function_exists('translate_action')) { /** * Generate a translatable url using the action method. * @@ -92,18 +93,18 @@ function translate_action(string|array $action, mixed $parameters = [], bool $ab $locale = $locale->code; } - if ( ! $locale) { + if (! $locale) { $locale = translation_locale(); } $parameters = Arr::wrap($parameters); $parameters['language'] = $locale; - return Illuminate\Support\Facades\URL::action($action, $parameters, $absolute); + return URL::action($action, $parameters, $absolute); } } -if ( ! function_exists('locale_direction')) { +if (! function_exists('locale_direction')) { /** * Generate a translatable route for the model. */ @@ -117,7 +118,7 @@ function locale_direction(Language|string|null $language = null): string } } -if ( ! function_exists('ma')) { +if (! function_exists('ma')) { /** * Get a model attribute instance */ @@ -127,7 +128,7 @@ function ma(Model $model, string $attribute): ModelAttribute } } -if ( ! function_exists('translate_old')) { +if (! function_exists('translate_old')) { /** * Retrieve an old input item or parent item value. */ diff --git a/tests/Feature/IsJsonTranslatableCRUDTest.php b/tests/Feature/IsJsonTranslatableCRUDTest.php index 9cf0065..5a4d093 100644 --- a/tests/Feature/IsJsonTranslatableCRUDTest.php +++ b/tests/Feature/IsJsonTranslatableCRUDTest.php @@ -20,26 +20,26 @@ protected function setUp(): void $this->artisan('migrate:fresh'); Language::create([ - 'name' => 'English', - 'code' => 'en', + 'name' => 'English', + 'code' => 'en', 'locale' => 'en', - 'flag' => '🇬🇧', + 'flag' => '🇬🇧', 'is_rtl' => false, 'active' => true, ]); Language::create([ - 'name' => 'Dhivehi', - 'code' => 'dv', + 'name' => 'Dhivehi', + 'code' => 'dv', 'locale' => 'dv', - 'flag' => '🇲🇻', + 'flag' => '🇲🇻', 'is_rtl' => true, 'active' => true, ]); Language::create([ - 'name' => 'Japanese', - 'code' => 'jp', + 'name' => 'Japanese', + 'code' => 'jp', 'locale' => 'jp', - 'flag' => '🇯🇵', + 'flag' => '🇯🇵', 'is_rtl' => false, 'active' => true, ]); @@ -51,9 +51,9 @@ protected function setUp(): void Route::post('/{language}/articles', function () { $validated = request()->validate([ - 'title' => 'required', - 'body' => 'required', - 'slug' => 'required', + 'title' => 'required', + 'body' => 'required', + 'slug' => 'required', 'author_id' => 'required|exists:authors,id', ]); @@ -63,9 +63,9 @@ protected function setUp(): void Route::patch('/{language}/articles/{id}', function (Language $language, $articleId) { $article = Article::find($articleId); $validated = request()->validate([ - 'title' => '', - 'body' => '', - 'slug' => '', + 'title' => '', + 'body' => '', + 'slug' => '', 'author_id' => 'exists:authors,id', ]); @@ -81,16 +81,16 @@ public function it_can_add_a_translation() $article = Article::factory()->make(); $response = $this->post('/en/articles', [ - 'title' => $article->title, - 'body' => $article->body, - 'slug' => $article->slug, + 'title' => $article->title, + 'body' => $article->body, + 'slug' => $article->slug, 'author_id' => $author->id, ]); $article->id = $response->json('id'); $this->assertDatabaseHas('articles', [ 'title' => $article->title, - 'body' => $article->body, + 'body' => $article->body, ]); $this->patch('/dv/articles/' . $article->id, [ @@ -98,12 +98,12 @@ public function it_can_add_a_translation() ]); $this->assertDatabaseHas('articles', [ - 'title' => $article->title, - 'body' => $article->body, + 'title' => $article->title, + 'body' => $article->body, 'translations' => Json::encode([ 'dv' => [ 'title' => 'Mee dhivehi title eh', - 'lang' => 'dv', + 'lang' => 'dv', ], ]), ]); @@ -114,12 +114,12 @@ public function it_can_add_a_translation() ]); $this->assertDatabaseHas('articles', [ - 'title' => $article->title, - 'body' => $article->body, + 'title' => $article->title, + 'body' => $article->body, 'translations' => Json::encode([ 'dv' => [ 'title' => 'Mee dhivehi title eh', - 'lang' => 'dv', + 'lang' => 'dv', ], ]), ]); @@ -140,12 +140,12 @@ public function it_can_edit_a_translation() Languages::setCurrentLocale('en'); $this->assertDatabaseHas('articles', [ - 'title' => $article->title, - 'body' => $article->body, + 'title' => $article->title, + 'body' => $article->body, 'translations' => Json::encode([ 'dv' => [ 'title' => 'Mee ehen dhivehi title eh', - 'lang' => 'dv', + 'lang' => 'dv', ], ]), ]); diff --git a/tests/TestSupport/Factories/ArticleFactory.php b/tests/TestSupport/Factories/ArticleFactory.php index e38d3f2..a4d7832 100644 --- a/tests/TestSupport/Factories/ArticleFactory.php +++ b/tests/TestSupport/Factories/ArticleFactory.php @@ -14,10 +14,10 @@ class ArticleFactory extends Factory public function definition(): array { return [ - 'title' => $this->faker->title(), - 'slug' => $this->faker->slug(), - 'body' => $this->faker->paragraph(5), - 'lang' => $this->faker->locale(), + 'title' => $this->faker->title(), + 'slug' => $this->faker->slug(), + 'body' => $this->faker->paragraph(5), + 'lang' => $this->faker->locale(), 'created_at' => Carbon::now(), 'updated_at' => Carbon::now(), ]; diff --git a/tests/TestSupport/Factories/AuthorFactory.php b/tests/TestSupport/Factories/AuthorFactory.php index 730f5c6..a6b9c69 100644 --- a/tests/TestSupport/Factories/AuthorFactory.php +++ b/tests/TestSupport/Factories/AuthorFactory.php @@ -13,7 +13,7 @@ class AuthorFactory extends Factory public function definition(): array { return [ - 'name' => $this->faker->name(), + 'name' => $this->faker->name(), 'created_at' => Carbon::now(), 'updated_at' => Carbon::now(), ]; diff --git a/tests/TestSupport/Factories/PostFactory.php b/tests/TestSupport/Factories/PostFactory.php index ff7a5c2..69c0f59 100644 --- a/tests/TestSupport/Factories/PostFactory.php +++ b/tests/TestSupport/Factories/PostFactory.php @@ -14,10 +14,10 @@ class PostFactory extends Factory public function definition(): array { return [ - 'title' => $this->faker->sentence(), - 'slug' => $this->faker->slug(), - 'body' => $this->faker->paragraph(5), - 'lang' => $this->faker->locale(), + 'title' => $this->faker->sentence(), + 'slug' => $this->faker->slug(), + 'body' => $this->faker->paragraph(5), + 'lang' => $this->faker->locale(), 'created_at' => Carbon::now(), 'updated_at' => Carbon::now(), ]; diff --git a/tests/TestSupport/Models/Article.php b/tests/TestSupport/Models/Article.php index c4a235d..57e6d72 100644 --- a/tests/TestSupport/Models/Article.php +++ b/tests/TestSupport/Models/Article.php @@ -30,7 +30,7 @@ class Article extends Model implements JsonTranslatable protected static function newFactory(): ArticleFactory { - return new ArticleFactory(); + return new ArticleFactory; } public function getTranslatables(): array diff --git a/tests/TestSupport/Models/Post.php b/tests/TestSupport/Models/Post.php index b66b230..651110e 100644 --- a/tests/TestSupport/Models/Post.php +++ b/tests/TestSupport/Models/Post.php @@ -26,7 +26,7 @@ class Post extends Model implements Translatable protected static function newFactory(): PostFactory { - return new PostFactory(); + return new PostFactory; } public function author(): BelongsTo diff --git a/tests/Unit/DbTranslatable/IsDbTranslatableTest.php b/tests/Unit/DbTranslatable/IsDbTranslatableTest.php index 272aada..ed07bb9 100644 --- a/tests/Unit/DbTranslatable/IsDbTranslatableTest.php +++ b/tests/Unit/DbTranslatable/IsDbTranslatableTest.php @@ -21,26 +21,26 @@ protected function setUp(): void parent::setUp(); Language::create([ - 'name' => 'English', - 'code' => 'en', + 'name' => 'English', + 'code' => 'en', 'locale' => 'en', - 'flag' => '🇬🇧', + 'flag' => '🇬🇧', 'is_rtl' => false, 'active' => true, ]); Language::create([ - 'name' => 'Dhivehi', - 'code' => 'dv', + 'name' => 'Dhivehi', + 'code' => 'dv', 'locale' => 'dv', - 'flag' => '🇲🇻', + 'flag' => '🇲🇻', 'is_rtl' => true, 'active' => true, ]); Language::create([ - 'name' => 'Japanese', - 'code' => 'jp', + 'name' => 'Japanese', + 'code' => 'jp', 'locale' => 'jp', - 'flag' => '🇯🇵', + 'flag' => '🇯🇵', 'is_rtl' => false, 'active' => true, ]); @@ -50,7 +50,7 @@ protected function setUp(): void #[Test] public function it_can_get_fields_ignored_for_translation() { - $post = new Post(); + $post = new Post; $this->assertEquals([ 'id', @@ -65,7 +65,7 @@ public function it_can_get_fields_ignored_for_translation() #[Test] public function it_can_get_translatable_fields() { - $post = new Post(); + $post = new Post; $this->assertEquals([ 'title', @@ -76,7 +76,7 @@ public function it_can_get_translatable_fields() #[Test] public function it_can_get_non_translatable_fields() { - $post = new Post(); + $post = new Post; $this->assertEquals([ 'slug', @@ -86,7 +86,7 @@ public function it_can_get_non_translatable_fields() #[Test] public function it_can_get_non_translatable_pivots() { - $post = new Post(); + $post = new Post; $this->assertEquals([ 'author_id', @@ -96,7 +96,7 @@ public function it_can_get_non_translatable_pivots() #[Test] public function it_can_get_all_non_translatable_fields() { - $post = new Post(); + $post = new Post; $this->assertEquals([ 'slug', @@ -113,7 +113,7 @@ public function it_can_get_all_non_translatable_fields() #[Test] public function it_can_check_if_is_a_non_translatable_pivot() { - $post = new Post(); + $post = new Post; $this->assertFalse($post->isNonTranslatablePivot('id')); $this->assertTrue($post->isNonTranslatablePivot('author_id')); @@ -152,23 +152,23 @@ public function it_can_check_if_given_locale_is_allowed() public function it_can_translate_field_via_translate_function() { $post = Post::factory()->withAuthor()->create([ - 'lang' => 'en', + 'lang' => 'en', 'title' => 'This is an English title', - 'slug' => 'this-is-an-english-slug', - 'body' => 'This is an English body', + 'slug' => 'this-is-an-english-slug', + 'body' => 'This is an English body', ]); $post_dv = Post::factory()->withAuthor()->create([ - 'lang' => 'dv', - 'title' => 'Mee dhivehi title eh', - 'slug' => 'mee-dhivehi-slug-eh', - 'body' => 'Mee dhivehi liyumeh', + 'lang' => 'dv', + 'title' => 'Mee dhivehi title eh', + 'slug' => 'mee-dhivehi-slug-eh', + 'body' => 'Mee dhivehi liyumeh', 'translatable_parent_id' => $post->id, ]); $post_jp = Post::factory()->withAuthor()->create([ - 'lang' => 'jp', - 'title' => 'Kore wa taitorudesu', - 'slug' => 'kore-wa-namekujidesu', - 'body' => 'Kore wa kijidesu', + 'lang' => 'jp', + 'title' => 'Kore wa taitorudesu', + 'slug' => 'kore-wa-namekujidesu', + 'body' => 'Kore wa kijidesu', 'translatable_parent_id' => $post->id, ]); @@ -208,16 +208,16 @@ public function it_can_translate_field_via_translate_function() public function it_can_translate_field_via_translate_function_without_fallback() { $post = Post::factory()->withAuthor()->create([ - 'lang' => 'en', + 'lang' => 'en', 'title' => 'This is an English title', - 'slug' => 'this-is-an-english-slug', - 'body' => 'This is an English body', + 'slug' => 'this-is-an-english-slug', + 'body' => 'This is an English body', ]); $post_dv = Post::factory()->withAuthor()->create([ - 'lang' => 'dv', - 'title' => 'Mee dhivehi title eh', - 'slug' => 'mee-dhivehi-slug-eh', - 'body' => 'Mee dhivehi liyumeh', + 'lang' => 'dv', + 'title' => 'Mee dhivehi title eh', + 'slug' => 'mee-dhivehi-slug-eh', + 'body' => 'Mee dhivehi liyumeh', 'translatable_parent_id' => $post->id, ]); @@ -235,17 +235,17 @@ public function it_can_translate_field_via_magic_method() { $post = Post::factory()->withAuthor()->create(); $post_dv = Post::factory()->withAuthor()->create([ - 'lang' => 'dv', - 'title' => 'Mee dhivehi title eh', - 'slug' => 'mee-dhivehi-slug-eh', - 'body' => 'Mee dhivehi liyumeh', + 'lang' => 'dv', + 'title' => 'Mee dhivehi title eh', + 'slug' => 'mee-dhivehi-slug-eh', + 'body' => 'Mee dhivehi liyumeh', 'translatable_parent_id' => $post->id, ]); $post_jp = Post::factory()->withAuthor()->create([ - 'lang' => 'jp', - 'title' => 'Kore wa taitorudesu', - 'slug' => 'kore-wa-namekujidesu', - 'body' => 'Kore wa kijidesu', + 'lang' => 'jp', + 'title' => 'Kore wa taitorudesu', + 'slug' => 'kore-wa-namekujidesu', + 'body' => 'Kore wa kijidesu', 'translatable_parent_id' => $post->id, ]); @@ -261,17 +261,17 @@ public function it_can_translate_field_via_locale_change() { $post = Post::factory()->withAuthor()->create(); $post_dv = Post::factory()->withAuthor()->create([ - 'lang' => 'dv', - 'title' => 'Mee dhivehi title eh', - 'slug' => 'mee-dhivehi-slug-eh', - 'body' => 'Mee dhivehi liyumeh', + 'lang' => 'dv', + 'title' => 'Mee dhivehi title eh', + 'slug' => 'mee-dhivehi-slug-eh', + 'body' => 'Mee dhivehi liyumeh', 'translatable_parent_id' => $post->id, ]); $post_jp = Post::factory()->withAuthor()->create([ - 'lang' => 'jp', - 'title' => 'Kore wa taitorudesu', - 'slug' => 'kore-wa-namekujidesu', - 'body' => 'Kore wa kijidesu', + 'lang' => 'jp', + 'title' => 'Kore wa taitorudesu', + 'slug' => 'kore-wa-namekujidesu', + 'body' => 'Kore wa kijidesu', 'translatable_parent_id' => $post->id, ]); @@ -289,17 +289,17 @@ public function it_can_translate_fields_via_compoships() 'lang' => 'en', ]); $post_dv = Post::factory()->withAuthor()->create([ - 'lang' => 'dv', - 'title' => 'Mee dhivehi title eh', - 'slug' => 'mee-dhivehi-slug-eh', - 'body' => 'Mee dhivehi liyumeh', + 'lang' => 'dv', + 'title' => 'Mee dhivehi title eh', + 'slug' => 'mee-dhivehi-slug-eh', + 'body' => 'Mee dhivehi liyumeh', 'translatable_parent_id' => $post->id, ]); $post_jp = Post::factory()->withAuthor()->create([ - 'lang' => 'jp', - 'title' => 'Kore wa taitorudesu', - 'slug' => 'kore-wa-namekujidesu', - 'body' => 'Kore wa kijidesu', + 'lang' => 'jp', + 'title' => 'Kore wa taitorudesu', + 'slug' => 'kore-wa-namekujidesu', + 'body' => 'Kore wa kijidesu', 'translatable_parent_id' => $post->id, ]); @@ -316,17 +316,17 @@ public function it_can_check_if_given_field_is_translatable() { $post = Post::factory()->withAuthor()->create(); $post_dv = Post::factory()->withAuthor()->create([ - 'lang' => 'dv', - 'title' => 'Mee dhivehi title eh', - 'slug' => 'mee-dhivehi-slug-eh', - 'body' => 'Mee dhivehi liyumeh', + 'lang' => 'dv', + 'title' => 'Mee dhivehi title eh', + 'slug' => 'mee-dhivehi-slug-eh', + 'body' => 'Mee dhivehi liyumeh', 'translatable_parent_id' => $post->id, ]); $post_jp = Post::factory()->withAuthor()->create([ - 'lang' => 'jp', - 'title' => 'Kore wa taitorudesu', - 'slug' => 'kore-wa-namekujidesu', - 'body' => 'Kore wa kijidesu', + 'lang' => 'jp', + 'title' => 'Kore wa taitorudesu', + 'slug' => 'kore-wa-namekujidesu', + 'body' => 'Kore wa kijidesu', 'translatable_parent_id' => $post->id, ]); @@ -340,17 +340,17 @@ public function it_can_clear_translations_for_one_locale() 'lang' => 'en', ]); $post_dv = Post::factory()->withAuthor()->create([ - 'lang' => 'dv', - 'title' => 'Mee dhivehi title eh', - 'slug' => 'mee-dhivehi-slug-eh', - 'body' => 'Mee dhivehi liyumeh', + 'lang' => 'dv', + 'title' => 'Mee dhivehi title eh', + 'slug' => 'mee-dhivehi-slug-eh', + 'body' => 'Mee dhivehi liyumeh', 'translatable_parent_id' => $post->id, ]); $post_jp = Post::factory()->withAuthor()->create([ - 'lang' => 'jp', - 'title' => 'Kore wa taitorudesu', - 'slug' => 'kore-wa-namekujidesu', - 'body' => 'Kore wa kijidesu', + 'lang' => 'jp', + 'title' => 'Kore wa taitorudesu', + 'slug' => 'kore-wa-namekujidesu', + 'body' => 'Kore wa kijidesu', 'translatable_parent_id' => $post->id, ]); @@ -368,17 +368,17 @@ public function it_can_clear_translations_for_default_locale() 'lang' => 'en', ]); $post_dv = Post::factory()->withAuthor()->create([ - 'lang' => 'dv', - 'title' => 'Mee dhivehi title eh', - 'slug' => 'mee-dhivehi-slug-eh', - 'body' => 'Mee dhivehi liyumeh', + 'lang' => 'dv', + 'title' => 'Mee dhivehi title eh', + 'slug' => 'mee-dhivehi-slug-eh', + 'body' => 'Mee dhivehi liyumeh', 'translatable_parent_id' => $post->id, ]); $post_jp = Post::factory()->withAuthor()->create([ - 'lang' => 'jp', - 'title' => 'Kore wa taitorudesu', - 'slug' => 'kore-wa-namekujidesu', - 'body' => 'Kore wa kijidesu', + 'lang' => 'jp', + 'title' => 'Kore wa taitorudesu', + 'slug' => 'kore-wa-namekujidesu', + 'body' => 'Kore wa kijidesu', 'translatable_parent_id' => $post->id, ]); @@ -399,17 +399,17 @@ public function it_can_clear_translations_for_locale_via_translatable_parent() 'lang' => 'en', ]); $post_dv = Post::factory()->withAuthor()->create([ - 'lang' => 'dv', - 'title' => 'Mee dhivehi title eh', - 'slug' => 'mee-dhivehi-slug-eh', - 'body' => 'Mee dhivehi liyumeh', + 'lang' => 'dv', + 'title' => 'Mee dhivehi title eh', + 'slug' => 'mee-dhivehi-slug-eh', + 'body' => 'Mee dhivehi liyumeh', 'translatable_parent_id' => $post->id, ]); $post_jp = Post::factory()->withAuthor()->create([ - 'lang' => 'jp', - 'title' => 'Kore wa taitorudesu', - 'slug' => 'kore-wa-namekujidesu', - 'body' => 'Kore wa kijidesu', + 'lang' => 'jp', + 'title' => 'Kore wa taitorudesu', + 'slug' => 'kore-wa-namekujidesu', + 'body' => 'Kore wa kijidesu', 'translatable_parent_id' => $post->id, ]); @@ -428,17 +428,17 @@ public function it_can_clear_translations_for_all_locales() { $post = Post::factory()->withAuthor()->create(); $post_dv = Post::factory()->withAuthor()->create([ - 'lang' => 'dv', - 'title' => 'Mee dhivehi title eh', - 'slug' => 'mee-dhivehi-slug-eh', - 'body' => 'Mee dhivehi liyumeh', + 'lang' => 'dv', + 'title' => 'Mee dhivehi title eh', + 'slug' => 'mee-dhivehi-slug-eh', + 'body' => 'Mee dhivehi liyumeh', 'translatable_parent_id' => $post->id, ]); $post_jp = Post::factory()->withAuthor()->create([ - 'lang' => 'jp', - 'title' => 'Kore wa taitorudesu', - 'slug' => 'kore-wa-namekujidesu', - 'body' => 'Kore wa kijidesu', + 'lang' => 'jp', + 'title' => 'Kore wa taitorudesu', + 'slug' => 'kore-wa-namekujidesu', + 'body' => 'Kore wa kijidesu', 'translatable_parent_id' => $post->id, ]); @@ -455,17 +455,17 @@ public function it_can_check_if_any_translation_for_a_specific_locale() 'lang' => 'en', ]); $post_dv = Post::factory()->withAuthor()->create([ - 'lang' => 'dv', - 'title' => 'Mee dhivehi title eh', - 'slug' => 'mee-dhivehi-slug-eh', - 'body' => 'Mee dhivehi liyumeh', + 'lang' => 'dv', + 'title' => 'Mee dhivehi title eh', + 'slug' => 'mee-dhivehi-slug-eh', + 'body' => 'Mee dhivehi liyumeh', 'translatable_parent_id' => $post->id, ]); $post_jp = Post::factory()->withAuthor()->create([ - 'lang' => 'jp', - 'title' => 'Kore wa taitorudesu', - 'slug' => 'kore-wa-namekujidesu', - 'body' => 'Kore wa kijidesu', + 'lang' => 'jp', + 'title' => 'Kore wa taitorudesu', + 'slug' => 'kore-wa-namekujidesu', + 'body' => 'Kore wa kijidesu', 'translatable_parent_id' => $post->id, ]); @@ -485,17 +485,17 @@ public function it_can_check_if_is_default_translation_locale() 'lang' => 'en', ]); $post_dv = Post::factory()->withAuthor()->create([ - 'lang' => 'dv', - 'title' => 'Mee dhivehi title eh', - 'slug' => 'mee-dhivehi-slug-eh', - 'body' => 'Mee dhivehi liyumeh', + 'lang' => 'dv', + 'title' => 'Mee dhivehi title eh', + 'slug' => 'mee-dhivehi-slug-eh', + 'body' => 'Mee dhivehi liyumeh', 'translatable_parent_id' => $post->id, ]); $post_jp = Post::factory()->withAuthor()->create([ - 'lang' => 'jp', - 'title' => 'Kore wa taitorudesu', - 'slug' => 'kore-wa-namekujidesu', - 'body' => 'Kore wa kijidesu', + 'lang' => 'jp', + 'title' => 'Kore wa taitorudesu', + 'slug' => 'kore-wa-namekujidesu', + 'body' => 'Kore wa kijidesu', 'translatable_parent_id' => $post->id, ]); @@ -547,7 +547,7 @@ public function it_can_add_new_translation_locales_via_setter() public function it_can_add_new_translation_locales_via_app_locale() { $post = Post::factory()->withAuthor()->create([ - 'lang' => 'en', + 'lang' => 'en', 'title' => 'This is an English title', ]); @@ -573,7 +573,7 @@ public function it_can_add_translations_in_bulk() $post->addTranslations('dv', [ 'title' => 'Mee dhivehi title eh', - 'body' => 'Mee dhivehi liyumeh', + 'body' => 'Mee dhivehi liyumeh', ]); $this->assertEquals('Mee dhivehi title eh', $post->title_dv); @@ -596,8 +596,8 @@ public function it_cannot_add_translations_in_bulk_for_locales_that_are_not_allo $post->addTranslations('zh-CN', [ 'title' => '这是一个中文标题', - 'slug' => '这是一只中国蛞蝓', - 'body' => '这是一个中国人的身体', + 'slug' => '这是一只中国蛞蝓', + 'body' => '这是一个中国人的身体', ]); } @@ -696,7 +696,7 @@ public function it_cannot_delete_primary_translation() { $this->expectException(CannotDeletePrimaryTranslationException::class); $post = Post::factory()->withAuthor()->create([ - 'lang' => 'en', + 'lang' => 'en', 'title' => 'This is an English title', ]); diff --git a/tests/Unit/Facades/LanguageFacadeTest.php b/tests/Unit/Facades/LanguageFacadeTest.php index 9275a7c..612e42e 100644 --- a/tests/Unit/Facades/LanguageFacadeTest.php +++ b/tests/Unit/Facades/LanguageFacadeTest.php @@ -17,26 +17,26 @@ protected function setUp(): void parent::setUp(); Language::create([ - 'name' => 'English', - 'code' => 'en', + 'name' => 'English', + 'code' => 'en', 'locale' => 'en', - 'flag' => '🇬🇧', + 'flag' => '🇬🇧', 'is_rtl' => false, 'active' => true, ]); Language::create([ - 'name' => 'Dhivehi', - 'code' => 'dv', + 'name' => 'Dhivehi', + 'code' => 'dv', 'locale' => 'dv', - 'flag' => '🇲🇻', + 'flag' => '🇲🇻', 'is_rtl' => true, 'active' => true, ]); Language::create([ - 'name' => 'Japanese', - 'code' => 'jp', + 'name' => 'Japanese', + 'code' => 'jp', 'locale' => 'jp', - 'flag' => '🇯🇵', + 'flag' => '🇯🇵', 'is_rtl' => false, 'active' => true, ]); diff --git a/tests/Unit/JsonTranslatable/IsJsonTranslatableTest.php b/tests/Unit/JsonTranslatable/IsJsonTranslatableTest.php index 0542a56..0f54efd 100644 --- a/tests/Unit/JsonTranslatable/IsJsonTranslatableTest.php +++ b/tests/Unit/JsonTranslatable/IsJsonTranslatableTest.php @@ -20,26 +20,26 @@ protected function setUp(): void { parent::setUp(); Language::create([ - 'name' => 'English', - 'code' => 'en', + 'name' => 'English', + 'code' => 'en', 'locale' => 'en', - 'flag' => '🇬🇧', + 'flag' => '🇬🇧', 'is_rtl' => false, 'active' => true, ]); Language::create([ - 'name' => 'Dhivehi', - 'code' => 'dv', + 'name' => 'Dhivehi', + 'code' => 'dv', 'locale' => 'dv', - 'flag' => '🇲🇻', + 'flag' => '🇲🇻', 'is_rtl' => true, 'active' => true, ]); Language::create([ - 'name' => 'Japanese', - 'code' => 'jp', + 'name' => 'Japanese', + 'code' => 'jp', 'locale' => 'jp', - 'flag' => '🇯🇵', + 'flag' => '🇯🇵', 'is_rtl' => false, 'active' => true, ]); @@ -48,7 +48,7 @@ protected function setUp(): void #[Test] public function it_can_get_fields_ignored_for_translation() { - $article = new Article(); + $article = new Article; $this->assertEquals([ 'id', @@ -63,7 +63,7 @@ public function it_can_get_fields_ignored_for_translation() #[Test] public function it_can_get_translatable_fields() { - $article = new Article(); + $article = new Article; $this->assertEquals([ 'title', @@ -74,7 +74,7 @@ public function it_can_get_translatable_fields() #[Test] public function it_can_get_non_translatable_fields() { - $article = new Article(); + $article = new Article; $this->assertEquals([ 'slug', @@ -84,7 +84,7 @@ public function it_can_get_non_translatable_fields() #[Test] public function it_can_get_non_translatable_pivots() { - $article = new Article(); + $article = new Article; $this->assertEquals([ 'author_id', @@ -94,7 +94,7 @@ public function it_can_get_non_translatable_pivots() #[Test] public function it_can_get_all_non_translatable_fields() { - $article = new Article(); + $article = new Article; $this->assertEquals([ 'slug', @@ -111,7 +111,7 @@ public function it_can_get_all_non_translatable_fields() #[Test] public function it_can_check_if_is_a_non_translatable_pivot() { - $article = new Article(); + $article = new Article; $this->assertFalse($article->isNonTranslatablePivot('id')); $this->assertTrue($article->isNonTranslatablePivot('author_id')); @@ -150,20 +150,20 @@ public function it_can_check_if_given_locale_is_allowed() public function it_can_translate_field_via_translate_function() { $article = Article::factory()->withAuthor()->create([ - 'lang' => 'en', - 'title' => 'This is an English title', - 'slug' => 'this-is-an-english-slug', - 'body' => 'This is an English body', + 'lang' => 'en', + 'title' => 'This is an English title', + 'slug' => 'this-is-an-english-slug', + 'body' => 'This is an English body', 'translations' => [ 'dv' => [ 'title' => 'Mee dhivehi title eh', - 'slug' => 'mee-dhivehi-slug-eh', - 'body' => 'Mee dhivehi liyumeh', + 'slug' => 'mee-dhivehi-slug-eh', + 'body' => 'Mee dhivehi liyumeh', ], 'jp' => [ 'title' => 'Kore wa taitorudesu', - 'slug' => 'kore-wa-namekujidesu', - 'body' => 'Kore wa kijidesu', + 'slug' => 'kore-wa-namekujidesu', + 'body' => 'Kore wa kijidesu', ], ], ]); @@ -195,15 +195,15 @@ public function it_can_translate_field_via_translate_function() public function it_can_translate_field_via_translate_function_without_fallback() { $article = Article::factory()->withAuthor()->create([ - 'lang' => 'en', - 'title' => 'This is an English title', - 'slug' => 'this-is-an-english-slug', - 'body' => 'This is an English body', + 'lang' => 'en', + 'title' => 'This is an English title', + 'slug' => 'this-is-an-english-slug', + 'body' => 'This is an English body', 'translations' => [ 'dv' => [ 'title' => 'Mee dhivehi title eh', - 'slug' => 'mee-dhivehi-slug-eh', - 'body' => 'Mee dhivehi liyumeh', + 'slug' => 'mee-dhivehi-slug-eh', + 'body' => 'Mee dhivehi liyumeh', ], ], ]); @@ -224,13 +224,13 @@ public function it_can_translate_field_via_magic_method() 'translations' => [ 'dv' => [ 'title' => 'Mee dhivehi title eh', - 'slug' => 'mee-dhivehi-slug-eh', - 'body' => 'Mee dhivehi liyumeh', + 'slug' => 'mee-dhivehi-slug-eh', + 'body' => 'Mee dhivehi liyumeh', ], 'jp' => [ 'title' => 'Kore wa taitorudesu', - 'slug' => 'kore-wa-namekujidesu', - 'body' => 'Kore wa kijidesu', + 'slug' => 'kore-wa-namekujidesu', + 'body' => 'Kore wa kijidesu', ], ], ]); @@ -249,13 +249,13 @@ public function it_can_translate_field_via_locale_change() 'translations' => [ 'dv' => [ 'title' => 'Mee dhivehi title eh', - 'slug' => 'mee-dhivehi-slug-eh', - 'body' => 'Mee dhivehi liyumeh', + 'slug' => 'mee-dhivehi-slug-eh', + 'body' => 'Mee dhivehi liyumeh', ], 'jp' => [ 'title' => 'Kore wa taitorudesu', - 'slug' => 'kore-wa-namekujidesu', - 'body' => 'Kore wa kijidesu', + 'slug' => 'kore-wa-namekujidesu', + 'body' => 'Kore wa kijidesu', ], ], ]); @@ -274,13 +274,13 @@ public function it_can_check_if_given_field_is_translatable() 'translations' => [ 'dv' => [ 'title' => 'Mee dhivehi title eh', - 'slug' => 'mee-dhivehi-slug-eh', - 'body' => 'Mee dhivehi liyumeh', + 'slug' => 'mee-dhivehi-slug-eh', + 'body' => 'Mee dhivehi liyumeh', ], 'jp' => [ 'title' => 'Kore wa taitorudesu', - 'slug' => 'kore-wa-namekujidesu', - 'body' => 'Kore wa kijidesu', + 'slug' => 'kore-wa-namekujidesu', + 'body' => 'Kore wa kijidesu', ], ], ]); @@ -295,13 +295,13 @@ public function it_can_clear_translations_for_one_locale() 'translations' => [ 'dv' => [ 'title' => 'Mee dhivehi title eh', - 'slug' => 'mee-dhivehi-slug-eh', - 'body' => 'Mee dhivehi liyumeh', + 'slug' => 'mee-dhivehi-slug-eh', + 'body' => 'Mee dhivehi liyumeh', ], 'jp' => [ 'title' => 'Kore wa taitorudesu', - 'slug' => 'kore-wa-namekujidesu', - 'body' => 'Kore wa kijidesu', + 'slug' => 'kore-wa-namekujidesu', + 'body' => 'Kore wa kijidesu', ], ], ]); @@ -323,13 +323,13 @@ public function it_can_clear_translations_for_all_locales() 'translations' => [ 'dv' => [ 'title' => 'Mee dhivehi title eh', - 'slug' => 'mee-dhivehi-slug-eh', - 'body' => 'Mee dhivehi liyumeh', + 'slug' => 'mee-dhivehi-slug-eh', + 'body' => 'Mee dhivehi liyumeh', ], 'jp' => [ 'title' => 'Kore wa taitorudesu', - 'slug' => 'kore-wa-namekujidesu', - 'body' => 'Kore wa kijidesu', + 'slug' => 'kore-wa-namekujidesu', + 'body' => 'Kore wa kijidesu', ], ], ]); @@ -344,17 +344,17 @@ public function it_can_clear_translations_for_all_locales() public function it_can_check_if_any_translation_for_a_specific_locale() { $article = Article::factory()->withAuthor()->create([ - 'lang' => 'en', + 'lang' => 'en', 'translations' => [ 'dv' => [ 'title' => 'Mee dhivehi title eh', - 'slug' => 'mee-dhivehi-slug-eh', - 'body' => 'Mee dhivehi liyumeh', + 'slug' => 'mee-dhivehi-slug-eh', + 'body' => 'Mee dhivehi liyumeh', ], 'jp' => [ 'title' => 'Kore wa taitorudesu', - 'slug' => 'kore-wa-namekujidesu', - 'body' => 'Kore wa kijidesu', + 'slug' => 'kore-wa-namekujidesu', + 'body' => 'Kore wa kijidesu', ], ], ]); @@ -371,17 +371,17 @@ public function it_can_check_if_any_translation_for_a_specific_locale() public function it_can_check_if_is_default_translation_locale() { $article = Article::factory()->withAuthor()->create([ - 'lang' => 'en', + 'lang' => 'en', 'translations' => [ 'dv' => [ 'title' => 'Mee dhivehi title eh', - 'slug' => 'mee-dhivehi-slug-eh', - 'body' => 'Mee dhivehi liyumeh', + 'slug' => 'mee-dhivehi-slug-eh', + 'body' => 'Mee dhivehi liyumeh', ], 'jp' => [ 'title' => 'Kore wa taitorudesu', - 'slug' => 'kore-wa-namekujidesu', - 'body' => 'Kore wa kijidesu', + 'slug' => 'kore-wa-namekujidesu', + 'body' => 'Kore wa kijidesu', ], ], ]); @@ -433,7 +433,7 @@ public function it_can_add_new_translation_locales_via_setter() public function it_can_add_new_translation_locales_via_app_locale() { $article = Article::factory()->withAuthor()->create([ - 'lang' => 'en', + 'lang' => 'en', 'title' => 'This is an English title', ]); @@ -459,7 +459,7 @@ public function it_can_add_translations_in_bulk() $article->addTranslations('dv', [ 'title' => 'Mee dhivehi title eh', - 'body' => 'Mee dhivehi liyumeh', + 'body' => 'Mee dhivehi liyumeh', ]); $this->assertEquals('Mee dhivehi title eh', $article->title_dv); @@ -481,8 +481,8 @@ public function it_cannot_add_translations_in_bulk_for_locales_that_are_not_allo $article->addTranslations('zh-CN', [ 'title' => '这是一个中文标题', - 'slug' => '这是一只中国蛞蝓', - 'body' => '这是一个中国人的身体', + 'slug' => '这是一只中国蛞蝓', + 'body' => '这是一个中国人的身体', ]); } @@ -572,7 +572,7 @@ public function it_cannot_delete_primary_translation() { $this->expectException(CannotDeletePrimaryTranslationException::class); $article = Article::factory()->withAuthor()->create([ - 'lang' => 'en', + 'lang' => 'en', 'title' => 'This is an English title', ]); @@ -588,10 +588,10 @@ public function it_cannot_delete_primary_translation() #[Test] public function it_fills_only_translatable_attributes() { - $article = new Article(); + $article = new Article; $attributes = [ 'title' => 'Test Title', - 'body' => 'Test Body', + 'body' => 'Test Body', ]; $article->fill($attributes); $this->assertEquals('Test Title', $article->title); @@ -601,11 +601,11 @@ public function it_fills_only_translatable_attributes() #[Test] public function it_fills_translatable_and_non_translatable_attributes() { - $article = new Article(); + $article = new Article; $attributes = [ 'title' => 'Test Title', - 'body' => 'Test Body', - 'slug' => 'test-title', + 'body' => 'Test Body', + 'slug' => 'test-title', ]; $article->fill($attributes); $this->assertEquals('Test Title', $article->title); @@ -616,12 +616,12 @@ public function it_fills_translatable_and_non_translatable_attributes() #[Test] public function it_gives_current_locale_language_as_attribute_values() { - $article = new Article(); + $article = new Article; $article->fill([ - 'title' => 'Dhivehi Title', - 'body' => 'Test Body', - 'slug' => 'dhivehi-title', - 'lang' => 'dv', + 'title' => 'Dhivehi Title', + 'body' => 'Test Body', + 'slug' => 'dhivehi-title', + 'lang' => 'dv', 'translations' => [ 'en' => [ 'title' => 'English Title', diff --git a/tests/Unit/LocaleMiddlewareTest.php b/tests/Unit/LocaleMiddlewareTest.php index 7a13dd4..f964a5c 100644 --- a/tests/Unit/LocaleMiddlewareTest.php +++ b/tests/Unit/LocaleMiddlewareTest.php @@ -19,26 +19,26 @@ protected function setUp(): void parent::setUp(); Language::create([ - 'name' => 'English', - 'code' => 'en', + 'name' => 'English', + 'code' => 'en', 'locale' => 'en', - 'flag' => '🇬🇧', + 'flag' => '🇬🇧', 'is_rtl' => false, 'active' => true, ]); Language::create([ - 'name' => 'Dhivehi', - 'code' => 'dv', + 'name' => 'Dhivehi', + 'code' => 'dv', 'locale' => 'dv', - 'flag' => '🇲🇻', + 'flag' => '🇲🇻', 'is_rtl' => true, 'active' => true, ]); Language::create([ - 'name' => 'Japanese', - 'code' => 'jp', + 'name' => 'Japanese', + 'code' => 'jp', 'locale' => 'jp', - 'flag' => '🇯🇵', + 'flag' => '🇯🇵', 'is_rtl' => false, 'active' => true, ]); diff --git a/tests/Unit/Models/LanguageTest.php b/tests/Unit/Models/LanguageTest.php index e36fefb..b4a474e 100644 --- a/tests/Unit/Models/LanguageTest.php +++ b/tests/Unit/Models/LanguageTest.php @@ -17,18 +17,18 @@ protected function setUp(): void parent::setUp(); Language::create([ - 'name' => 'English', - 'code' => 'en', + 'name' => 'English', + 'code' => 'en', 'locale' => 'en', - 'flag' => '🇬🇧', + 'flag' => '🇬🇧', 'is_rtl' => false, 'active' => true, ]); Language::create([ - 'name' => 'Dhivehi', - 'code' => 'dv', + 'name' => 'Dhivehi', + 'code' => 'dv', 'locale' => 'dv', - 'flag' => '🇲🇻', + 'flag' => '🇲🇻', 'is_rtl' => true, 'active' => true, ]); diff --git a/tests/Unit/Models/ModelTest.php b/tests/Unit/Models/ModelTest.php index b14cfef..2051908 100644 --- a/tests/Unit/Models/ModelTest.php +++ b/tests/Unit/Models/ModelTest.php @@ -11,7 +11,7 @@ class ModelTest extends TestCase #[Test] public function it_can_make_a_new_instance_of_the_model_even_if_the_languages_table_is_missing(): void { - $article = new Article(); + $article = new Article; $this->assertInstanceOf(Article::class, $article); } diff --git a/tests/Unit/TableCellTranslatedUrlGenerationTest.php b/tests/Unit/TableCellTranslatedUrlGenerationTest.php index 327b4b0..f4252bd 100644 --- a/tests/Unit/TableCellTranslatedUrlGenerationTest.php +++ b/tests/Unit/TableCellTranslatedUrlGenerationTest.php @@ -21,26 +21,26 @@ protected function setUp(): void parent::setUp(); Language::create([ - 'name' => 'English', - 'code' => 'en', + 'name' => 'English', + 'code' => 'en', 'locale' => 'en', - 'flag' => '🇬🇧', + 'flag' => '🇬🇧', 'is_rtl' => false, 'active' => true, ]); Language::create([ - 'name' => 'Dhivehi', - 'code' => 'dv', + 'name' => 'Dhivehi', + 'code' => 'dv', 'locale' => 'dv', - 'flag' => '🇲🇻', + 'flag' => '🇲🇻', 'is_rtl' => true, 'active' => true, ]); Language::create([ - 'name' => 'Japanese', - 'code' => 'jp', + 'name' => 'Japanese', + 'code' => 'jp', 'locale' => 'jp', - 'flag' => '🇯🇵', + 'flag' => '🇯🇵', 'is_rtl' => false, 'active' => true, ]); @@ -53,15 +53,15 @@ public function it_can_generate_show_url_for_json_translatable_record(): void /** @var JsonTranslatable $article */ $article = Article::factory()->withAuthor()->create([ - 'lang' => 'en', - 'title' => 'This is an English title', - 'slug' => 'this-is-an-english-slug', - 'body' => 'This is an English body', + 'lang' => 'en', + 'title' => 'This is an English title', + 'slug' => 'this-is-an-english-slug', + 'body' => 'This is an English body', 'translations' => [ 'dv' => [ 'title' => 'Mee dhivehi title eh', - 'slug' => 'mee-dhivehi-slug-eh', - 'body' => 'Mee dhivehi liyumeh', + 'slug' => 'mee-dhivehi-slug-eh', + 'body' => 'Mee dhivehi liyumeh', ], ], ]); @@ -82,14 +82,14 @@ public function it_can_generate_show_url_for_db_translatable_record(): void $this->withoutExceptionHandling(); $post = Post::factory()->withAuthor()->create([ - 'lang' => 'en', + 'lang' => 'en', 'title' => 'This is an English title', ]); app()->setLocale('dv'); $dv_post = Post::factory()->withAuthor()->create([ - 'lang' => 'dv', + 'lang' => 'dv', 'title' => 'Mee dhivehi title eh', 'translatable_parent_id' => $post->id, ]); From 80d899b8e0e7ab9c97126763719208ba26f0a8ff Mon Sep 17 00:00:00 2001 From: Naffah Abdulla Rasheed Date: Wed, 4 Nov 2026 14:23:19 +0500 Subject: [PATCH 02/10] Add the contract for optional translatable support in other packages Translatable::enabled()/supports()/locales()/inputs()/rules()/values()/status()/shared(), Locale and LocaleInput value objects, InteractsWithLanguages for tests, and the translatable:add-languages command. Suffixed attributes are staged until save on both storage types, models can list fields in $translatable, rows without a lang read as the default locale, and the languages cache no longer re-queries when empty. Refs #2 Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 13 + config/translatable.php | 60 +++- .../add_local_name_to_languages_table.php | 28 ++ .../migrations/create_languages_table.php | 1 + docs/advanced-usage/for-package-authors.md | 296 +++++++++++++++++ lang/dv/strings.php | 5 + lang/en/strings.php | 5 + src/Commands/AddLanguagesCommand.php | 55 ++++ src/Contracts/Translatable.php | 4 +- src/DbTranslatable/IsDbTranslatable.php | 129 ++++++-- src/Facades/Translatable.php | 23 ++ src/JsonTranslatable/IsJsonTranslatable.php | 99 +++--- src/LanguageRegistrar.php | 52 +-- src/Languages.php | 6 +- src/Locale.php | 84 +++++ src/LocaleInput.php | 63 ++++ src/Models/Language.php | 13 + src/Support/LanguagePresets.php | 57 ++++ src/Testing/InteractsWithLanguages.php | 64 ++++ src/Traits/IsTranslatable.php | 118 ++++++- .../DbTranslatedUrlGenerator.php | 2 +- src/Translatable.php | 298 +++++++++++++++++- src/TranslatableServiceProvider.php | 73 +++-- tests/Feature/StagedTranslationsTest.php | 184 +++++++++++ tests/TestSupport/Models/Page.php | 27 ++ .../2025_02_21_000000_create_pages_table.php | 27 ++ tests/Unit/AddLanguagesCommandTest.php | 49 +++ .../Contract/TranslatableContractTest.php | 237 ++++++++++++++ tests/Unit/LanguageRegistrarTest.php | 83 +++++ .../AddLocalNameToLanguagesTableTest.php | 46 +++ 30 files changed, 2049 insertions(+), 152 deletions(-) create mode 100644 database/migrations/add_local_name_to_languages_table.php create mode 100644 docs/advanced-usage/for-package-authors.md create mode 100644 lang/dv/strings.php create mode 100644 lang/en/strings.php create mode 100644 src/Commands/AddLanguagesCommand.php create mode 100644 src/Locale.php create mode 100644 src/LocaleInput.php create mode 100644 src/Support/LanguagePresets.php create mode 100644 src/Testing/InteractsWithLanguages.php create mode 100644 tests/Feature/StagedTranslationsTest.php create mode 100644 tests/TestSupport/Models/Page.php create mode 100644 tests/TestSupport/database/2025_02_21_000000_create_pages_table.php create mode 100644 tests/Unit/AddLanguagesCommandTest.php create mode 100644 tests/Unit/Contract/TranslatableContractTest.php create mode 100644 tests/Unit/LanguageRegistrarTest.php create mode 100644 tests/Unit/migrations/AddLocalNameToLanguagesTableTest.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 0768fe6..0408864 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,3 +6,16 @@ All notable changes to `javaabu/translatable` are documented in this file. - Requires PHP 8.4 and Laravel 12 or 13 - Javaabu codestyle enforced with Pint; CI runs PHP 8.4/8.5 × Laravel 12/13 × lowest/stable, with coverage on Codecov +- **Contract for other packages:** `Translatable::enabled()`, `supports()`, `locales()`, `inputs()`, `rules()`, `values()`, `status()` and `shared()`, with `Locale` and `LocaleInput` value objects. See `docs/advanced-usage/for-package-authors.md` +- `Javaabu\Translatable\Testing\InteractsWithLanguages` for tests in apps and packages +- `php artisan translatable:add-languages en dv` adds languages from built-in presets +- Models can list their fields in `protected array $translatable` instead of overriding `getTranslatables()` +- Setting a suffixed attribute (`$post->title_dv = …`, `fill(['title_dv' => …])`) stages the translation until the model is saved, for both storage types; `fill()` accepts per-locale inputs on DB translatable models too +- `addTranslation()` accepts any value, not only strings +- Rows without a `lang` are read as the default locale, not the current one +- JSON translatable models cast `translations` to an array themselves +- The languages list is cached once per request, an empty list isn't cached, and changing a language clears the cache +- `Languages` and the registrar are scoped, so the current locale doesn't leak between Octane requests +- `languages.local_name` column (the language's own name); `add_local_name_to_languages_table` migration for v2 apps +- New config: `enabled`, `thaana_locales`, `views`, `inertia` +- Fixed: `$model->url()` read `translatable.default_redirect` instead of `default_redirect_route` diff --git a/config/translatable.php b/config/translatable.php index 18cb51b..c91aa9b 100644 --- a/config/translatable.php +++ b/config/translatable.php @@ -3,10 +3,20 @@ use Javaabu\Translatable\Models\Language; return [ - // Allows for custom models + /* + * Turn translations off without uninstalling the package. Other packages + * check this through Translatable::enabled(). + */ + 'enabled' => env('TRANSLATABLE_ENABLED', true), + + /* + * The model for the languages table. Extend Javaabu\Translatable\Models\Language. + */ 'language_model' => Language::class, - // Standard fields to be ignored for translation on all models + /* + * Columns that are never translated, on every model + */ 'fields_ignored_for_translation' => [ 'id', 'lang', @@ -15,28 +25,50 @@ 'deleted_at', ], - // Whether attr_dv should fall back to app locale if translations do not exist + /* + * Whether suffixed attributes (title_dv) fall back to the model's own + * value when the translation is missing + */ 'lang_suffix_should_fallback' => false, - // Default locale before translations are added + /* + * The default translation locale: the language of a model's own columns + * when it has no `lang`, and the first language in the views + */ 'default_locale' => 'en', - // Cache configuration for the Language Registrar + /* + * Locales whose free-text inputs get the Thaana keyboard in the views + */ + 'thaana_locales' => ['dv'], + + /* + * Which views the app uses: 'blade' (javaabu/forms) or 'inertia' + * (React with @javaabu/forms, shipped through javaabu/manifest) + */ + 'views' => env('TRANSLATABLE_VIEWS', 'blade'), + + /* + * With 'inertia' views, the languages are shared with every page as this prop + */ + 'inertia' => [ + 'share' => true, + 'prop' => 'translatable', + ], + + /* + * Cache for the active languages + */ 'cache' => [ 'expiration_time' => DateInterval::createFromDateString('24 hours'), 'key' => 'translation.languages.cache', 'driver' => 'default', ], - 'styles' => [ - 'table-cell-wrapper' => 'd-flex justify-content-center align-items-center', - - 'icons' => [ - 'add' => 'fas fa-plus', - 'edit' => 'fas fa-edit', - ], - ], - + /* + * Used by $model->url() when a DB translatable model has no translation in + * the requested locale on the default portal + */ 'default_portal' => 'web', 'default_redirect_route' => 'home', ]; diff --git a/database/migrations/add_local_name_to_languages_table.php b/database/migrations/add_local_name_to_languages_table.php new file mode 100644 index 0000000..7fd8510 --- /dev/null +++ b/database/migrations/add_local_name_to_languages_table.php @@ -0,0 +1,28 @@ +string('local_name')->nullable()->after('name'); + }); + } + + public function down(): void + { + // The column belongs to create_languages_table on new installs, so it stays + } +}; diff --git a/database/migrations/create_languages_table.php b/database/migrations/create_languages_table.php index 5270578..d2681ca 100644 --- a/database/migrations/create_languages_table.php +++ b/database/migrations/create_languages_table.php @@ -14,6 +14,7 @@ public function up(): void $table->string('code'); $table->string('locale'); $table->string('name'); + $table->string('local_name')->nullable(); $table->string('flag'); $table->boolean('is_rtl'); $table->boolean('active'); diff --git a/docs/advanced-usage/for-package-authors.md b/docs/advanced-usage/for-package-authors.md new file mode 100644 index 0000000..03fe07c --- /dev/null +++ b/docs/advanced-usage/for-package-authors.md @@ -0,0 +1,296 @@ +--- +title: For package authors +sidebar_position: 1 +--- + +Many Javaabu packages (permissions, settings, notifications, locations, meta, menus, widgets, form-builder, post-types, cms, …) let apps translate their models **when javaabu/translatable is installed**, and work exactly as before when it isn't. This page is the contract they build on. Everything here is covered by semver: it doesn't change within v3. + +The example is a `javaabu/locations` package whose `Island` names can be translated (`Malé` / `މާލެ`). + +# composer.json + +Translatable is optional: suggest it, and install it for your tests. + +```json +{ + "suggest": { + "javaabu/translatable": "Translate island names (^3.0)" + }, + "require-dev": { + "javaabu/translatable": "^3.0@dev" + }, + "repositories": [ + { "type": "vcs", "url": "https://github.com/n4ff4h/translatable" } + ] +} +``` + +Drop the repository once v3 is on Packagist. + +# Detecting translatable + +Never call a translatable class without checking that it exists. Put the check in one place in your package: + +```php +namespace Javaabu\Locations\Support; + +use Javaabu\Translatable\Translatable; + +class Translations +{ + public static function enabled(): bool + { + return class_exists(Translatable::class) && Translatable::enabled(); + } + + public static function supports(object|string|null $model, ?string $field = null): bool + { + return class_exists(Translatable::class) && Translatable::supports($model, $field); + } +} +``` + +- `Translatable::enabled()` is `true` when `translatable.enabled` is on and at least one language is active. +- `Translatable::supports($model, $field = null)` is `true` when it's enabled, the model (an instance or a class name) implements `Javaabu\Translatable\Contracts\Translatable`, and the field (if given) is one of its translatable fields. +- `$model instanceof \Javaabu\Translatable\Contracts\Translatable` is safe even when the package isn't installed: PHP returns `false` for an unknown class. + +# Making your model translatable + +Translations are opt-in per model. Use the **JSON** storage for package models: it adds two columns and keeps one row per record, so ids, routes and relations don't change. + +## 1. Add the columns + +Your migration runs whether or not translatable is installed, so add the columns yourself, both nullable: + +```php +Schema::create('islands', function (Blueprint $table) { + $table->id(); + $table->string('name'); + // ... + + // Translations (javaabu/translatable). Harmless without it. + $table->json('translations')->nullable(); + $table->string('lang')->nullable()->index(); + + $table->timestamps(); +}); +``` + +Rows saved before translatable was installed have no `lang`; they're read as the default locale (`translatable.default_locale`). + +## 2. Let the model be swapped + +Your package resolves its model class from config, as every next-gen package does: + +```php +// config/locations.php +'models' => [ + 'island' => Javaabu\Locations\Models\Island::class, +], +``` + +Then pick one of these. Both are fine; the first needs nothing from your package. + +### The app opts in + +The app extends your model, adds the trait, and points your config at it: + +```php +namespace App\Models; + +use Javaabu\Locations\Models\Island as BaseIsland; +use Javaabu\Translatable\Contracts\JsonTranslatable; +use Javaabu\Translatable\JsonTranslatable\IsJsonTranslatable; + +class Island extends BaseIsland implements JsonTranslatable +{ + use IsJsonTranslatable; + + protected array $translatable = ['name']; +} +``` + +```php +// config/locations.php +'models' => ['island' => App\Models\Island::class], +``` + +### Your package ships the translatable model + +Put a subclass next to your model. PHP only loads it when it's referenced, so it's safe to ship even when translatable isn't installed: + +```php +namespace Javaabu\Locations\Models\Translatable; + +use Javaabu\Locations\Models\Island as BaseIsland; +use Javaabu\Translatable\Contracts\JsonTranslatable; +use Javaabu\Translatable\JsonTranslatable\IsJsonTranslatable; + +class Island extends BaseIsland implements JsonTranslatable +{ + use IsJsonTranslatable; + + protected array $translatable = ['name']; + + // Keep morph types pointing at the base model + public function getMorphClass(): string + { + return BaseIsland::class; + } +} +``` + +If the base model uses `HasFactory`, override `newFactory()` in the subclass to return the base model's factory, and register policies for both classes. + +Choose it when the app hasn't configured a model: + +```php +// LocationsServiceProvider::register() +$this->app->bind('locations.models.island', function () { + return config('locations.models.island') + ?? (Translations::enabled() ? Models\Translatable\Island::class : Models\Island::class); +}); +``` + +`Translations::enabled()` reads the languages table, so call it lazily (in a binding or when the model is first needed), not while providers are registering. + +# Forms + +A translatable field has one input per active locale, named `{field}_{locale}`, the default locale included: `name_en`, `name_dv`. Validation, old input, model binding and `fill()` all use these names. + +## Validation + +Expand your rules with `Translatable::rules()`. The model's primary locale keeps the rules you wrote; the others get `nullable` instead of `required`: + +```php +public function rules(): array +{ + $rules = [ + 'name' => ['required', 'string', 'max:255'], + 'atoll_id' => ['required', 'exists:atolls,id'], + ]; + + $island = $this->route('island') ?? config('locations.models.island'); + + if (Translations::supports($island)) { + $rules = Translatable::rules($island, $rules); + // ['name_en' => ['required', 'string', 'max:255'], 'name_dv' => ['nullable', 'string', 'max:255'], 'atoll_id' => [...]] + } + + return $rules; +} +``` + +Pass `require_all: true` to require every locale. Fields that aren't translatable pass through unchanged. + +## Saving + +`fill()`, `create()` and `update()` accept the per-locale names, and the translations are written when the model is saved: + +```php +$island = Island::create($request->validated()); // name_en and name_dv +$island->update($request->validated()); +``` + +## Rendering (Blade) + +Use translatable's components when the field is translatable, and your usual component otherwise: + +```blade +@if(\Javaabu\Locations\Support\Translations::supports($island, 'name')) + +@else + +@endif +``` + +`` renders one `` per locale (with `dir`, `lang` and the Thaana keyboard for Dhivehi), in tabs. See [Blade components](../basic-usage/blade-components.md). + +## Rendering (Inertia React) + +Send the inputs and values with the page; don't import translatable's React components from your package, because they only exist in apps that install it: + +```php +return Inertia::render('locations::Islands/Edit', [ + 'island' => $island, + 'translations' => Translations::supports($island) ? [ + 'values' => Translatable::values($island), // ['name_en' => 'Malé', 'name_dv' => 'މާލެ'] + 'inputs' => ['name' => Translatable::inputs('name', $island)->values()], + ] : null, +]); +``` + +Each input serializes to `{ field, name, locale, direction, thaana, primary, language: { code, name, native_name, … } }`, enough to render one `@javaabu/forms` `` per locale with `dir`, `thaana` and a label. Apps that install translatable can use its `TranslatableText` component, which takes the same data. See [React components](../basic-usage/react-components.md). + +## Stencil fields + +javaabu/stencil fields call `->translatable()`. When `Translatable::supports($model, $field)` is true, the field expands into the per-locale inputs from `Translatable::inputs()`; otherwise it renders as a plain field. Both of stencil's drivers use the same data, so Blade and React forms match. + +# Reading translations + +| Call | Gives | +| --- | --- | +| `$island->name` | The name in the current locale, falling back to the model's own value | +| `$island->name_dv` | The Dhivehi name, or `null` (see `lang_suffix_should_fallback`) | +| `$island->translate('name', 'dv', fallback: false)` | The same, explicitly | +| `Translatable::values($island)` | Every translatable field in every locale, keyed by input name | +| `Translatable::status($island)` | Which locales the model has, for status badges | +| `$island->toArray()` | Translatable fields in the current locale | + +For an API resource, merge `Translatable::values($island)` when your clients need every locale. + +# API reference + +All static, on `Javaabu\Translatable\Translatable`: + +| Method | Returns | +| --- | --- | +| `enabled()` | `bool` | +| `supports(object\|string\|null $model, ?string $field = null)` | `bool` | +| `isDbTranslatable(object\|string\|null $model)` | `bool`: the model keeps each locale in its own row | +| `locales()` | `Collection`, the default first | +| `locale(?string $code = null)` | `?Locale`, the current one by default | +| `defaultLocale()` / `currentLocale()` | `string` | +| `primaryLocale(object\|string\|null $model = null)` | `string`: the model's `lang`, or the current locale for a new model | +| `inputName(string $field, string $locale)` | `string`: `name_dv` | +| `inputs(string $field, object\|string\|null $model = null)` | `Collection` keyed by locale, the primary first | +| `rules(object\|string\|null $model, array $rules, bool $require_all = false)` | `array` | +| `values(?Model $model, ?array $fields = null)` | `array` | +| `status(?Model $model, ?Closure $url = null)` | `array` of `{locale, translated, primary, url, language}` | +| `shared()` | `{enabled, current, default, direction, locales}` for the front end | + +`Locale` has `code`, `locale` (the HTML `lang`), `name`, `native_name`, `direction`, `is_default`, `flag_url`, `isRtl()`, `isThaana()` and `toArray()`. `LocaleInput` has `field`, `name`, `locale` (a `Locale`), `is_primary`, `label(?string $label)` ("Name (ދިވެހި)") and `toArray()`. + +# Testing both ways + +Test with translatable on, and with it off: + +```php +use Javaabu\Translatable\Testing\InteractsWithLanguages; + +class IslandTranslationsTest extends TestCase +{ + use InteractsWithLanguages; + use RefreshDatabase; + + #[Test] + public function it_saves_the_dhivehi_name(): void + { + $this->setUpLanguages(['en', 'dv']); // creates the languages table if needed, adds the languages + + $this->post('/islands', ['name_en' => 'Malé', 'name_dv' => 'މާލެ', 'atoll_id' => $atoll->id]); + + $this->assertEquals('މާލެ', Island::first()->name_dv); + } + + #[Test] + public function it_works_without_translations(): void + { + $this->withoutTranslatable(function () { + $this->post('/islands', ['name' => 'Malé', 'atoll_id' => $atoll->id])->assertRedirect(); + }); + } +} +``` + +To be sure nothing references translatable when it's missing, add a CI leg that removes it before running the tests that don't need it: `composer remove --dev javaabu/translatable --no-update && composer update`. diff --git a/lang/dv/strings.php b/lang/dv/strings.php new file mode 100644 index 0000000..5556c30 --- /dev/null +++ b/lang/dv/strings.php @@ -0,0 +1,5 @@ + ':label (:locale)', +]; diff --git a/lang/en/strings.php b/lang/en/strings.php new file mode 100644 index 0000000..5556c30 --- /dev/null +++ b/lang/en/strings.php @@ -0,0 +1,5 @@ + ':label (:locale)', +]; diff --git a/src/Commands/AddLanguagesCommand.php b/src/Commands/AddLanguagesCommand.php new file mode 100644 index 0000000..46ab60c --- /dev/null +++ b/src/Commands/AddLanguagesCommand.php @@ -0,0 +1,55 @@ +argument('codes') ?: array_values(array_unique([config('translatable.default_locale'), 'dv'])); + + /** @var class-string $language_class */ + $language_class = config('translatable.language_model', Language::class); + + $added = 0; + + foreach ($codes as $code) { + $this->info("Adding `{$code}`..."); + + $query = method_exists($language_class, 'bootSoftDeletes') + ? $language_class::query()->withTrashed() + : $language_class::query(); + + if ($query->where('code', $code)->exists()) { + $this->comment("`{$code}` is already in the languages table."); + + continue; + } + + if (! LanguagePresets::has($code)) { + $this->warn("There's no preset for `{$code}`; check its name and direction in the languages table."); + } + + $language_class::query()->create([ + ...LanguagePresets::get($code), + 'active' => ! $this->option('inactive'), + ]); + + $added++; + } + + $this->comment("Added {$added} language(s)."); + + return self::SUCCESS; + } +} diff --git a/src/Contracts/Translatable.php b/src/Contracts/Translatable.php index 74e848a..23adf26 100644 --- a/src/Contracts/Translatable.php +++ b/src/Contracts/Translatable.php @@ -15,6 +15,8 @@ public function getFieldsIgnoredForTranslation(): array; /** * Get the translatable fields + * + * @return array */ public function getTranslatables(): array; @@ -91,7 +93,7 @@ public function isAllowedTranslationLocale(string $locale): bool; * * @throws LanguageNotAllowedException */ - public function addTranslation(string $locale, string $field, string $value): static; + public function addTranslation(string $locale, string $field, mixed $value): static; /** * Add a new locale to this object diff --git a/src/DbTranslatable/IsDbTranslatable.php b/src/DbTranslatable/IsDbTranslatable.php index 62501db..03d8814 100644 --- a/src/DbTranslatable/IsDbTranslatable.php +++ b/src/DbTranslatable/IsDbTranslatable.php @@ -15,6 +15,13 @@ trait IsDbTranslatable use DbTranslatedUrlGenerator; use IsTranslatable; + /** + * Translations set through attributes, written to their rows after the model is saved + * + * @var array> + */ + protected array $pending_translations = []; + public static function bootIsDbTranslatable(): void { static::creating(function (Translatable $model) { @@ -24,6 +31,32 @@ public static function bootIsDbTranslatable(): void $model->lang = translation_locale(); } }); + + static::saved(function (Translatable $model) { + $model->savePendingTranslations(); + }); + } + + /** + * Write the translations staged through attributes ($post->title_dv = …) + */ + public function savePendingTranslations(): void + { + $pending = $this->pending_translations; + + $this->pending_translations = []; + + foreach ($pending as $locale => $fields) { + $this->writeTranslation($locale, $fields); + } + } + + /** + * @return array> + */ + public function getPendingTranslations(): array + { + return $this->pending_translations; } public array $fields_ignored_for_translation = ['id', 'translatable_parent_id', 'lang']; @@ -104,6 +137,10 @@ public function translate(string $field, ?string $locale = null, bool $fallback return $fallback ? $this->getAttributeValue($field) : null; } + if (array_key_exists($field, $this->pending_translations[$locale] ?? [])) { + return $this->pending_translations[$locale][$field]; + } + $translation = $this->getTranslation($locale); if ($translation) { @@ -154,6 +191,10 @@ public function hasTranslation(?string $locale = null): bool return true; } + if (isset($this->pending_translations[$locale])) { + return true; + } + if ($this->isDefaultTranslation()) { $translation = $this->translations()->where('lang', $locale); @@ -172,10 +213,44 @@ public function hasTranslation(?string $locale = null): bool } /** + * Add a translation and save it to its row + * + * @throws LanguageNotAllowedException + * @throws FieldNotAllowedException + */ + public function addTranslation(string $locale, string $field, mixed $value): static + { + $this->ensureTranslationAllowed($locale, $field); + + return $this->writeTranslation($locale, [$field => $value]); + } + + /** + * Stage a translation; the row is written when this model is saved + * * @throws LanguageNotAllowedException * @throws FieldNotAllowedException */ - public function addTranslation(string $locale, string $field, string $value): static + protected function stageTranslation(string $locale, string $field, mixed $value): static + { + $this->ensureTranslationAllowed($locale, $field); + + if ($locale === $this->primaryLocale()) { + $this->setAttributeInternal($field, $value); + + return $this; + } + + $this->pending_translations[$locale][$field] = $value; + + return $this; + } + + /** + * @throws LanguageNotAllowedException + * @throws FieldNotAllowedException + */ + protected function ensureTranslationAllowed(string $locale, string $field): void { if (! $this->isAllowedTranslationLocale($locale)) { throw LanguageNotAllowedException::create($locale); @@ -184,45 +259,55 @@ public function addTranslation(string $locale, string $field, string $value): st if (! $this->isTranslatable($field)) { throw FieldNotAllowedException::create($field, $locale); } + } + /** + * Write the given fields to the row for the locale, creating it from the default translation if needed + * + * @param array $fields + */ + protected function writeTranslation(string $locale, array $fields): static + { // Get the default translation (root/parent translation). If the current record is // the default, we will use it, otherwise we will fetch the default translation - $defaultTranslation = $this->isDefaultTranslation() ? $this : $this->defaultTranslation; + $default_translation = $this->isDefaultTranslation() ? $this : $this->defaultTranslation; - // check if the default translation is already the correct locale - if ($defaultTranslation->lang === $locale) { - $defaultTranslation->setAttributeInternal($field, $value); + if ($default_translation->lang === $locale) { + foreach ($fields as $field => $value) { + $default_translation->setAttributeInternal($field, $value); + } - $defaultTranslation->save(); + $default_translation->save(); - return $defaultTranslation; + return $default_translation; } - // check if a translated object exists for this locale - $newTranslation = $defaultTranslation->translations()->where('lang', $locale)->first(); + $translation = $default_translation->translations()->where('lang', $locale)->first(); - // if there is none, make a new blank translation of this object - if (! $newTranslation) { - $newTranslation = new self; - // copy all the attributes of the current object to the new translation - // this ensures no columns are left null + // if there is none, make a new translation of the default translation, + // copying its columns so that none are left empty + if (! $translation) { + $translation = new self; foreach ($this->getAllAttributes() as $attribute) { - // TODO: make this primary key rely on some sort of config available per model - if ($attribute === 'id') { + if ($attribute === $this->getKeyName()) { continue; } - $newTranslation->setAttributeInternal($attribute, $defaultTranslation->getAttributeValue($attribute)); + $translation->setAttributeInternal($attribute, $default_translation->getAttributeValue($attribute)); } - $newTranslation->setAttributeInternal('translatable_parent_id', $defaultTranslation->id); - $newTranslation->setAttributeInternal('lang', $locale); + + $translation->setAttributeInternal('translatable_parent_id', $default_translation->getKey()); + $translation->setAttributeInternal('lang', $locale); + } + + foreach ($fields as $field => $value) { + $translation->setAttributeInternal($field, $value); } - $newTranslation->setAttributeInternal($field, $value); - $newTranslation->save(); + $translation->save(); - return $newTranslation; + return $translation; } /** diff --git a/src/Facades/Translatable.php b/src/Facades/Translatable.php index d309795..7b7dd27 100644 --- a/src/Facades/Translatable.php +++ b/src/Facades/Translatable.php @@ -2,9 +2,32 @@ namespace Javaabu\Translatable\Facades; +use Closure; +use Illuminate\Database\Eloquent\Model; +use Illuminate\Support\Collection; use Illuminate\Support\Facades\Facade; +use Javaabu\Translatable\Locale; +use Javaabu\Translatable\LocaleInput; +use Javaabu\Translatable\Models\Language; /** + * @method static bool enabled() + * @method static bool supports(object|string|null $model, ?string $field = null) + * @method static bool isDbTranslatable(object|string|null $model) + * @method static Collection locales() + * @method static Locale|null locale(?string $code = null) + * @method static string defaultLocale() + * @method static string currentLocale() + * @method static string primaryLocale(object|string|null $model = null) + * @method static string inputName(string $field, string $locale) + * @method static Collection inputs(string $field, object|string|null $model = null) + * @method static array rules(object|string|null $model, array $rules, bool $require_all = false) + * @method static array values(Model|null $model, array|null $fields = null) + * @method static array status(Model|null $model, Closure|null $url = null) + * @method static array shared() + * @method static array getAllowedTranslationLocales() + * @method static bool isAllowedTranslationLocale(Language|string $locale) + * * @see \Javaabu\Translatable\Translatable */ class Translatable extends Facade diff --git a/src/JsonTranslatable/IsJsonTranslatable.php b/src/JsonTranslatable/IsJsonTranslatable.php index ab1a492..e91e027 100644 --- a/src/JsonTranslatable/IsJsonTranslatable.php +++ b/src/JsonTranslatable/IsJsonTranslatable.php @@ -25,6 +25,13 @@ public static function bootIsJsonTranslatable(): void public array $fields_ignored_for_translation = ['id', 'translations', 'lang']; + public function initializeIsJsonTranslatable(): void + { + if (! $this->hasCast('translations')) { + $this->mergeCasts(['translations' => 'array']); + } + } + /** * Get fields that will be ignored for translation */ @@ -89,7 +96,7 @@ public function translate(string $field, ?string $locale = null, bool $fallback */ public function getDefaultTranslationLocale(): string { - return $this->getAttributeValue('lang') ?? translation_locale(); + return $this->primaryLocale(); } /** @@ -134,15 +141,31 @@ public function hasTranslation(?string $locale = null): bool } /** - * Add a new locale to this object + * Add a translation and save the model * - * @param string $value * @return $this * * @throws LanguageNotAllowedException * @throws FieldNotAllowedException */ - public function addTranslation(string $locale, string $field, $value): static + public function addTranslation(string $locale, string $field, mixed $value): static + { + $this->stageTranslation($locale, $field, $value); + + $this->save(); + + return $this; + } + + /** + * Put a translation in the translations column without saving + * + * @return $this + * + * @throws LanguageNotAllowedException + * @throws FieldNotAllowedException + */ + protected function stageTranslation(string $locale, string $field, mixed $value): static { if (! $this->isAllowedTranslationLocale($locale)) { throw LanguageNotAllowedException::create($locale); @@ -152,35 +175,32 @@ public function addTranslation(string $locale, string $field, $value): static throw FieldNotAllowedException::create($field, $locale); } - /** @var array $translations */ - $translations = $this->translations ?? []; - if ($this->isDefaultTranslationLocale($locale)) { $this->setAttributeInternal($field, $value); $this->setAttributeInternal('lang', $locale); - $this->save(); return $this; } + if (empty($this->getAttributeValue('lang'))) { + $this->setAttributeInternal('lang', $this->primaryLocale()); + } + + $translations = Arr::wrap($this->getAttributeValue('translations')); + $translations[$locale] = array_merge( - array_key_exists($locale, $translations) ? $translations[$locale] : [], + $translations[$locale] ?? [], [$field => $value], ['lang' => $locale], ); - $this->translations = $translations; - if (! $this->isDefaultTranslationLocale($locale)) { - $this->setTranslationAttributeValue($field, $locale, $value); + $this->setAttributeInternal('translations', $translations); - // if it's a new model and the default value is not set, set the default - if ((! $this->exists) && (! parent::getAttribute($field))) { - $this->setAttributeInternal($field, $value); - } + // A new model needs its own columns filled, so a translation fills them until the primary value is set + if ((! $this->exists) && (! parent::getAttribute($field))) { + $this->setAttributeInternal($field, $value); } - $this->save(); - return $this; } @@ -193,7 +213,7 @@ public function deleteTranslation(string $locale): void throw CannotDeletePrimaryTranslationException::create($locale); } - if (! array_key_exists($locale, $this->translations)) { + if (! array_key_exists($locale, Arr::wrap($this->translations))) { return; } @@ -215,47 +235,6 @@ public function deleteTranslations(): void $this->translations = []; } - public function getFillableTranslatables(): array - { - $translatables = $this->getTranslatables(); - $fillables = $this->getFillable(); - - $translatable_fillables = array_intersect($translatables, $fillables); - $language_codes = Languages::all()->pluck('code')->all(); - - $suffixed_fillables = []; - - foreach ($translatable_fillables as $fillable) { - foreach ($language_codes as $code) { - $suffixed_fillables[] = $fillable . '_' . $code; - } - } - - return $suffixed_fillables; - } - - protected function fillableTranslatablesFromArray(array $attributes): array - { - return array_intersect_key($attributes, array_flip($this->getFillableTranslatables())); - } - - /** - * @return $this - * - * @throws FieldNotAllowedException - * @throws LanguageNotAllowedException - */ - public function fill(array $attributes): static - { - $fillable_translatables = $this->fillableTranslatablesFromArray($attributes); - - foreach ($fillable_translatables as $key => $value) { - $this->setAttribute($key, $value); - } - - return parent::fill($attributes); - } - /** * Get the value of an attribute using its mutator for array conversion. * diff --git a/src/LanguageRegistrar.php b/src/LanguageRegistrar.php index a99605c..70e6bf3 100644 --- a/src/LanguageRegistrar.php +++ b/src/LanguageRegistrar.php @@ -28,7 +28,7 @@ class LanguageRegistrar public static string $cache_driver; /** - * PermissionRegistrar constructor. + * LanguageRegistrar constructor. */ public function __construct( string $language_class, @@ -88,24 +88,8 @@ public function forgetCachedLanguages(): void */ public function getLanguages(array $params = []): Collection { - // If the languages are empty, forget the cache - if ($this->languages?->isEmpty()) { - $this->forgetCachedLanguages(); - } - - // Load up languages from cache if ($this->languages === null) { - try { - $this->languages = $this->cache->remember(self::$cache_key, self::$cache_expiration_time, function () { - return $this->getLanguageClass() - ->active() - ->get(); - }); - } catch (QueryException $e) { - // silently fail if languages can't be loaded - Log::error('LanguageRegistrarError: ' . $e->getMessage()); - $this->languages = collect(); - } + $this->languages = $this->loadLanguages(); } $languages = clone $this->languages; @@ -117,6 +101,38 @@ public function getLanguages(array $params = []): Collection return $languages; } + /** + * Load the active languages from the cache, or the database when they aren't cached + * + * An empty result isn't cached, so languages added later show up straight away. + */ + protected function loadLanguages(): Collection + { + try { + $languages = $this->cache->get(self::$cache_key); + + if ($languages instanceof Collection && $languages->isNotEmpty()) { + return $languages; + } + + $languages = $this->getLanguageClass() + ->active() + ->orderBy('id') + ->get(); + + if ($languages->isNotEmpty()) { + $this->cache->put(self::$cache_key, $languages, self::$cache_expiration_time); + } + + return $languages; + } catch (QueryException $e) { + // silently fail if languages can't be loaded + Log::error('LanguageRegistrarError: ' . $e->getMessage()); + + return collect(); + } + } + /** * Get an instance of the language class. */ diff --git a/src/Languages.php b/src/Languages.php index a6e70f2..01be8b3 100644 --- a/src/Languages.php +++ b/src/Languages.php @@ -7,7 +7,7 @@ class Languages { - protected static ?string $current_locale = null; + protected ?string $current_locale = null; public function all(): Collection { @@ -77,12 +77,12 @@ public function default(): Language */ public function currentLanguageCode(): string { - return static::$current_locale ?: app()->getLocale(); + return $this->current_locale ?: app()->getLocale(); } public function setCurrentLocale(string $locale, bool $set_app_locale = true): void { - static::$current_locale = $locale; + $this->current_locale = $locale; if ($set_app_locale) { app()->setLocale($locale); diff --git a/src/Locale.php b/src/Locale.php new file mode 100644 index 0000000..42dd08b --- /dev/null +++ b/src/Locale.php @@ -0,0 +1,84 @@ + + */ +final readonly class Locale implements Arrayable, JsonSerializable +{ + public function __construct( + public string $code, + public string $locale, + public string $name, + public string $native_name, + public string $direction, + public bool $is_default, + public ?string $flag_url = null, + ) {} + + public static function fromLanguage(Language $language): self + { + return new self( + code: $language->code, + locale: $language->locale ?: $language->code, + name: $language->name, + native_name: $language->local_name ?: $language->name, + direction: $language->is_rtl ? 'rtl' : 'ltr', + is_default: $language->code === Translatable::defaultLocale(), + flag_url: $language->flag ? $language->flag_url : null, + ); + } + + public function isRtl(): bool + { + return $this->direction === 'rtl'; + } + + /** + * Whether free-text inputs in this locale should use the Thaana keyboard + */ + public function isThaana(): bool + { + return in_array($this->code, config('translatable.thaana_locales', ['dv']), true); + } + + /** + * @return array{ + * code: string, + * locale: string, + * name: string, + * native_name: string, + * direction: string, + * rtl: bool, + * thaana: bool, + * default: bool, + * flag_url: ?string + * } + */ + public function toArray(): array + { + return [ + 'code' => $this->code, + 'locale' => $this->locale, + 'name' => $this->name, + 'native_name' => $this->native_name, + 'direction' => $this->direction, + 'rtl' => $this->isRtl(), + 'thaana' => $this->isThaana(), + 'default' => $this->is_default, + 'flag_url' => $this->flag_url, + ]; + } + + public function jsonSerialize(): array + { + return $this->toArray(); + } +} diff --git a/src/LocaleInput.php b/src/LocaleInput.php new file mode 100644 index 0000000..58b9111 --- /dev/null +++ b/src/LocaleInput.php @@ -0,0 +1,63 @@ + + */ +final readonly class LocaleInput implements Arrayable, JsonSerializable +{ + public function __construct( + public string $field, + public string $name, + public Locale $locale, + public bool $is_primary, + ) {} + + /** + * The field label followed by the locale's own name: "Title (ދިވެހި)" + */ + public function label(?string $label = null): string + { + $label ??= str($this->field)->replace('_', ' ')->ucfirst()->toString(); + + return __('translatable::strings.field_in_locale', [ + 'label' => $label, + 'locale' => $this->locale->native_name, + ]); + } + + /** + * @return array{ + * field: string, + * name: string, + * locale: string, + * direction: string, + * thaana: bool, + * primary: bool, + * language: array + * } + */ + public function toArray(): array + { + return [ + 'field' => $this->field, + 'name' => $this->name, + 'locale' => $this->locale->code, + 'direction' => $this->locale->direction, + 'thaana' => $this->locale->isThaana(), + 'primary' => $this->is_primary, + 'language' => $this->locale->toArray(), + ]; + } + + public function jsonSerialize(): array + { + return $this->toArray(); + } +} diff --git a/src/Models/Language.php b/src/Models/Language.php index f0ed6ce..feae761 100644 --- a/src/Models/Language.php +++ b/src/Models/Language.php @@ -10,6 +10,7 @@ use Javaabu\Helpers\AdminModel\IsAdminModel; use Javaabu\Translatable\Enums\Flags; use Javaabu\Translatable\Facades\Languages; +use Javaabu\Translatable\LanguageRegistrar; class Language extends Model implements AdminModel { @@ -72,6 +73,18 @@ protected function casts(): array ]; } + protected static function booted(): void + { + $forget = fn () => app(LanguageRegistrar::class)->forgetCachedLanguages(); + + static::saved($forget); + static::deleted($forget); + + if (method_exists(static::class, 'restored')) { + static::restored($forget); + } + } + public function getRouteKeyName(): string { return 'locale'; diff --git a/src/Support/LanguagePresets.php b/src/Support/LanguagePresets.php new file mode 100644 index 0000000..90bc76c --- /dev/null +++ b/src/Support/LanguagePresets.php @@ -0,0 +1,57 @@ + + */ + protected static array $presets = [ + 'en' => ['name' => 'English', 'local_name' => 'English', 'locale' => 'en', 'flag' => 'gb', 'is_rtl' => false], + 'dv' => ['name' => 'Dhivehi', 'local_name' => 'ދިވެހި', 'locale' => 'dv', 'flag' => 'mv', 'is_rtl' => true], + 'ar' => ['name' => 'Arabic', 'local_name' => 'العربية', 'locale' => 'ar', 'flag' => 'sa', 'is_rtl' => true], + 'zh' => ['name' => 'Chinese', 'local_name' => '中文', 'locale' => 'zh', 'flag' => 'cn', 'is_rtl' => false], + 'ja' => ['name' => 'Japanese', 'local_name' => '日本語', 'locale' => 'ja', 'flag' => 'jp', 'is_rtl' => false], + 'hi' => ['name' => 'Hindi', 'local_name' => 'हिन्दी', 'locale' => 'hi', 'flag' => 'in', 'is_rtl' => false], + 'ru' => ['name' => 'Russian', 'local_name' => 'Русский', 'locale' => 'ru', 'flag' => 'ru', 'is_rtl' => false], + 'de' => ['name' => 'German', 'local_name' => 'Deutsch', 'locale' => 'de', 'flag' => 'de', 'is_rtl' => false], + 'fr' => ['name' => 'French', 'local_name' => 'Français', 'locale' => 'fr', 'flag' => 'fr', 'is_rtl' => false], + 'it' => ['name' => 'Italian', 'local_name' => 'Italiano', 'locale' => 'it', 'flag' => 'it', 'is_rtl' => false], + 'es' => ['name' => 'Spanish', 'local_name' => 'Español', 'locale' => 'es', 'flag' => 'es', 'is_rtl' => false], + ]; + + public static function has(string $code): bool + { + return array_key_exists($code, static::$presets); + } + + /** + * The attributes for a languages row: a preset, or a minimal row for an unknown code + * + * @return array{code: string, name: string, local_name: string, locale: string, flag: string, is_rtl: bool, active: bool} + */ + public static function get(string $code): array + { + $preset = static::$presets[$code] ?? [ + 'name' => mb_strtoupper($code), + 'local_name' => mb_strtoupper($code), + 'locale' => $code, + 'flag' => $code, + 'is_rtl' => false, + ]; + + return ['code' => $code, ...$preset, 'active' => true]; + } + + /** + * @return array + */ + public static function codes(): array + { + return array_keys(static::$presets); + } +} diff --git a/src/Testing/InteractsWithLanguages.php b/src/Testing/InteractsWithLanguages.php new file mode 100644 index 0000000..5979984 --- /dev/null +++ b/src/Testing/InteractsWithLanguages.php @@ -0,0 +1,64 @@ +$this->setUpLanguages(['en', 'dv']); creates the languages table when it's + * missing and adds the languages, so models can be translated straight away. + */ +trait InteractsWithLanguages +{ + /** + * @param array $codes + */ + protected function setUpLanguages(array $codes = ['en', 'dv'], ?string $current = null): void + { + $this->createLanguagesTable(); + + /** @var class-string $language_class */ + $language_class = config('translatable.language_model', Language::class); + + foreach ($codes as $code) { + $language_class::query()->firstOrCreate(['code' => $code], LanguagePresets::get($code)); + } + + app(LanguageRegistrar::class)->forgetCachedLanguages(); + + Languages::setCurrentLocale($current ?? $codes[0] ?? config('translatable.default_locale')); + } + + protected function createLanguagesTable(): void + { + if (Schema::hasTable('languages')) { + return; + } + + $migration = require __DIR__ . '/../../database/migrations/create_languages_table.php'; + + $migration->up(); + } + + /** + * Run the callback with translatable switched off, as if the package weren't installed + */ + protected function withoutTranslatable(callable $callback): mixed + { + $enabled = config('translatable.enabled'); + + config(['translatable.enabled' => false]); + + try { + return $callback(); + } finally { + config(['translatable.enabled' => $enabled]); + } + } +} diff --git a/src/Traits/IsTranslatable.php b/src/Traits/IsTranslatable.php index ac05318..eeae7a7 100644 --- a/src/Traits/IsTranslatable.php +++ b/src/Traits/IsTranslatable.php @@ -4,18 +4,49 @@ use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Support\Arr; +use Illuminate\Support\Facades\Schema; use Illuminate\Support\Str; use Javaabu\Translatable\Exceptions\FieldNotAllowedException; use Javaabu\Translatable\Exceptions\LanguageNotAllowedException; use Javaabu\Translatable\Facades\Languages; use Javaabu\Translatable\Facades\Translatable; use Javaabu\Translatable\Models\Language; -use Schema; trait IsTranslatable { private bool $skipTranslation = false; + /** + * Get the translatable fields + * + * Reads the model's `$translatable` property. Override to compute them. + * + * @return array + */ + public function getTranslatables(): array + { + return property_exists($this, 'translatable') ? $this->translatable : []; + } + + /** + * The locale the model's own columns are written in + * + * Existing rows without a `lang` (columns added to a table that already had data) + * are in the default locale; new models are in the current locale. + */ + public function primaryLocale(): string + { + $lang = $this->getAttributeValue('lang'); + + if ($lang) { + return $lang; + } + + return $this->exists + ? Languages::defaultLanguageCode() + : translation_locale(); + } + public function getNonTranslatablePivots(): array { return []; @@ -128,6 +159,71 @@ public function addTranslations(string $locale, array $fields): static return $this; } + /** + * Get the translatable fillable attributes, suffixed with every allowed locale + * + * ['title'] gives ['title_en', 'title_dv'] + * + * @return array + */ + public function getFillableTranslatables(): array + { + $translatable_fillables = $this->getFillable() + ? array_intersect($this->getTranslatables(), $this->getFillable()) + : $this->getTranslatables(); + + $language_codes = $this->getAllowedTranslationLocales(); + + $suffixed_fillables = []; + + foreach ($translatable_fillables as $fillable) { + foreach ($language_codes as $code) { + $suffixed_fillables[] = $fillable . '_' . $code; + } + } + + return $suffixed_fillables; + } + + protected function fillableTranslatablesFromArray(array $attributes): array + { + if ($this->totallyGuarded()) { + return []; + } + + return array_intersect_key($attributes, array_flip($this->getFillableTranslatables())); + } + + /** + * Fill the model, including suffixed translations (title_dv) of fillable translatable fields + * + * Translations are written when the model is saved. + * + * @return $this + * + * @throws FieldNotAllowedException + * @throws LanguageNotAllowedException + */ + public function fill(array $attributes): static + { + $fillable_translatables = $this->fillableTranslatablesFromArray($attributes); + + parent::fill(array_diff_key($attributes, $fillable_translatables)); + + foreach ($fillable_translatables as $key => $value) { + $this->setAttribute($key, $value); + } + + return $this; + } + + /** + * Stage a translation in memory; it is written when the model is saved + * + * @return $this + */ + abstract protected function stageTranslation(string $locale, string $field, mixed $value): static; + /** * Get the field and locale for a given attribute if possible * @@ -247,18 +343,20 @@ public function setAttribute($key, $value): mixed [$field, $locale] = $this->getFieldAndLocale($key); if ($locale && $this->isTranslatable($field)) { - return $this->addTranslation($locale, $field, $value); + return $this->stageTranslation($locale, $field, $value); } // set to translation via app locale - $lang = $this->getAttribute('lang'); - $locale = translation_locale(); - if (empty($lang)) { - parent::setAttribute('lang', $locale); - $lang = $locale; + $lang = $this->primaryLocale(); + + if (empty($this->getAttributeValue('lang'))) { + parent::setAttribute('lang', $lang); } - if ($locale !== $lang && $this->isTranslatable($field)) { - return $this->addTranslation($locale, $field, $value); + + $locale = translation_locale(); + + if ($locale !== $lang && $this->isTranslatable($key) && $this->isAllowedTranslationLocale($locale)) { + return $this->stageTranslation($locale, $key, $value); } return parent::setAttribute($key, $value); @@ -283,7 +381,7 @@ protected function setAttributeInternal($key, $value) public function localeDirection(): Attribute { return Attribute::get(function () { - return Languages::getDirection($this->lang); + return Languages::getDirection($this->primaryLocale()); }); } diff --git a/src/Traits/UrlGenerators/DbTranslatedUrlGenerator.php b/src/Traits/UrlGenerators/DbTranslatedUrlGenerator.php index 2a53802..3798b06 100644 --- a/src/Traits/UrlGenerators/DbTranslatedUrlGenerator.php +++ b/src/Traits/UrlGenerators/DbTranslatedUrlGenerator.php @@ -38,7 +38,7 @@ public function url(string $action = 'show', Language|string|null $locale = null } else { // If the portal is the default portal, and there is no translation available, redirect to default route if ($portal === config('translatable.default_portal')) { - return translate_route(config('translatable.default_redirect')); + return translate_route(config('translatable.default_redirect_route')); } // Switch to the create route if no translation exists diff --git a/src/Translatable.php b/src/Translatable.php index 9a02584..33e0e42 100644 --- a/src/Translatable.php +++ b/src/Translatable.php @@ -2,19 +2,277 @@ namespace Javaabu\Translatable; +use Closure; +use Illuminate\Database\Eloquent\Model; +use Illuminate\Support\Collection; +use Javaabu\Translatable\Contracts\DbTranslatable; +use Javaabu\Translatable\Contracts\Translatable as TranslatableContract; +use Javaabu\Translatable\DbTranslatable\IsDbTranslatable; use Javaabu\Translatable\Facades\Languages as LanguagesFacade; use Javaabu\Translatable\Models\Language; +/** + * The entry point for apps and for other packages that support translatable optionally + * + * Every static method is part of the contract other packages build on (see + * docs/advanced-usage/for-package-authors.md), so they keep their signatures within a major. + */ class Translatable { /** - * Return all the translation locales allowed in the config file + * Whether translations are on: the package is enabled in config and at least one language is active + * + * Other packages guard with class_exists(Translatable::class) && Translatable::enabled(). + */ + public static function enabled(): bool + { + if (! config('translatable.enabled', true)) { + return false; + } + + return LanguagesFacade::all()->isNotEmpty(); + } + + /** + * Whether the model (an instance or a class name) is translatable, and so is the field when given + */ + public static function supports(object|string|null $model, ?string $field = null): bool + { + if (! $model || ! is_a($model, TranslatableContract::class, true)) { + return false; + } + + if (! static::enabled()) { + return false; + } + + if ($field === null) { + return true; + } + + return static::model($model)->isTranslatable($field); + } + + /** + * Whether the model keeps each locale in its own row + */ + public static function isDbTranslatable(object|string|null $model): bool + { + if (! $model) { + return false; + } + + return is_a($model, DbTranslatable::class, true) + || in_array(IsDbTranslatable::class, class_uses_recursive($model), true); + } + + /** + * The active languages, the default one first + * + * @return Collection + */ + public static function locales(): Collection + { + $default = static::defaultLocale(); + + return LanguagesFacade::all() + ->map(fn (Language $language) => Locale::fromLanguage($language)) + ->sortBy(fn (Locale $locale) => $locale->code === $default ? 0 : 1) + ->values(); + } + + /** + * The given locale, or the current one + */ + public static function locale(?string $code = null): ?Locale + { + $code ??= static::currentLocale(); + + return static::locales()->first(fn (Locale $locale) => $locale->code === $code); + } + + /** + * The default translation locale (translatable.default_locale) + */ + public static function defaultLocale(): string + { + return LanguagesFacade::defaultLanguageCode(); + } + + /** + * The locale translatable attributes are read in during this request + */ + public static function currentLocale(): string + { + return LanguagesFacade::currentLanguageCode(); + } + + /** + * The locale a model's own columns are in: its `lang`, or the current locale for a new model + */ + public static function primaryLocale(object|string|null $model = null): string + { + if ($model instanceof TranslatableContract && method_exists($model, 'primaryLocale')) { + return $model->primaryLocale(); + } + + return static::currentLocale(); + } + + /** + * The input name of a field in a locale: title_dv + */ + public static function inputName(string $field, string $locale): string + { + return "{$field}_{$locale}"; + } + + /** + * One input per active locale for a translatable field, keyed by locale code + * + * @return Collection + */ + public static function inputs(string $field, object|string|null $model = null): Collection + { + $primary = static::primaryLocale($model); + + return static::orderedLocales($primary) + ->mapWithKeys(fn (Locale $locale) => [ + $locale->code => new LocaleInput( + field: $field, + name: static::inputName($field, $locale->code), + locale: $locale, + is_primary: $locale->code === $primary, + ), + ]); + } + + /** + * Expand validation rules for the model's translatable fields into one rule per locale + * + * ['title' => ['required', 'string']] becomes title_en with the rules + * as given for the primary locale, and title_dv with required + * swapped for nullable for the others. Other fields are returned unchanged. + * + * @param array $rules + * @return array + */ + public static function rules(object|string|null $model, array $rules, bool $require_all = false): array + { + $expanded = []; + + foreach ($rules as $field => $field_rules) { + if (! static::supports($model, $field)) { + $expanded[$field] = $field_rules; + + continue; + } + + foreach (static::inputs($field, $model) as $input) { + $expanded[$input->name] = ($input->is_primary || $require_all) + ? $field_rules + : static::optionalRules($field_rules); + } + } + + return $expanded; + } + + /** + * The values of the model's translatable fields in every locale, keyed by input name + * + * Use it to bind a form (or an Inertia page) to a model: ['title_en' => '…', 'title_dv' => '…']. + * Missing translations are null; nothing falls back to another locale. + * + * @param array|null $fields + * @return array + */ + public static function values(?Model $model, ?array $fields = null): array + { + if (! static::supports($model)) { + return []; + } + + $values = []; + + foreach ($fields ?? $model->getTranslatables() as $field) { + foreach (static::locales() as $locale) { + $values[static::inputName($field, $locale->code)] = $model->translate($field, $locale->code, false); + } + } + + return $values; + } + + /** + * Whether the model has each locale, for translation status indicators + * + * @param (Closure(Locale, bool): ?string)|null $url gives the link for a locale, given whether it's translated + * @return array + * }> + */ + public static function status(?Model $model, ?Closure $url = null): array + { + if (! static::supports($model)) { + return []; + } + + $primary = static::primaryLocale($model); + + return static::orderedLocales($primary) + ->map(function (Locale $locale) use ($model, $primary, $url) { + $translated = $model->exists && $model->hasTranslation($locale->code); + + return [ + 'locale' => $locale->code, + 'translated' => $translated, + 'primary' => $locale->code === $primary, + 'url' => $url ? $url($locale, $translated) : null, + 'language' => $locale->toArray(), + ]; + }) + ->all(); + } + + /** + * Data for the front end (Inertia shared props, or a script tag) + * + * @return array{ + * enabled: bool, + * current: string, + * default: string, + * direction: string, + * locales: array> + * } + */ + public static function shared(): array + { + $current = static::currentLocale(); + + return [ + 'enabled' => static::enabled(), + 'current' => $current, + 'default' => static::defaultLocale(), + 'direction' => static::locale($current)?->direction ?? 'ltr', + 'locales' => static::locales()->map->toArray()->all(), + ]; + } + + /** + * Return all the translation locales allowed + * + * @return array */ public function getAllowedTranslationLocales(): array { return LanguagesFacade::all()->map(function ($lang) { return $lang->code; - })->toArray(); + })->values()->toArray(); } /** @@ -28,4 +286,40 @@ public function isAllowedTranslationLocale(Language|string $locale): bool return in_array($locale, $this->getAllowedTranslationLocales()); } + + /** + * The locales with the primary one first + * + * @return Collection + */ + protected static function orderedLocales(string $primary): Collection + { + return static::locales() + ->sortBy(fn (Locale $locale) => $locale->code === $primary ? 0 : 1) + ->values(); + } + + protected static function model(object|string $model): TranslatableContract + { + return is_string($model) ? new $model : $model; + } + + protected static function optionalRules(mixed $rules): mixed + { + $is_string = is_string($rules); + + $rules = match (true) { + $is_string => explode('|', $rules), + is_array($rules) => $rules, + default => [$rules], + }; + + $rules = array_values(array_filter($rules, fn ($rule) => $rule !== 'required')); + + if (! in_array('nullable', $rules, true)) { + array_unshift($rules, 'nullable'); + } + + return $is_string ? implode('|', $rules) : $rules; + } } diff --git a/src/TranslatableServiceProvider.php b/src/TranslatableServiceProvider.php index a4e78dd..e454efb 100644 --- a/src/TranslatableServiceProvider.php +++ b/src/TranslatableServiceProvider.php @@ -12,6 +12,8 @@ use Illuminate\Support\Facades\Route; use Illuminate\Support\ServiceProvider; use Illuminate\Support\Str; +use Inertia\Inertia; +use Javaabu\Translatable\Commands\AddLanguagesCommand; use Javaabu\Translatable\DbTranslatable\DbTranslatableSchema; use Javaabu\Translatable\JsonTranslatable\JsonTranslatableSchema; use Javaabu\Translatable\Middleware\LocaleMiddleware; @@ -21,6 +23,7 @@ class TranslatableServiceProvider extends ServiceProvider { protected array $migrations = [ 'create_languages_table', + 'add_local_name_to_languages_table', ]; public function boot(): void @@ -31,26 +34,11 @@ public function boot(): void $this->loadViewsFrom(__DIR__ . '/../resources/views', 'translatable'); - Blade::componentNamespace('Javaabu\\Translatable\\Views\\Components', 'translatable'); - - $this->app->singleton(LanguageRegistrar::class, function ($app) { - $config = $this->app['config']['translatable']; - - $language_class = $config['language_model'] ?? Language::class; - $cache_manager = $this->app->make(CacheManager::class); + $this->loadTranslationsFrom(__DIR__ . '/../lang', 'translatable'); - $cache_expiration_time = Arr::get($config, 'cache.expiration_time'); - $cache_key = Arr::get($config, 'cache.key'); - $cache_driver = Arr::get($config, 'cache.driver'); + Blade::componentNamespace('Javaabu\\Translatable\\Views\\Components', 'translatable'); - return new LanguageRegistrar( - $language_class, - $cache_manager, - $cache_expiration_time, - $cache_key, - $cache_driver, - ); - }); + $this->registerInertiaSharedData(); // Register the translatable macros for URL generation Redirect::macro('translateRoute', function ($routeName, $parameters = [], $status = 302, $headers = []) { @@ -102,17 +90,56 @@ public function register(): void public function registerSingletons(): void { + $this->app->scoped(LanguageRegistrar::class, function ($app) { + $config = $this->app['config']['translatable']; + + $language_class = $config['language_model'] ?? Language::class; + $cache_manager = $this->app->make(CacheManager::class); + + $cache_expiration_time = Arr::get($config, 'cache.expiration_time'); + $cache_key = Arr::get($config, 'cache.key'); + $cache_driver = Arr::get($config, 'cache.driver'); + + return new LanguageRegistrar( + $language_class, + $cache_manager, + $cache_expiration_time, + $cache_key, + $cache_driver, + ); + }); + $this->app->singleton(Translatable::class, function () { return new Translatable; }); $this->app->alias(Translatable::class, 'translatable'); - $this->app->singleton(Languages::class, function () { + $this->app->scoped(Languages::class, function () { return new Languages; }); $this->app->alias(Languages::class, 'languages'); } + /** + * Share the languages with Inertia pages when the views are Inertia React + */ + public function registerInertiaSharedData(): void + { + if (config('translatable.views') !== 'inertia') { + return; + } + + if (! config('translatable.inertia.share', true)) { + return; + } + + if (! class_exists(Inertia::class)) { + return; + } + + Inertia::share(config('translatable.inertia.prop', 'translatable'), fn () => Translatable::shared()); + } + /** * Database Migration Macros that can be used instead * of calling the static functions directly. @@ -144,6 +171,10 @@ public function offerPublishing(): void { // declare publishes if ($this->app->runningInConsole()) { + $this->commands([ + AddLanguagesCommand::class, + ]); + // Publish config $this->publishes([ __DIR__ . '/../config/translatable.php' => config_path('translatable.php'), @@ -160,9 +191,9 @@ public function offerPublishing(): void ], 'translatable-flags'); // Publish migrations - foreach ($this->migrations as $migration) { + foreach ($this->migrations as $index => $migration) { $vendorMigration = __DIR__ . '/../database/migrations/' . $migration . '.php'; - $appMigration = $this->generateMigrationName($migration, now()->addSecond()); + $appMigration = $this->generateMigrationName($migration, now()->addSeconds($index + 1)); $this->publishes([ $vendorMigration => $appMigration, diff --git a/tests/Feature/StagedTranslationsTest.php b/tests/Feature/StagedTranslationsTest.php new file mode 100644 index 0000000..84865f5 --- /dev/null +++ b/tests/Feature/StagedTranslationsTest.php @@ -0,0 +1,184 @@ +setUpLanguages(['en', 'dv']); + } + + #[Test] + public function it_creates_a_json_model_from_per_locale_inputs(): void + { + $page = Page::query()->create([ + 'title_dv' => 'މަރުޙަބާ', + 'title_en' => 'Welcome', + 'body_en' => 'Hello', + 'slug' => 'welcome', + ]); + + $page = $page->fresh(); + + $this->assertEquals('en', $page->lang); + $this->assertEquals('Welcome', $page->title_en); + $this->assertEquals('މަރުޙަބާ', $page->title_dv); + $this->assertEquals('Hello', $page->body); + $this->assertNull($page->body_dv); + } + + #[Test] + public function it_fills_a_new_json_model_from_a_translation_when_the_primary_value_is_missing(): void + { + $page = new Page(['title_dv' => 'މަރުޙަބާ', 'slug' => 'welcome']); + + $page->save(); + + $this->assertDatabaseHas('pages', ['id' => $page->id, 'title' => 'މަރުޙަބާ', 'lang' => 'en']); + $this->assertEquals('މަރުޙަބާ', $page->fresh()->title_dv); + } + + #[Test] + public function it_does_not_save_json_translations_set_through_attributes_until_the_model_is_saved(): void + { + $page = Page::query()->create(['title' => 'Welcome', 'lang' => 'en']); + + $page->title_dv = 'މަރުޙަބާ'; + + $this->assertEquals('މަރުޙަބާ', $page->title_dv); + $this->assertNull($page->fresh()->title_dv); + + $page->save(); + + $this->assertEquals('މަރުޙަބާ', $page->fresh()->title_dv); + } + + #[Test] + public function it_casts_the_translations_column_when_the_model_does_not(): void + { + $page = Page::query()->create(['title' => 'Welcome', 'lang' => 'en', 'title_dv' => 'މަރުޙަބާ']); + + $this->assertEquals(['dv' => ['title' => 'މަރުޙަބާ', 'lang' => 'dv']], $page->fresh()->translations); + } + + #[Test] + public function it_creates_a_db_model_and_its_translation_rows_from_per_locale_inputs(): void + { + $author = Author::factory()->create(); + + $post = Post::query()->create([ + 'title_en' => 'Welcome', + 'title_dv' => 'މަރުޙަބާ', + 'body_en' => 'Hello', + 'body_dv' => 'ހެލޯ', + 'slug' => 'welcome', + 'author_id' => $author->id, + ]); + + $this->assertEquals('en', $post->lang); + $this->assertDatabaseCount('posts', 2); + $this->assertDatabaseHas('posts', ['id' => $post->id, 'title' => 'Welcome', 'translatable_parent_id' => null]); + $this->assertDatabaseHas('posts', ['title' => 'މަރުޙަބާ', 'body' => 'ހެލޯ', 'lang' => 'dv', 'slug' => 'welcome', 'translatable_parent_id' => $post->id]); + $this->assertEquals('މަރުޙަބާ', $post->fresh()->title_dv); + $this->assertEquals([], $post->getPendingTranslations()); + } + + #[Test] + public function it_writes_db_translations_set_through_attributes_when_the_model_is_saved(): void + { + $post = Post::factory()->withAuthor()->create(['lang' => 'en', 'title' => 'Welcome']); + + $post->title_dv = 'މަރުޙަބާ'; + $post->body_dv = 'ހެލޯ'; + + $this->assertEquals('މަރުޙަބާ', $post->title_dv); + $this->assertTrue($post->hasTranslation('dv')); + $this->assertDatabaseCount('posts', 1); + + DB::enableQueryLog(); + $post->save(); + $inserts = collect(DB::getQueryLog())->filter(fn ($query) => str_starts_with($query['query'], 'insert')); + + $this->assertCount(1, $inserts); + $this->assertDatabaseHas('posts', ['title' => 'މަރުޙަބާ', 'body' => 'ހެލޯ', 'translatable_parent_id' => $post->id]); + } + + #[Test] + public function it_updates_the_parent_row_from_a_translation_row(): void + { + $post = Post::factory()->withAuthor()->create(['lang' => 'en', 'title' => 'Welcome']); + $translation = $post->addTranslation('dv', 'title', 'މަރުޙަބާ'); + + $translation->fill(['title_en' => 'Welcome back', 'title_dv' => 'އަލުން މަރުޙަބާ'])->save(); + + $this->assertEquals('Welcome back', $post->fresh()->title); + $this->assertEquals('އަލުން މަރުޙަބާ', $translation->fresh()->title_dv); + $this->assertDatabaseHas('posts', ['id' => $translation->id, 'title' => 'އަލުން މަރުޙަބާ']); + } + + #[Test] + public function it_treats_existing_rows_without_a_lang_as_the_default_locale(): void + { + DB::table('pages')->insert(['title' => 'Welcome', 'translations' => null, 'lang' => null]); + + Languages::setCurrentLocale('dv'); + + $page = Page::query()->first(); + + $this->assertEquals('en', $page->primaryLocale()); + $this->assertEquals('en', $page->getDefaultTranslationLocale()); + $this->assertEquals('ltr', $page->locale_direction); + + // Editing in Dhivehi adds a translation instead of overwriting the English columns + $page->update(['title' => 'މަރުޙަބާ']); + + $this->assertDatabaseHas('pages', ['id' => $page->id, 'title' => 'Welcome', 'lang' => 'en']); + $this->assertEquals('މަރުޙަބާ', $page->fresh()->title_dv); + } + + #[Test] + public function it_writes_the_columns_when_the_current_locale_is_not_an_allowed_language(): void + { + $page = Page::query()->create(['title' => 'Welcome', 'lang' => 'en']); + + Languages::setCurrentLocale('fr'); + + $page->update(['title' => 'Bienvenue']); + + $this->assertDatabaseHas('pages', ['id' => $page->id, 'title' => 'Bienvenue']); + } + + #[Test] + public function it_validates_and_saves_a_form_with_the_expanded_rules(): void + { + $rules = Translatable::rules(Page::class, ['title' => ['required', 'string'], 'slug' => ['nullable']]); + + $validator = validator(['title_dv' => 'މަރުޙަބާ'], $rules); + + $this->assertTrue($validator->fails()); + $this->assertEquals(['title_en'], $validator->errors()->keys()); + + $validated = validator(['title_en' => 'Welcome', 'title_dv' => 'މަރުޙަބާ', 'slug' => 'welcome'], $rules)->validate(); + + $page = Page::query()->create($validated); + + $this->assertEquals(['title_en' => 'Welcome', 'title_dv' => 'މަރުޙަބާ'], Translatable::values($page->fresh(), ['title'])); + } +} diff --git a/tests/TestSupport/Models/Page.php b/tests/TestSupport/Models/Page.php new file mode 100644 index 0000000..0a27a76 --- /dev/null +++ b/tests/TestSupport/Models/Page.php @@ -0,0 +1,27 @@ +id(); + $table->string('title'); + $table->text('body')->nullable(); + $table->string('slug')->nullable(); + $table->json('translations')->nullable(); + $table->string('lang')->nullable()->index(); + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists('pages'); + } +}; diff --git a/tests/Unit/AddLanguagesCommandTest.php b/tests/Unit/AddLanguagesCommandTest.php new file mode 100644 index 0000000..c2f08e1 --- /dev/null +++ b/tests/Unit/AddLanguagesCommandTest.php @@ -0,0 +1,49 @@ +artisan('translatable:add-languages') + ->expectsOutputToContain('Added 2 language(s).') + ->assertSuccessful(); + + $this->assertDatabaseHas('languages', ['code' => 'en', 'name' => 'English', 'is_rtl' => false, 'active' => true]); + $this->assertDatabaseHas('languages', ['code' => 'dv', 'local_name' => 'ދިވެހި', 'flag' => 'mv', 'is_rtl' => true]); + } + + #[Test] + public function it_skips_languages_that_exist_and_warns_about_unknown_codes(): void + { + Language::query()->create(LanguagePresets::get('en')); + + $this->artisan('translatable:add-languages', ['codes' => ['en', 'ar', 'xx'], '--inactive' => true]) + ->expectsOutputToContain('`en` is already in the languages table.') + ->expectsOutputToContain("There's no preset for `xx`") + ->expectsOutputToContain('Added 2 language(s).') + ->assertSuccessful(); + + $this->assertDatabaseHas('languages', ['code' => 'ar', 'is_rtl' => true, 'active' => false]); + $this->assertDatabaseHas('languages', ['code' => 'xx', 'name' => 'XX', 'active' => false]); + $this->assertDatabaseCount('languages', 3); + } + + #[Test] + public function it_knows_its_presets(): void + { + $this->assertTrue(LanguagePresets::has('dv')); + $this->assertFalse(LanguagePresets::has('xx')); + $this->assertContains('en', LanguagePresets::codes()); + } +} diff --git a/tests/Unit/Contract/TranslatableContractTest.php b/tests/Unit/Contract/TranslatableContractTest.php new file mode 100644 index 0000000..8d035d5 --- /dev/null +++ b/tests/Unit/Contract/TranslatableContractTest.php @@ -0,0 +1,237 @@ +assertFalse(Translatable::enabled()); + + $this->setUpLanguages(['en']); + + $this->assertTrue(Translatable::enabled()); + } + + #[Test] + public function it_can_be_disabled_in_config(): void + { + $this->setUpLanguages(); + + $this->withoutTranslatable(function () { + $this->assertFalse(Translatable::enabled()); + $this->assertFalse(Translatable::supports(new Page)); + $this->assertEquals(['title' => ['required']], Translatable::rules(new Page, ['title' => ['required']])); + }); + + $this->assertTrue(Translatable::enabled()); + } + + #[Test] + public function it_checks_whether_a_model_and_field_are_translatable(): void + { + $this->setUpLanguages(); + + $this->assertTrue(Translatable::supports(new Page)); + $this->assertTrue(Translatable::supports(Page::class)); + $this->assertTrue(Translatable::supports(Page::class, 'title')); + $this->assertTrue(Translatable::supports(new Post, 'body')); + $this->assertFalse(Translatable::supports(Page::class, 'slug')); + $this->assertFalse(Translatable::supports(new Author)); + $this->assertFalse(Translatable::supports(Author::class, 'name')); + $this->assertFalse(Translatable::supports(null)); + } + + #[Test] + public function it_tells_db_translatable_models_apart(): void + { + $this->assertTrue(Translatable::isDbTranslatable(Post::class)); + $this->assertFalse(Translatable::isDbTranslatable(new Article)); + $this->assertFalse(Translatable::isDbTranslatable(null)); + } + + #[Test] + public function it_lists_the_active_locales_with_the_default_first(): void + { + $this->setUpLanguages(['dv', 'en']); + + $locales = Translatable::locales(); + + $this->assertEquals(['en', 'dv'], $locales->pluck('code')->all()); + + $dhivehi = $locales->last(); + + $this->assertInstanceOf(Locale::class, $dhivehi); + $this->assertEquals('ދިވެހި', $dhivehi->native_name); + $this->assertEquals('rtl', $dhivehi->direction); + $this->assertTrue($dhivehi->isRtl()); + $this->assertTrue($dhivehi->isThaana()); + $this->assertFalse($dhivehi->is_default); + $this->assertTrue($locales->first()->is_default); + + $this->assertEquals([ + 'code' => 'dv', + 'locale' => 'dv', + 'name' => 'Dhivehi', + 'native_name' => 'ދިވެހި', + 'direction' => 'rtl', + 'rtl' => true, + 'thaana' => true, + 'default' => false, + 'flag_url' => asset('vendors/flags/mv.svg'), + ], $dhivehi->toArray()); + + $this->assertEquals('dv', Translatable::locale('dv')->code); + $this->assertEquals('dv', Translatable::locale()->code); + $this->assertNull(Translatable::locale('fr')); + } + + #[Test] + public function it_gives_one_input_per_locale_with_the_primary_first(): void + { + $this->setUpLanguages(['en', 'dv']); + + $page = Page::query()->create(['title' => 'ފުރަތަމަ ސަފްހާ', 'lang' => 'dv']); + + $inputs = Translatable::inputs('title', $page); + + $this->assertEquals(['dv', 'en'], $inputs->keys()->all()); + $this->assertEquals('title_dv', $inputs['dv']->name); + $this->assertTrue($inputs['dv']->is_primary); + $this->assertFalse($inputs['en']->is_primary); + $this->assertEquals('ސުރުޚީ (ދިވެހި)', $inputs['dv']->label('ސުރުޚީ')); + $this->assertEquals('Title (English)', $inputs['en']->label()); + $this->assertEquals([ + 'field' => 'title', + 'name' => 'title_dv', + 'locale' => 'dv', + 'direction' => 'rtl', + 'thaana' => true, + 'primary' => true, + 'language' => Translatable::locale('dv')->toArray(), + ], $inputs['dv']->toArray()); + + // A new model is written in the current locale + $this->assertTrue(Translatable::inputs('title', Page::class)['en']->is_primary); + $this->assertEquals('title_en', Translatable::inputName('title', 'en')); + } + + #[Test] + public function it_expands_rules_for_translatable_fields(): void + { + $this->setUpLanguages(['en', 'dv']); + + $unique = Rule::unique('pages', 'title'); + + $rules = Translatable::rules(Page::class, [ + 'title' => ['required', 'string', 'max:255'], + 'body' => 'required|string', + 'slug' => ['required', 'alpha_dash'], + ]); + + $this->assertEquals([ + 'title_en' => ['required', 'string', 'max:255'], + 'title_dv' => ['nullable', 'string', 'max:255'], + 'body_en' => 'required|string', + 'body_dv' => 'nullable|string', + 'slug' => ['required', 'alpha_dash'], + ], $rules); + + $this->assertEquals( + ['title_en' => ['required'], 'title_dv' => ['required']], + Translatable::rules(Page::class, ['title' => ['required']], require_all: true), + ); + + $this->assertEquals( + ['title_en' => $unique, 'title_dv' => ['nullable', $unique]], + Translatable::rules(Page::class, ['title' => $unique]), + ); + + $this->assertEquals(['name' => ['required']], Translatable::rules(Author::class, ['name' => ['required']])); + } + + #[Test] + public function it_gives_the_values_in_every_locale_without_falling_back(): void + { + $this->setUpLanguages(['en', 'dv']); + + $page = Page::query()->create(['title' => 'Welcome', 'body' => 'Hello', 'lang' => 'en']); + $page->addTranslation('dv', 'title', 'މަރުޙަބާ'); + + $this->assertEquals([ + 'title_en' => 'Welcome', + 'title_dv' => 'މަރުޙަބާ', + 'body_en' => 'Hello', + 'body_dv' => null, + ], Translatable::values($page)); + + $this->assertEquals(['title_en' => 'Welcome', 'title_dv' => 'މަރުޙަބާ'], Translatable::values($page, ['title'])); + $this->assertEquals([], Translatable::values(Author::factory()->create())); + } + + #[Test] + public function it_reports_the_translation_status_of_a_model(): void + { + $this->setUpLanguages(['en', 'dv', 'ar']); + + $page = Page::query()->create(['title' => 'Welcome', 'lang' => 'en']); + $page->addTranslation('dv', 'title', 'މަރުޙަބާ'); + + $status = Translatable::status($page, fn (Locale $locale, bool $translated) => ($translated ? 'edit/' : 'add/') . $locale->code); + + $this->assertEquals(['en', 'dv', 'ar'], array_column($status, 'locale')); + $this->assertEquals([true, true, false], array_column($status, 'translated')); + $this->assertEquals([true, false, false], array_column($status, 'primary')); + $this->assertEquals(['edit/en', 'edit/dv', 'add/ar'], array_column($status, 'url')); + $this->assertNull(Translatable::status($page)[0]['url']); + $this->assertEquals([false, false, false], array_column(Translatable::status(new Page), 'translated')); + } + + #[Test] + public function it_shares_the_languages_for_the_front_end(): void + { + $this->setUpLanguages(['en', 'dv'], current: 'dv'); + + $shared = Translatable::shared(); + + $this->assertTrue($shared['enabled']); + $this->assertEquals('dv', $shared['current']); + $this->assertEquals('en', $shared['default']); + $this->assertEquals('rtl', $shared['direction']); + $this->assertEquals(['en', 'dv'], array_column($shared['locales'], 'code')); + } + + #[Test] + public function it_answers_through_the_facade_too(): void + { + $this->setUpLanguages(['en', 'dv']); + + $this->assertTrue(TranslatableFacade::enabled()); + $this->assertEquals(['en', 'dv'], TranslatableFacade::getAllowedTranslationLocales()); + $this->assertEquals('en', TranslatableFacade::defaultLocale()); + $this->assertEquals('en', TranslatableFacade::currentLocale()); + + Languages::setCurrentLocale('dv'); + + $this->assertEquals('dv', Translatable::currentLocale()); + $this->assertEquals('dv', Translatable::primaryLocale()); + } +} diff --git a/tests/Unit/LanguageRegistrarTest.php b/tests/Unit/LanguageRegistrarTest.php new file mode 100644 index 0000000..7ac1ecf --- /dev/null +++ b/tests/Unit/LanguageRegistrarTest.php @@ -0,0 +1,83 @@ +create(LanguagePresets::get('en')); + + app()->forgetScopedInstances(); + + DB::enableQueryLog(); + + Languages::all(); + Languages::all(); + Languages::get('en'); + + $this->assertCount(1, DB::getQueryLog()); + + // The next request reads them from the cache + app()->forgetScopedInstances(); + DB::flushQueryLog(); + Languages::all(); + $this->assertCount(0, DB::getQueryLog()); + $this->assertEquals(['en'], Languages::all()->pluck('code')->all()); + } + + #[Test] + public function it_does_not_cache_an_empty_list(): void + { + $this->assertTrue(Languages::all()->isEmpty()); + $this->assertNull(Cache::get('translation.languages.cache')); + + DB::table('languages')->insert(LanguagePresets::get('dv')); + app()->forgetScopedInstances(); + + $this->assertEquals(['dv'], Languages::all()->pluck('code')->all()); + } + + #[Test] + public function it_forgets_the_languages_when_one_changes(): void + { + $english = Language::query()->create(LanguagePresets::get('en')); + Language::query()->create(LanguagePresets::get('dv')); + + $this->assertCount(2, Languages::all()); + + $english->update(['active' => false]); + + $this->assertEquals(['dv'], Languages::all()->pluck('code')->values()->all()); + + $english->delete(); + $english->restore(); + $english->update(['active' => true]); + + $this->assertCount(2, Languages::all()); + } + + #[Test] + public function it_returns_no_languages_when_the_table_is_missing(): void + { + DB::statement('drop table languages'); + + $registrar = app(LanguageRegistrar::class); + $registrar->forgetCachedLanguages(); + + $this->assertTrue($registrar->getLanguages()->isEmpty()); + } +} diff --git a/tests/Unit/migrations/AddLocalNameToLanguagesTableTest.php b/tests/Unit/migrations/AddLocalNameToLanguagesTableTest.php new file mode 100644 index 0000000..9c2d639 --- /dev/null +++ b/tests/Unit/migrations/AddLocalNameToLanguagesTableTest.php @@ -0,0 +1,46 @@ +dropColumn('local_name'); + }); + + $migration = require __DIR__ . '/../../../database/migrations/add_local_name_to_languages_table.php'; + + $migration->up(); + + $this->assertTrue(Schema::hasColumn('languages', 'local_name')); + + // Running it again (or on a new install) changes nothing + $migration->up(); + $migration->down(); + + $this->assertTrue(Schema::hasColumn('languages', 'local_name')); + } + + #[Test] + public function it_does_nothing_before_the_languages_table_exists(): void + { + Schema::drop('languages'); + + $migration = require __DIR__ . '/../../../database/migrations/add_local_name_to_languages_table.php'; + + $migration->up(); + + $this->assertFalse(Schema::hasTable('languages')); + } +} From f5a767cd9db5c604ac4a272b4a258df1915b4c66 Mon Sep 17 00:00:00 2001 From: Naffah Abdulla Rasheed Date: Wed, 4 Nov 2026 14:31:15 +0500 Subject: [PATCH 03/10] Add the Blade components, built on javaabu/forms v2 Per-locale fields (text, textarea, field: tabs or stacked, bound values, errors open their tab, dir/lang and Thaana), the language switcher, translation status badges, the add-translation notice for DB models, and the table titles/cells rebuilt for Onu and Bootstrap 6. Tests render every component in both frameworks. Refs #3 Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 3 + composer.json | 18 +- lang/dv/strings.php | 8 + lang/en/strings.php | 8 + resources/views/common/field.blade.php | 49 +++ .../views/common/language-switcher.blade.php | 8 + resources/views/common/table-cells.blade.php | 16 + resources/views/common/table-titles.blade.php | 3 + .../views/common/translation-notice.blade.php | 5 + .../views/common/translation-status.blade.php | 23 ++ resources/views/table-cells.blade.php | 17 -- resources/views/table-titles.blade.php | 5 - src/Translatable.php | 52 ++++ src/Views/Components/Component.php | 46 +++ src/Views/Components/Field.php | 146 +++++++++ src/Views/Components/LanguageSwitcher.php | 56 ++++ src/Views/Components/TableCells.php | 60 +++- src/Views/Components/Text.php | 18 ++ src/Views/Components/Textarea.php | 18 ++ src/Views/Components/Titles.php | 22 +- src/Views/Components/TranslationNotice.php | 54 ++++ src/Views/Components/TranslationStatus.php | 58 ++++ tests/Feature/Views/BladeComponentsTest.php | 285 ++++++++++++++++++ tests/TestCase.php | 7 + 24 files changed, 937 insertions(+), 48 deletions(-) create mode 100644 resources/views/common/field.blade.php create mode 100644 resources/views/common/language-switcher.blade.php create mode 100644 resources/views/common/table-cells.blade.php create mode 100644 resources/views/common/table-titles.blade.php create mode 100644 resources/views/common/translation-notice.blade.php create mode 100644 resources/views/common/translation-status.blade.php delete mode 100644 resources/views/table-cells.blade.php delete mode 100644 resources/views/table-titles.blade.php create mode 100644 src/Views/Components/Component.php create mode 100644 src/Views/Components/Field.php create mode 100644 src/Views/Components/LanguageSwitcher.php create mode 100644 src/Views/Components/Text.php create mode 100644 src/Views/Components/Textarea.php create mode 100644 src/Views/Components/TranslationNotice.php create mode 100644 src/Views/Components/TranslationStatus.php create mode 100644 tests/Feature/Views/BladeComponentsTest.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 0408864..8f6a09e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,3 +19,6 @@ All notable changes to `javaabu/translatable` are documented in this file. - `languages.local_name` column (the language's own name); `add_local_name_to_languages_table` migration for v2 apps - New config: `enabled`, `thaana_locales`, `views`, `inertia` - Fixed: `$model->url()` read `translatable.default_redirect` instead of `default_redirect_route` +- **Blade components on javaabu/forms v2** (Onu and Bootstrap 6): ``, `textarea` and `field` (an input per locale, in tabs or stacked, with direction, language and the Thaana keyboard), `language-switcher`, `translation-status`, `translation-notice` (adding a DB translation), and the rebuilt `titles` / `table-cells` +- `Translatable::translationUrl()` and `Translatable::switchUrl()` +- The `styles` config is gone: the components follow javaabu/forms' framework diff --git a/composer.json b/composer.json index a868de4..45f33df 100644 --- a/composer.json +++ b/composer.json @@ -38,9 +38,13 @@ }, "require-dev": { "javaabu/boost-javaabu-guidelines": "^1.4", + "javaabu/forms": "^2.0@dev", + "javaabu/manifest": "^1.0@dev", "laravel/pint": "^1.25", "orchestra/testbench": "^10.0 || ^11.0", - "phpunit/phpunit": "^11.5.3 || ^12.5.8" + "phpunit/phpunit": "^11.5.3 || ^12.5.8", + "symfony/css-selector": "^7.2 || ^8.0", + "symfony/dom-crawler": "^7.2 || ^8.0" }, "suggest": { "javaabu/forms": "Required for the Blade components (^2.0)", @@ -79,5 +83,15 @@ "sort-packages": true }, "minimum-stability": "dev", - "prefer-stable": true + "prefer-stable": true, + "repositories": [ + { + "type": "vcs", + "url": "https://github.com/Javaabu/forms" + }, + { + "type": "vcs", + "url": "https://github.com/Javaabu/manifest" + } + ] } diff --git a/lang/dv/strings.php b/lang/dv/strings.php index 5556c30..280a873 100644 --- a/lang/dv/strings.php +++ b/lang/dv/strings.php @@ -2,4 +2,12 @@ return [ 'field_in_locale' => ':label (:locale)', + 'language' => 'ބަސް', + 'translation_status' => 'ތަރުޖަމާތައް', + 'original_in' => 'އަސްލު: :locale', + 'translated_in' => ':locale އަށް ތަރުޖަމާކުރެވިފައި', + 'not_translated_in' => ':locale އަށް ތަރުޖަމާކުރެވިފައެއް ނުވޭ', + 'add_translation_in' => ':locale ތަރުޖަމާ އިތުރުކުރުން', + 'edit_translation_in' => ':locale ތަރުޖަމާ ބަދަލުކުރުން', + 'adding_translation' => '“:title” ގެ :locale ތަރުޖަމާ އިތުރުކުރަނީ.', ]; diff --git a/lang/en/strings.php b/lang/en/strings.php index 5556c30..30fc31e 100644 --- a/lang/en/strings.php +++ b/lang/en/strings.php @@ -2,4 +2,12 @@ return [ 'field_in_locale' => ':label (:locale)', + 'language' => 'Language', + 'translation_status' => 'Translations', + 'original_in' => 'Original: :locale', + 'translated_in' => 'Translated into :locale', + 'not_translated_in' => 'Not translated into :locale', + 'add_translation_in' => 'Add :locale translation', + 'edit_translation_in' => 'Edit :locale translation', + 'adding_translation' => 'You are adding the :locale translation of “:title”.', ]; diff --git a/resources/views/common/field.blade.php b/resources/views/common/field.blade.php new file mode 100644 index 0000000..c4d9367 --- /dev/null +++ b/resources/views/common/field.blade.php @@ -0,0 +1,49 @@ +@if($isTranslatable()) + @if($layout === 'stacked') +
only(['class', 'id'])->merge(['class' => 'translatable-field']) }} data-translatable-field="{{ $name }}"> + @foreach($inputs as $input) + + @endforeach +
+ @else + + @foreach($inputs as $input) + + + + @endforeach + + @endif +@else + {{-- Not translatable (or translations are off): a plain field --}} + +@endif diff --git a/resources/views/common/language-switcher.blade.php b/resources/views/common/language-switcher.blade.php new file mode 100644 index 0000000..6e0521e --- /dev/null +++ b/resources/views/common/language-switcher.blade.php @@ -0,0 +1,8 @@ +@php($items = $items()) +@if(count($items) > 1) + +@endif diff --git a/resources/views/common/table-cells.blade.php b/resources/views/common/table-cells.blade.php new file mode 100644 index 0000000..f65cdc2 --- /dev/null +++ b/resources/views/common/table-cells.blade.php @@ -0,0 +1,16 @@ +@foreach($languages as $language) + +
+ @php($url = $translationUrl($language)) + @if($url) + + + + @else + + + + @endif +
+
+@endforeach diff --git a/resources/views/common/table-titles.blade.php b/resources/views/common/table-titles.blade.php new file mode 100644 index 0000000..d7b4fcc --- /dev/null +++ b/resources/views/common/table-titles.blade.php @@ -0,0 +1,3 @@ +@foreach($languages as $language) + +@endforeach diff --git a/resources/views/common/translation-notice.blade.php b/resources/views/common/translation-notice.blade.php new file mode 100644 index 0000000..9bf5ecc --- /dev/null +++ b/resources/views/common/translation-notice.blade.php @@ -0,0 +1,5 @@ + + {{ $message() }} + + + diff --git a/resources/views/common/translation-status.blade.php b/resources/views/common/translation-status.blade.php new file mode 100644 index 0000000..a513f9e --- /dev/null +++ b/resources/views/common/translation-status.blade.php @@ -0,0 +1,23 @@ +@php($items = $items()) +@if($items) + +@endif diff --git a/resources/views/table-cells.blade.php b/resources/views/table-cells.blade.php deleted file mode 100644 index be1ce9b..0000000 --- a/resources/views/table-cells.blade.php +++ /dev/null @@ -1,17 +0,0 @@ -@foreach($languages as $language) - -
- @if($model->hasTranslation($language->code)) - - - @else - - - @endif -
-
-@endforeach diff --git a/resources/views/table-titles.blade.php b/resources/views/table-titles.blade.php deleted file mode 100644 index b32e322..0000000 --- a/resources/views/table-titles.blade.php +++ /dev/null @@ -1,5 +0,0 @@ -@foreach($languages as $language) - - {{ $language->code }} - -@endforeach diff --git a/src/Translatable.php b/src/Translatable.php index 33e0e42..badc28f 100644 --- a/src/Translatable.php +++ b/src/Translatable.php @@ -4,12 +4,15 @@ use Closure; use Illuminate\Database\Eloquent\Model; +use Illuminate\Routing\Route; +use Illuminate\Support\Arr; use Illuminate\Support\Collection; use Javaabu\Translatable\Contracts\DbTranslatable; use Javaabu\Translatable\Contracts\Translatable as TranslatableContract; use Javaabu\Translatable\DbTranslatable\IsDbTranslatable; use Javaabu\Translatable\Facades\Languages as LanguagesFacade; use Javaabu\Translatable\Models\Language; +use Throwable; /** * The entry point for apps and for other packages that support translatable optionally @@ -239,6 +242,55 @@ public static function status(?Model $model, ?Closure $url = null): array ->all(); } + /** + * The link to add or edit the model's translation in a locale, through the model's url() (v2's URL generators) + * + * Translated: the edit page in that locale. Not translated: the create page with + * `lang_parent` for DB translatable models, the edit page for JSON ones. Null when the + * model has no url() or the route doesn't exist. + */ + public static function translationUrl(?Model $model, string $locale, ?bool $translated = null): ?string + { + if (! $model || ! method_exists($model, 'url')) { + return null; + } + + $translated ??= $model->exists && $model->hasTranslation($locale); + + $action = (! $translated && static::isDbTranslatable($model)) ? 'create' : 'edit'; + + try { + $url = $model->url($action, $locale); + } catch (Throwable) { + return null; + } + + if ($action === 'create' && ! str_contains($url, 'lang_parent=')) { + $url .= (str_contains($url, '?') ? '&' : '?') . http_build_query(['lang_parent' => $model->getKey()]); + } + + return $url; + } + + /** + * The current page in another locale: the current route with its {language} parameter + * swapped, or the current URL with ?lang= (which the `language` middleware reads) + */ + public static function switchUrl(string $locale): string + { + $request = request(); + $route = $request->route(); + + if ($route instanceof Route && $route->getName() && $route->hasParameter('language')) { + $url = route($route->getName(), [...$route->parameters(), 'language' => $locale]); + $query = Arr::except($request->query(), ['lang']); + + return $query ? $url . '?' . Arr::query($query) : $url; + } + + return $request->fullUrlWithQuery(['lang' => $locale]); + } + /** * Data for the front end (Inertia shared props, or a script tag) * diff --git a/src/Views/Components/Component.php b/src/Views/Components/Component.php new file mode 100644 index 0000000..62d7e23 --- /dev/null +++ b/src/Views/Components/Component.php @@ -0,0 +1,46 @@ +view; + } + + /** + * Layout utilities for the few wrappers javaabu/forms has no component for + */ + public function layoutClasses(string $key): string + { + $bootstrap = str_starts_with($this->framework, 'bootstrap'); + + return match ($key) { + 'inline-list' => $bootstrap ? 'd-flex flex-wrap align-items-center gap-1' : 'flex flex-wrap items-center gap-1', + 'center' => $bootstrap ? 'd-flex justify-content-center align-items-center gap-1' : 'flex items-center justify-center gap-1', + default => '', + }; + } + + /** + * The model given to the component, or the one bound by the surrounding + */ + protected function resolveModel(mixed $model): mixed + { + if ($model) { + return $model; + } + + return app(FormsDataBinder::class)->get(); + } +} diff --git a/src/Views/Components/Field.php b/src/Views/Components/Field.php new file mode 100644 index 0000000..530b5fe --- /dev/null +++ b/src/Views/Components/Field.php @@ -0,0 +1,146 @@ + + */ +class Field extends Component +{ + protected string $view = 'field'; + + /** @var Collection */ + public Collection $inputs; + + public string $active; + + public mixed $model; + + public function __construct( + public string $name, + public string $type = 'text', + public string $label = '', + mixed $model = null, + public bool $required = false, + public bool $requireAll = false, + public string $layout = 'tabs', + string $framework = '', + ) { + parent::__construct($framework); + + $this->model = $this->resolveModel($model); + + $this->inputs = $this->shouldTranslate() + ? Translatable::inputs($name, $this->model) + : collect(); + + $this->active = $this->firstLocaleWithErrors() ?? $this->inputs->keys()->first() ?? ''; + } + + /** + * Translatable when the bound model supports the field, or, with no model (a create form), + * whenever translations are on + */ + protected function shouldTranslate(): bool + { + if (is_object($this->model)) { + return Translatable::supports($this->model, $this->name); + } + + return Translatable::enabled(); + } + + public function activeTab(): string + { + return $this->stringToId($this->name) . '-' . $this->active; + } + + public function isTranslatable(): bool + { + return $this->inputs->isNotEmpty(); + } + + public function inputComponent(): string + { + return Str::startsWith($this->type, 'forms::') ? $this->type : "forms::{$this->type}"; + } + + public function inputLabel(LocaleInput $input): string + { + return $input->label($this->label ?: null); + } + + public function isRequired(LocaleInput $input): bool + { + return $this->required && ($input->is_primary || $this->requireAll); + } + + /** + * The tab and panel name for a locale: title-dv + */ + public function tabName(LocaleInput $input): string + { + return $this->stringToId($this->name) . '-' . $input->locale->code; + } + + /** + * The slot variable javaabu/forms' tabs read for a tab name + */ + public function slotName(LocaleInput $input): string + { + return Str::of($this->tabName($input))->camel()->snake()->replace('_', ' ')->camel()->toString(); + } + + /** + * @return array + */ + public function tabs(): array + { + return $this->inputs + ->map(fn (LocaleInput $input) => [ + 'name' => $this->tabName($input), + 'title' => $input->locale->native_name, + ]) + ->values() + ->all(); + } + + /** + * HTML attributes for a locale's input: direction and language, and the Thaana keyboard for Dhivehi + * + * @return array + */ + public function inputAttributes(LocaleInput $input): array + { + $attributes = ['dir' => $input->locale->direction, 'lang' => $input->locale->locale]; + + // What javaabu/forms' `thaana` option adds; its script turns on the Thaana keyboard + if ($input->locale->isThaana()) { + $attributes['data-thaana'] = ''; + } + + return $attributes; + } + + protected function firstLocaleWithErrors(): ?string + { + $errors = session('errors'); + + if (! $errors instanceof ViewErrorBag) { + return null; + } + + return $this->inputs + ->first(fn (LocaleInput $input) => $errors->has($input->name)) + ?->locale + ->code; + } +} diff --git a/src/Views/Components/LanguageSwitcher.php b/src/Views/Components/LanguageSwitcher.php new file mode 100644 index 0000000..c0f9fa9 --- /dev/null +++ b/src/Views/Components/LanguageSwitcher.php @@ -0,0 +1,56 @@ + + * + */ +class LanguageSwitcher extends Component +{ + protected string $view = 'language-switcher'; + + public function __construct( + public ?Model $model = null, + public string $action = 'show', + string $framework = '', + ) { + parent::__construct($framework); + } + + /** + * @return array + */ + public function items(): array + { + $current = Translatable::currentLocale(); + + return Translatable::locales() + ->map(fn (Locale $locale) => [ + 'title' => $locale->native_name, + 'url' => $this->url($locale), + 'active' => $locale->code === $current, + ]) + ->all(); + } + + protected function url(Locale $locale): string + { + if ($this->model && method_exists($this->model, 'url')) { + try { + return $this->model->url($this->action, $locale->code); + } catch (Throwable) { + // fall back to the current page + } + } + + return Translatable::switchUrl($locale->code); + } +} diff --git a/src/Views/Components/TableCells.php b/src/Views/Components/TableCells.php index f47f5b3..929736f 100644 --- a/src/Views/Components/TableCells.php +++ b/src/Views/Components/TableCells.php @@ -2,42 +2,72 @@ namespace Javaabu\Translatable\Views\Components; -use Illuminate\Contracts\View\View; use Illuminate\Support\Collection; -use Illuminate\View\Component; -use Javaabu\Translatable\Contracts\DbTranslatable; -use Javaabu\Translatable\Contracts\Translatable; -use Javaabu\Translatable\Facades\Languages; +use Javaabu\Translatable\Contracts\Translatable as TranslatableContract; +use Javaabu\Translatable\Locale; +use Javaabu\Translatable\Translatable; +/** + * One table cell per language (except the current one) with a link to add or edit the translation + * + * Pair with in the table head. + */ class TableCells extends Component { + protected string $view = 'table-cells'; + + /** @var Collection */ public Collection $languages; public function __construct( - public Translatable $model, + public TranslatableContract $model, + string $framework = '', ) { - $this->languages = Languages::allExceptCurrent(); + parent::__construct($framework); + + $current = Translatable::currentLocale(); + + $this->languages = Translatable::locales()->reject(fn (Locale $locale) => $locale->code === $current)->values(); + } + + public function isTranslated(Locale $locale): bool + { + return $this->model->hasTranslation($locale->code); } - public function getUrl(string $action, string $locale): string + /** + * The link to add or edit the translation in the locale + */ + public function translationUrl(Locale $locale): ?string { + return Translatable::translationUrl($this->model, $locale->code, $this->isTranslated($locale)); + } + + /** + * The model's URL for an action in a locale (v2 API) + */ + public function getUrl(string $action, Locale|string $locale): string + { + $locale = $locale instanceof Locale ? $locale->code : $locale; + $url = $this->model->url($action, $locale); - // If the model is we need to add "lang_parent" to the params - if ($this->isModelDbTranslatable() && ($action === 'create')) { - $url = add_query_arg('lang_parent', $this->model->id, $url); + if ($this->isModelDbTranslatable() && $action === 'create' && ! str_contains($url, 'lang_parent=')) { + $url = add_query_arg('lang_parent', $this->model->getKey(), $url); } return $url; } - public function isModelDbTranslatable(): bool + public function linkTitle(Locale $locale): string { - return $this->model instanceof DbTranslatable; + return $this->isTranslated($locale) + ? __('translatable::strings.edit_translation_in', ['locale' => $locale->native_name]) + : __('translatable::strings.add_translation_in', ['locale' => $locale->native_name]); } - public function render(): View + public function isModelDbTranslatable(): bool { - return view('translatable::table-cells'); + return Translatable::isDbTranslatable($this->model); } } diff --git a/src/Views/Components/Text.php b/src/Views/Components/Text.php new file mode 100644 index 0000000..d44eac7 --- /dev/null +++ b/src/Views/Components/Text.php @@ -0,0 +1,18 @@ + + */ class Titles extends Component { + protected string $view = 'table-titles'; + + /** @var Collection */ public Collection $languages; - public function __construct() + public function __construct(string $framework = '') { - $this->languages = Languages::allExceptCurrent(); - } + parent::__construct($framework); - public function render(): View - { - return view('translatable::table-titles'); + $current = Translatable::currentLocale(); + + $this->languages = Translatable::locales()->reject(fn (Locale $locale) => $locale->code === $current)->values(); } } diff --git a/src/Views/Components/TranslationNotice.php b/src/Views/Components/TranslationNotice.php new file mode 100644 index 0000000..73a0e5b --- /dev/null +++ b/src/Views/Components/TranslationNotice.php @@ -0,0 +1,54 @@ + + */ +class TranslationNotice extends Component +{ + protected string $view = 'translation-notice'; + + public string $locale; + + public function __construct( + public ?Model $parent = null, + ?string $locale = null, + public string $titleAttribute = '', + string $framework = '', + ) { + parent::__construct($framework); + + $this->locale = $locale ?? Translatable::currentLocale(); + } + + public function shouldRender(): bool + { + return $this->parent !== null; + } + + public function message(): string + { + return __('translatable::strings.adding_translation', [ + 'locale' => Translatable::locale($this->locale)?->native_name ?? $this->locale, + 'title' => $this->parentTitle(), + ]); + } + + protected function parentTitle(): string + { + $attribute = $this->titleAttribute ?: collect(['title', 'name'])->first(fn ($key) => filled($this->parent->getAttribute($key))); + + if (! $attribute) { + return (string) $this->parent->getKey(); + } + + return (string) $this->parent->translate($attribute, $this->parent->primaryLocale()); + } +} diff --git a/src/Views/Components/TranslationStatus.php b/src/Views/Components/TranslationStatus.php new file mode 100644 index 0000000..b927879 --- /dev/null +++ b/src/Views/Components/TranslationStatus.php @@ -0,0 +1,58 @@ + + */ +class TranslationStatus extends Component +{ + protected string $view = 'translation-status'; + + public function __construct( + public ?Model $model = null, + public bool $links = true, + string $framework = '', + ) { + parent::__construct($framework); + } + + /** + * @return array> + */ + public function items(): array + { + return Translatable::status( + $this->model, + $this->links ? fn (Locale $locale, bool $translated) => Translatable::translationUrl($this->model, $locale->code, $translated) : null, + ); + } + + /** + * @param array $item + */ + public function color(array $item): string + { + return $item['translated'] ? 'success' : 'secondary'; + } + + /** + * @param array $item + */ + public function description(array $item): string + { + $locale = $item['language']['native_name']; + + return match (true) { + $item['primary'] => __('translatable::strings.original_in', ['locale' => $locale]), + $item['translated'] => __('translatable::strings.translated_in', ['locale' => $locale]), + default => __('translatable::strings.not_translated_in', ['locale' => $locale]), + }; + } +} diff --git a/tests/Feature/Views/BladeComponentsTest.php b/tests/Feature/Views/BladeComponentsTest.php new file mode 100644 index 0000000..1de6d15 --- /dev/null +++ b/tests/Feature/Views/BladeComponentsTest.php @@ -0,0 +1,285 @@ +setUpLanguages(['en', 'dv']); + } + + protected function defineRoutes($router): void + { + parent::defineRoutes($router); + + $router->get('{language}/articles/{article}/edit', fn () => 'edit')->name('articles.edit'); + $router->get('{language}/posts/create', fn () => 'create')->name('posts.create'); + $router->get('{language}/posts/{post}/edit', fn () => 'edit')->name('posts.edit'); + + $router->get('{language}/about', fn () => Blade::render('')) + ->middleware(['web', 'language']) + ->name('about'); + + $router->get('contact', fn () => Blade::render(''))->middleware(['web', 'language']); + } + + public static function frameworks(): array + { + return [ + 'onu' => ['onu'], + 'bootstrap-6' => ['bootstrap-6'], + ]; + } + + protected function render(string $blade, array $data = [], string $framework = 'onu'): Crawler + { + config(['forms.framework' => $framework]); + + return new Crawler(Blade::render($blade, $data)); + } + + #[Test] + #[DataProvider('frameworks')] + public function it_renders_a_tab_per_locale_with_the_bound_values(string $framework): void + { + $page = Page::query()->create(['title' => 'Welcome', 'lang' => 'en', 'title_dv' => 'މަރުޙަބާ']); + + $html = $this->render('', compact('page'), $framework); + + $this->assertEquals(['English', 'ދިވެހި'], $html->filter('[role="tab"]')->each(fn (Crawler $tab) => trim($tab->text()))); + $this->assertCount(2, $html->filter('[role="tabpanel"]')); + $this->assertEquals('Welcome', $html->filter('input[name="title_en"]')->attr('value')); + $this->assertEquals('މަރުޙަބާ', $html->filter('input[name="title_dv"]')->attr('value')); + + $dhivehi = $html->filter('input[name="title_dv"]'); + $this->assertEquals('rtl', $dhivehi->attr('dir')); + $this->assertEquals('dv', $dhivehi->attr('lang')); + $this->assertNotNull($dhivehi->attr('data-thaana')); + $this->assertNull($dhivehi->attr('required')); + $this->assertNotNull($html->filter('input[name="title_en"]')->attr('required')); + $this->assertStringContainsString('Title (ދިވެހި)', $html->filter('label[for="title_dv"]')->text()); + } + + #[Test] + public function it_puts_the_models_primary_locale_first(): void + { + $page = Page::query()->create(['title' => 'މަރުޙަބާ', 'lang' => 'dv']); + + $html = $this->render('', compact('page')); + + $this->assertEquals('ދިވެހި', trim($html->filter('[role="tab"]')->first()->text())); + $this->assertEquals('މަރުޙަބާ', trim($html->filter('textarea[name="title_dv"]')->text())); + } + + #[Test] + public function it_opens_the_first_tab_with_an_error(): void + { + $errors = (new ViewErrorBag)->put('default', new MessageBag(['title_dv' => ['ސުރުޚީ ލިޔުއްވާ']])); + + session()->put('errors', $errors); + view()->share('errors', $errors); + + $html = $this->render(''); + + $this->assertEquals('true', $html->filter('#title-dv-tab')->attr('aria-selected')); + $this->assertStringContainsString('ސުރުޚީ ލިޔުއްވާ', $html->filter('#title-dv')->text()); + } + + #[Test] + #[DataProvider('frameworks')] + public function it_stacks_the_inputs_when_asked(string $framework): void + { + $html = $this->render('', [], $framework); + + $this->assertCount(0, $html->filter('[role="tab"]')); + $this->assertCount(1, $html->filter('.my-field[data-translatable-field="body"]')); + $this->assertEquals(['body_en', 'body_dv'], $html->filter('textarea')->each(fn (Crawler $input) => $input->attr('name'))); + $this->assertEquals('3', $html->filter('textarea[name="body_dv"]')->attr('rows')); + } + + #[Test] + public function it_gives_rtl_languages_without_thaana_their_direction(): void + { + $this->setUpLanguages(['en', 'dv', 'ar']); + + $html = $this->render(''); + + $arabic = $html->filter('input[name="title_ar"]'); + + $this->assertEquals('rtl', $arabic->attr('dir')); + $this->assertEquals('ar', $arabic->attr('lang')); + $this->assertNull($arabic->attr('data-thaana')); + $this->assertEquals('ltr', $html->filter('input[name="title_en"]')->attr('dir')); + } + + #[Test] + public function it_renders_a_plain_field_when_the_model_or_field_is_not_translatable(): void + { + $author = Author::factory()->create(['name' => 'އަޙްމަދު']); + $page = Page::query()->create(['title' => 'Welcome', 'slug' => 'welcome', 'lang' => 'en']); + + $html = $this->render('', compact('author')); + $this->assertEquals('އަޙްމަދު', $html->filter('input[name="name"]')->attr('value')); + $this->assertCount(0, $html->filter('[role="tab"]')); + + $html = $this->render('', compact('page')); + $this->assertEquals('welcome', $html->filter('input[name="slug"]')->attr('value')); + + $html = $this->withoutTranslatable(fn () => $this->render('')); + $this->assertCount(1, $html->filter('input[name="title"]')); + } + + #[Test] + #[DataProvider('frameworks')] + public function it_links_to_the_current_page_in_each_language(string $framework): void + { + config(['forms.framework' => $framework]); + + $html = new Crawler($this->get('/dv/about?tab=2')->assertOk()->getContent()); + + $links = $html->filter('[data-translatable="language-switcher"] a'); + + $this->assertEquals(['English', 'ދިވެހި'], $links->each(fn (Crawler $link) => trim($link->text()))); + $this->assertEquals(url('/en/about') . '?tab=2', $links->first()->attr('href')); + $this->assertEquals('page', $links->last()->attr('aria-current')); + } + + #[Test] + public function it_switches_with_a_query_string_on_routes_without_a_language(): void + { + $html = new Crawler($this->get('/contact?lang=dv')->getContent()); + + $this->assertEquals(url('/contact') . '?lang=en', $html->filter('a')->first()->attr('href')); + $this->assertEquals('page', $html->filter('a')->last()->attr('aria-current')); + } + + #[Test] + public function it_hides_the_switcher_with_one_language(): void + { + Languages::get('dv')->update(['active' => false]); + + $this->assertEquals('', trim(Blade::render(''))); + } + + #[Test] + public function it_links_the_switcher_to_the_models_pages(): void + { + $article = Article::factory()->withAuthor()->create(['lang' => 'en']); + + $html = $this->render('', compact('article')); + + $this->assertEquals(url("/dv/articles/{$article->id}"), $html->filter('a')->last()->attr('href')); + } + + #[Test] + #[DataProvider('frameworks')] + public function it_shows_the_translation_status_of_a_json_model(string $framework): void + { + $article = Article::factory()->withAuthor()->create(['lang' => 'en']); + + $html = $this->render('', compact('article'), $framework); + + $items = $html->filter('[data-translatable="translation-status"] li'); + + $this->assertEquals(['true', 'false'], $items->each(fn (Crawler $item) => $item->attr('data-translated'))); + $this->assertEquals(url("/dv/articles/{$article->id}/edit"), $items->last()->filter('a')->attr('href')); + $this->assertEquals('Not translated into ދިވެހި', $items->last()->filter('a')->attr('title')); + $this->assertStringContainsString('ދިވެހި', $items->last()->text()); + } + + #[Test] + public function it_links_missing_db_translations_to_the_create_page(): void + { + $post = Post::factory()->withAuthor()->create(['lang' => 'en']); + + $html = $this->render('', compact('post')); + + $this->assertEquals(url("/dv/posts/create?lang_parent={$post->id}"), $html->filter('li')->last()->filter('a')->attr('href')); + + $translation = $post->addTranslation('dv', 'title', 'މަރުޙަބާ'); + + $html = $this->render('', compact('post')); + + $this->assertEquals(url("/dv/posts/{$translation->id}/edit"), $html->filter('li')->last()->filter('a')->attr('href')); + } + + #[Test] + public function it_shows_the_status_without_links(): void + { + $page = Page::query()->create(['title' => 'Welcome', 'lang' => 'en']); + + Languages::setCurrentLocale('dv'); + app()->setLocale('dv'); + + $html = $this->render('', compact('page')); + + $this->assertCount(0, $html->filter('a')); + $this->assertEquals('ދިވެހި އަށް ތަރުޖަމާކުރެވިފައެއް ނުވޭ', $html->filter('li')->last()->filter('span')->attr('title')); + $this->assertEquals('ތަރުޖަމާތައް', $html->filter('ul')->attr('aria-label')); + } + + #[Test] + #[DataProvider('frameworks')] + public function it_renders_table_headings_and_cells_for_the_other_languages(string $framework): void + { + $article = Article::factory()->withAuthor()->create(['lang' => 'en']); + + $html = $this->render('
', compact('article'), $framework); + + $this->assertEquals(['ދިވެހި'], $html->filter('th')->each(fn (Crawler $th) => trim($th->text()))); + $this->assertEquals('dv', $html->filter('th')->attr('lang')); + $this->assertCount(1, $html->filter('td')); + $this->assertEquals(url("/dv/articles/{$article->id}/edit"), $html->filter('td a')->attr('href')); + $this->assertEquals('Add ދިވެހި translation', $html->filter('td a')->attr('title')); + $this->assertCount(1, $html->filter('td a svg')); + } + + #[Test] + public function it_renders_table_cells_without_links_when_the_routes_are_missing(): void + { + $page = Page::query()->create(['title' => 'Welcome', 'lang' => 'en', 'title_dv' => 'މަރުޙަބާ']); + + $html = $this->render('', compact('page')); + + $this->assertCount(0, $html->filter('a')); + $this->assertEquals('Edit ދިވެހި translation', $html->filter('span[data-translated="true"]')->attr('title')); + } + + #[Test] + #[DataProvider('frameworks')] + public function it_renders_the_notice_for_adding_a_db_translation(string $framework): void + { + $post = Post::factory()->withAuthor()->create(['lang' => 'en', 'title' => 'Welcome']); + + Languages::setCurrentLocale('dv'); + + $html = $this->render('
', compact('post'), $framework); + + $this->assertStringContainsString('“Welcome” ގެ ދިވެހި ތަރުޖަމާ އިތުރުކުރަނީ.', $html->filter('[data-translatable="translation-notice"]')->text()); + $this->assertEquals('dv', $html->filter('input[name="lang"]')->attr('value')); + $this->assertEquals((string) $post->id, $html->filter('input[name="lang_parent"]')->attr('value')); + + $this->assertEquals('', trim(Blade::render(''))); + } +} diff --git a/tests/TestCase.php b/tests/TestCase.php index 273e598..396b3b2 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -2,7 +2,9 @@ namespace Javaabu\Translatable\Tests; +use Javaabu\Forms\FormsServiceProvider; use Javaabu\Helpers\HelpersServiceProvider; +use Javaabu\Manifest\ManifestServiceProvider; use Javaabu\Translatable\Tests\TestSupport\Providers\TestServiceProvider; use Javaabu\Translatable\TranslatableServiceProvider; use Orchestra\Testbench\TestCase as BaseTestCase; @@ -16,6 +18,9 @@ protected function setUp(): void $this->app['config']->set('app.key', 'base64:yWa/ByhLC/GUvfToOuaPD7zDwB64qkc/QkaQOrT5IpE='); $this->app['config']->set('session.serialization', 'php'); + + // Views are checked for markup; injection is javaabu/manifest's job + $this->app['config']->set('manifest.inject', false); } protected function defineRoutes($router): void @@ -34,6 +39,8 @@ protected function getPackageProviders($app): array return [ TranslatableServiceProvider::class, HelpersServiceProvider::class, + ManifestServiceProvider::class, + FormsServiceProvider::class, TestServiceProvider::class, ]; } From 868f50b0c3f376291f755a8c872da40f8968dac0 Mon Sep 17 00:00:00 2001 From: Naffah Abdulla Rasheed Date: Wed, 4 Nov 2026 14:39:03 +0500 Subject: [PATCH 04/10] Add the Inertia React components, shipped through javaabu/manifest TranslatableField/Text/Textarea, LanguageSwitcher, TranslationStatus, the table headings/cells and TranslationNotice on @javaabu/forms, with en/dv strings through @javaabu/i18n; registered as @javaabu/translatable when manifest is installed, and Translatable::shared() as an Inertia prop with views = inertia. JS toolchain (pnpm, Vitest, ESLint, Prettier) and a CI job with coverage. Refs #4, #8 Co-Authored-By: Claude Opus 5.5 --- .github/workflows/tests.yml | 43 + .prettierignore | 3 + .prettierrc.json | 5 + CHANGELOG.md | 2 + composer.json | 4 +- eslint.config.js | 19 + package.json | 50 + pnpm-lock.yaml | 4850 +++++++++++++++++ resources/js/components/LanguageSwitcher.tsx | 37 + resources/js/components/TranslatableField.tsx | 136 + resources/js/components/TranslationNotice.tsx | 34 + resources/js/components/TranslationStatus.tsx | 61 + resources/js/components/TranslationTable.tsx | 71 + resources/js/context.tsx | 49 + resources/js/icons.tsx | 38 + resources/js/index.ts | 29 + resources/js/locales/dv.ts | 14 + resources/js/locales/en.ts | 15 + resources/js/locales/index.ts | 6 + resources/js/strings.ts | 36 + resources/js/types.ts | 46 + src/TranslatableServiceProvider.php | 25 + .../Views/ReactComponentsRegistrationTest.php | 66 + tests/js/TranslatableField.test.tsx | 124 + tests/js/components.test.tsx | 158 + tests/js/fixtures.ts | 46 + tests/js/setup.ts | 20 + tsconfig.json | 20 + vitest.config.ts | 21 + 29 files changed, 6027 insertions(+), 1 deletion(-) create mode 100644 .prettierignore create mode 100644 .prettierrc.json create mode 100644 eslint.config.js create mode 100644 package.json create mode 100644 pnpm-lock.yaml create mode 100644 resources/js/components/LanguageSwitcher.tsx create mode 100644 resources/js/components/TranslatableField.tsx create mode 100644 resources/js/components/TranslationNotice.tsx create mode 100644 resources/js/components/TranslationStatus.tsx create mode 100644 resources/js/components/TranslationTable.tsx create mode 100644 resources/js/context.tsx create mode 100644 resources/js/icons.tsx create mode 100644 resources/js/index.ts create mode 100644 resources/js/locales/dv.ts create mode 100644 resources/js/locales/en.ts create mode 100644 resources/js/locales/index.ts create mode 100644 resources/js/strings.ts create mode 100644 resources/js/types.ts create mode 100644 tests/Feature/Views/ReactComponentsRegistrationTest.php create mode 100644 tests/js/TranslatableField.test.tsx create mode 100644 tests/js/components.test.tsx create mode 100644 tests/js/fixtures.ts create mode 100644 tests/js/setup.ts create mode 100644 tsconfig.json create mode 100644 vitest.config.ts diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 4b5113b..5a41f8e 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -80,3 +80,46 @@ jobs: - name: Check code style run: vendor/bin/pint --test + + js: + runs-on: ubuntu-latest + + name: React components + + steps: + - name: Checkout code + uses: actions/checkout@v5 + + - name: Setup pnpm + uses: pnpm/action-setup@v4 + + - name: Setup Node + uses: actions/setup-node@v5 + with: + node-version: 22 + package-manager-cache: false + + # The default store is inside a node_modules folder, which breaks the declaration + # build of @javaabu/forms (installed from git and built on install) + - name: Install dependencies + run: pnpm install --frozen-lockfile --store-dir "$RUNNER_TEMP/pnpm-store" + + - name: Lint + run: pnpm lint + + - name: Check formatting + run: pnpm format:check + + - name: Type check + run: pnpm typecheck + + - name: Execute tests + run: pnpm test:coverage + + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v5 + with: + files: coverage/clover.xml + flags: js + fail_ci_if_error: false + token: ${{ secrets.CODECOV_TOKEN }} diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..ef44fb7 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,3 @@ +vendor +node_modules +coverage diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 0000000..6c1772b --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,5 @@ +{ + "semi": false, + "printWidth": 120, + "tabWidth": 2 +} diff --git a/CHANGELOG.md b/CHANGELOG.md index 8f6a09e..d9abfe3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,3 +22,5 @@ All notable changes to `javaabu/translatable` are documented in this file. - **Blade components on javaabu/forms v2** (Onu and Bootstrap 6): ``, `textarea` and `field` (an input per locale, in tabs or stacked, with direction, language and the Thaana keyboard), `language-switcher`, `translation-status`, `translation-notice` (adding a DB translation), and the rebuilt `titles` / `table-cells` - `Translatable::translationUrl()` and `Translatable::switchUrl()` - The `styles` config is gone: the components follow javaabu/forms' framework +- **Inertia React components**, imported as `@javaabu/translatable` through javaabu/manifest: `TranslatableProvider`, `TranslatableField`/`Text`/`Textarea`, `LanguageSwitcher`, `TranslationStatus`, `TranslationHeadings`/`TranslationCells`, `TranslationNotice`, with `en`/`dv` strings through @javaabu/i18n +- With `translatable.views` set to `inertia`, the languages are shared with every Inertia page as the `translatable` prop diff --git a/composer.json b/composer.json index 45f33df..7ac0a4a 100644 --- a/composer.json +++ b/composer.json @@ -37,6 +37,7 @@ "javaabu/helpers": "^1.74" }, "require-dev": { + "inertiajs/inertia-laravel": "^2.0 || ^3.0", "javaabu/boost-javaabu-guidelines": "^1.4", "javaabu/forms": "^2.0@dev", "javaabu/manifest": "^1.0@dev", @@ -48,7 +49,8 @@ }, "suggest": { "javaabu/forms": "Required for the Blade components (^2.0)", - "javaabu/manifest": "Ships the Inertia React components to your app (^1.0)" + "javaabu/manifest": "Ships the Inertia React components to your app (^1.0)", + "inertiajs/inertia-laravel": "Shares the languages with Inertia pages when translatable.views is inertia (^2.0)" }, "autoload": { "psr-4": { diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..183d483 --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,19 @@ +import js from "@eslint/js" +import globals from "globals" +import reactHooks from "eslint-plugin-react-hooks" +import tseslint from "typescript-eslint" + +export default tseslint.config( + { ignores: ["dist", "coverage", "node_modules", "vendor", "build"] }, + js.configs.recommended, + ...tseslint.configs.recommended, + { + files: ["**/*.{ts,tsx,js,mjs}"], + languageOptions: { globals: { ...globals.browser, ...globals.node } }, + plugins: { "react-hooks": reactHooks }, + rules: { + ...reactHooks.configs.recommended.rules, + "@typescript-eslint/no-unused-vars": ["error", { argsIgnorePattern: "^_", varsIgnorePattern: "^_" }], + }, + }, +) diff --git a/package.json b/package.json new file mode 100644 index 0000000..fb75183 --- /dev/null +++ b/package.json @@ -0,0 +1,50 @@ +{ + "name": "javaabu-translatable-dev", + "private": true, + "description": "Development tooling for the Inertia React components of javaabu/translatable. Apps don't install this; they import the components as @javaabu/translatable through javaabu/manifest.", + "license": "MIT", + "type": "module", + "packageManager": "pnpm@10.28.0", + "engines": { + "node": ">=20.19" + }, + "scripts": { + "lint": "eslint resources/js tests/js eslint.config.js vitest.config.ts", + "format": "prettier --write resources/js tests/js eslint.config.js vitest.config.ts tsconfig.json .prettierrc.json", + "format:check": "prettier --check resources/js tests/js eslint.config.js vitest.config.ts tsconfig.json .prettierrc.json", + "typecheck": "tsc --noEmit -p tsconfig.json", + "test": "vitest run", + "test:coverage": "vitest run --coverage", + "check": "pnpm lint && pnpm format:check && pnpm typecheck && pnpm test" + }, + "devDependencies": { + "@eslint/js": "^10.0.1", + "@javaabu/forms": "github:Javaabu/react-forms#next-gen", + "@javaabu/i18n": "github:Javaabu/i18n#build-on-git-install", + "@javaabu/onu": "^0.2.1", + "@testing-library/jest-dom": "^7.0.1", + "@testing-library/react": "^16.3.3", + "@testing-library/user-event": "^14.6.8", + "@types/node": "^26.6.6", + "@types/react": "^19.3.0", + "@types/react-dom": "^19.3.0", + "@vitest/coverage-v8": "^5.1.0", + "eslint": "^10.14.0", + "eslint-plugin-react-hooks": "^7.1.1", + "globals": "^17.14.0", + "jsdom": "^30.1.3", + "prettier": "^3.9.10", + "react": "^19.3.1", + "react-dom": "^19.3.1", + "typescript": "^6.0.3", + "typescript-eslint": "^8.72.1", + "vitest": "^5.1.0" + }, + "pnpm": { + "onlyBuiltDependencies": [ + "@javaabu/forms", + "@javaabu/i18n", + "esbuild" + ] + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..48c65f1 --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,4850 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + devDependencies: + '@eslint/js': + specifier: ^10.0.1 + version: 10.0.1(eslint@10.14.0) + '@javaabu/forms': + specifier: github:Javaabu/react-forms#next-gen + version: https://codeload.github.com/Javaabu/react-forms/tar.gz/41eb14a07158a6dedb62bcc4b022507d3910c3e0(@javaabu/onu@0.2.1(@tanstack/react-query@5.105.3(@types/react@19.3.0)(react@19.3.1))(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react-hook-form@7.91.0(@types/react@19.3.0)(react@19.3.1))(react@19.3.1)(tailwindcss@4.3.4))(@testing-library/react@16.3.3(@testing-library/dom@10.4.2)(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1))(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + '@javaabu/i18n': + specifier: github:Javaabu/i18n#build-on-git-install + version: https://codeload.github.com/Javaabu/i18n/tar.gz/76730212d6f0f1d87a78635ed6e005dd5713cbba(react@19.3.1) + '@javaabu/onu': + specifier: ^0.2.1 + version: 0.2.1(@tanstack/react-query@5.105.3(@types/react@19.3.0)(react@19.3.1))(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react-hook-form@7.91.0(@types/react@19.3.0)(react@19.3.1))(react@19.3.1)(tailwindcss@4.3.4) + '@testing-library/jest-dom': + specifier: ^7.0.1 + version: 7.0.1(@testing-library/dom@10.4.2)(vitest@5.1.0) + '@testing-library/react': + specifier: ^16.3.3 + version: 16.3.3(@testing-library/dom@10.4.2)(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + '@testing-library/user-event': + specifier: ^14.6.8 + version: 14.6.8(@testing-library/dom@10.4.2) + '@types/node': + specifier: ^26.6.6 + version: 26.6.6 + '@types/react': + specifier: ^19.3.0 + version: 19.3.0 + '@types/react-dom': + specifier: ^19.3.0 + version: 19.3.0(@types/react@19.3.0) + '@vitest/coverage-v8': + specifier: ^5.1.0 + version: 5.1.0(vitest@5.1.0) + eslint: + specifier: ^10.14.0 + version: 10.14.0 + eslint-plugin-react-hooks: + specifier: ^7.1.1 + version: 7.1.1(eslint@10.14.0) + globals: + specifier: ^17.14.0 + version: 17.14.0 + jsdom: + specifier: ^30.1.3 + version: 30.1.3 + prettier: + specifier: ^3.9.10 + version: 3.9.10 + react: + specifier: ^19.3.1 + version: 19.3.1 + react-dom: + specifier: ^19.3.1 + version: 19.3.1(react@19.3.1) + typescript: + specifier: ^6.0.3 + version: 6.0.3 + typescript-eslint: + specifier: ^8.72.1 + version: 8.72.1(eslint@10.14.0)(typescript@6.0.3) + vitest: + specifier: ^5.1.0 + version: 5.1.0(@types/node@26.6.6)(@vitest/coverage-v8@5.1.0)(jsdom@30.1.3)(vite@8.3.5(@types/node@26.6.6)) + +packages: + + '@adobe/css-tools@4.5.0': + resolution: {integrity: sha512-6OzddxPio9UiWTCemp4N8cYLV2ZN1ncRnV1cVGtve7dhPOtRkleRyx32GQCYSwDYgaHU3USMm84tNsvKzRCa1Q==} + + '@asamuzakjp/css-color@7.1.9': + resolution: {integrity: sha512-vZ6JQVq8Ivgz6b/IBlAf90wZviXdluY13cQOPO+nJHbmXwGrPl2GcpqUY7a55/8ZVdfONXpCFMpRijOBCHNlcQ==} + engines: {node: ^22.22.2 || ^24.15.0 || >=26.0.0} + + '@asamuzakjp/dom-selector@10.0.5': + resolution: {integrity: sha512-EgmJ7oEaGQECTn3H3wmnQpEy4hKY66VvM7VQndwPIQob3p877BgbyOUE3EX0LgXQCciF2H8KMdR+RznqjMNOLw==} + engines: {node: ^22.22.2 || ^24.15.0 || >=26.0.0} + + '@babel/code-frame@7.29.7': + resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==} + engines: {node: '>=6.9.0'} + + '@babel/compat-data@7.29.7': + resolution: {integrity: sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==} + engines: {node: '>=6.9.0'} + + '@babel/core@7.29.7': + resolution: {integrity: sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.29.12': + resolution: {integrity: sha512-2W76mhjrpQFO1lK5e8IvGYB2D0YZ7S2R1Z2BjO2ZUYuW+amcpK/z60U6ZZmzskwoa7sJ6XohP/Wg8H5KxXI/IA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-compilation-targets@7.29.7': + resolution: {integrity: sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==} + engines: {node: '>=6.9.0'} + + '@babel/helper-globals@7.29.7': + resolution: {integrity: sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.29.7': + resolution: {integrity: sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-transforms@7.29.7': + resolution: {integrity: sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-string-parser@7.29.11': + resolution: {integrity: sha512-snZbO7MEDOof+hvNa97EjEY7qD7h2kwvQr5AJC3ZhQudKW5dnej6k3kP9NQV88hJwaS8vDqRLFo9rivLEhle2g==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.29.7': + resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-option@7.29.7': + resolution: {integrity: sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==} + engines: {node: '>=6.9.0'} + + '@babel/helpers@7.29.10': + resolution: {integrity: sha512-EMCx3AGZ3WfAl6ht7AyQoPm2Lh+BGYEMdc4Q6Hf7uSCDOZFuLq44qNwL+iaim8VlyYhRAo4X43+weadK8ZB4Yg==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.29.12': + resolution: {integrity: sha512-WJ7JBp/Q4+z/ImydtI7jS6NVco5++632ExBemSX+Ysb+G1lCwQ5BVo3SY+jGGOcZXI4ZccbZSUSJLNBW6eEpJg==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/runtime@7.29.10': + resolution: {integrity: sha512-NpugTzbKgGrNe45Ui32cN8vCFFMxVtP/9rtfwv0nP5FRDAjvYBxJcLVpYA9mr1jxP6cnIT7x9LUzuDTbWYYNRA==} + engines: {node: '>=6.9.0'} + + '@babel/template@7.29.7': + resolution: {integrity: sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.29.12': + resolution: {integrity: sha512-MscXe/NAZcOkYEsRajHZFt3ynsb22PUi6CTrp5U7+RYycR9By84mn9mFN54YgfITiqK4hFx3Ju3tZSyzfpS5ew==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.29.11': + resolution: {integrity: sha512-WCsemW40Cpc6gouC1SrY6PBaZ+oLRlEVY9XBvZBI7il/HyUrb2nJ505wE1MHEFyDZNnrWDEuy4jL50WVHfTTSQ==} + engines: {node: '>=6.9.0'} + + '@bcoe/v8-coverage@1.0.2': + resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==} + engines: {node: '>=18'} + + '@bramus/specificity@2.4.3': + resolution: {integrity: sha512-0GEz8qJWokkUVYQYRukmkSHIFNoBH8QTreeHrAFbvz1XFYc3Y+puhMs8PQ5x8GNugiC2l5jvDBXKLbBIByeSBw==} + hasBin: true + + '@cacheable/memory@2.2.0': + resolution: {integrity: sha512-CTLKqLItRCEixEAewD3/j9DB3/o96gpTPD4eJ1v+DGOlxZRZncRQkGYqqnAGCscYd6RNeXfGeiuCphsPtqyIfQ==} + + '@cacheable/utils@2.5.0': + resolution: {integrity: sha512-buipgOVDkkPXNR5+xBpDw7Zk2n1EvU7qBJCNUcL7rhQ//kfpOXPAvQ511Os0vpLYJ1pZnvudNytkQt2hst3wqA==} + + '@csstools/color-helpers@6.1.2': + resolution: {integrity: sha512-grhRy3OKmniaAEKXMjua5z/EODX0MSqBGjunw8+j/3HQjOnahs2AGhvEOIYVUWcU6ScApbhLhVrQTX8XqrMrow==} + engines: {node: '>=20.19.0'} + + '@csstools/css-calc@3.4.3': + resolution: {integrity: sha512-iex20d8CHVkyvg6B7UKV7uHnI2Bqo9g+EFfT9E0y+GvTvhZ/DwONJ+9aKb1dlqm0ZiGsL5RXjp0fCoJYnkeDjA==} + engines: {node: '>=20.19.0'} + peerDependencies: + '@csstools/css-parser-algorithms': ^4.0.2 + '@csstools/css-tokenizer': ^4.0.2 + + '@csstools/css-color-parser@4.2.7': + resolution: {integrity: sha512-zgm9yjQz5iJB4VL8RyVwd71TWB8Q89FSxt/NzXoWqPSkEBOVtn9L6vAPtSc9hpKfB0Awo/AL2VKFHjYetBLFHQ==} + engines: {node: '>=20.19.0'} + peerDependencies: + '@csstools/css-parser-algorithms': ^4.0.2 + '@csstools/css-tokenizer': ^4.0.2 + + '@csstools/css-parser-algorithms@4.0.2': + resolution: {integrity: sha512-40cSKyMvK+tq4qz6Awrlye2WGuOKt3FwPgtGg6KTfbHOWNw+Rk1rzbAtZnZ6IBhsY491HLRnDXwoyBAijmmILA==} + engines: {node: '>=20.19.0'} + peerDependencies: + '@csstools/css-tokenizer': ^4.0.2 + + '@csstools/css-syntax-patches-for-csstree@1.1.16': + resolution: {integrity: sha512-LsDbJPP/B8cPasUJyTA5S99F7C4G93ngW8hUluXz8CFeOaaRnBPtBiIlvgcPYU0HmHNz8CXg+1vwFrIB4F6Dgw==} + peerDependencies: + css-tree: ^3.2.1 + peerDependenciesMeta: + css-tree: + optional: true + + '@csstools/css-tokenizer@4.0.2': + resolution: {integrity: sha512-OoKoR0f76dCY666JlcbhmVTs2drYj1GUXZTYTcbUgJjh9Nv41aFfZ21bPQTERm5+L5cBDo466NltB2lplS5GBw==} + engines: {node: '>=20.19.0'} + + '@date-fns/tz@1.5.0': + resolution: {integrity: sha512-lwYN/vDPeNRULcepoE/LO2Pgx+7/RV+S9ARfbc9lr2DtGkOD7pAiruHvbR1RX3Qyf6ja47EWJDMsNK5vK08DJg==} + + '@dnd-kit/abstract@0.5.3': + resolution: {integrity: sha512-KZQ2vftkO9BHupVzlUqB5d7JETL7ITlvslx3hrj4aObwjliAzuzV/n5wCpQllNd0Hxvg7FaQtLekJ8dplqKwbA==} + + '@dnd-kit/collision@0.5.3': + resolution: {integrity: sha512-DBo8czkI72xnsHRWOMTn/GAyYgSWy9bmde85mrZ7ShqLhcbIbcu4PVJ3ZU9MBikePCSAVOoUTZE2gS40jogrEw==} + + '@dnd-kit/dom@0.5.3': + resolution: {integrity: sha512-JJe+Qu/JmofGzHes3np91sQYwp5spvLcknjRkq+VO59JTqSnQNPXlhHXavRXTwQFabaaRyRzXyH/faAvubCNGw==} + + '@dnd-kit/geometry@0.5.3': + resolution: {integrity: sha512-U+zscNjoHfJ1xUV+XDGF89jPCoILLivWSb/U9mfM7h83f6o3X9AS9uIwG46bXUpCqPvlI/9JcOlyga5uvfyjgw==} + + '@dnd-kit/helpers@0.5.3': + resolution: {integrity: sha512-fErbbjwKMJEdr7e+AMsXaZ6aQY4xPYBMK+nqybB34tE6/lQDLetZHsgLDKNVePsYIZCq/7uWMtlFjqysscWg5A==} + + '@dnd-kit/react@0.5.3': + resolution: {integrity: sha512-BGn6bakgcRSoKMRQP8cRIGu59e5lGC3/sL0zVWmDMo/9CMPSxgjCcQltDn5TDTwAARnEbuwNEksor4rLtH/yEg==} + peerDependencies: + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + + '@dnd-kit/state@0.5.3': + resolution: {integrity: sha512-NJTQkBglAT/SXLc+IOLiKwNvsTrWmPG5AO+7Gn0F0n8XyVaf4VCbTCuPuU48sDBfcpg23O0tfGkfZNiZbYTvMw==} + + '@eslint-community/eslint-utils@4.10.1': + resolution: {integrity: sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + + '@eslint-community/regexpp@4.12.2': + resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + + '@eslint/config-array@0.23.5': + resolution: {integrity: sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@eslint/config-helpers@0.7.0': + resolution: {integrity: sha512-DObd/KKUsU+FaFv4PLxSRenpXfQWmPXXP3pPZ6/K1PCrMu2vQpMDMuQe/BqYeoLcz8ro0bVDF1RxOJgfVEdhUw==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@eslint/core@1.2.1': + resolution: {integrity: sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@eslint/js@10.0.1': + resolution: {integrity: sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + peerDependencies: + eslint: ^10.0.0 + peerDependenciesMeta: + eslint: + optional: true + + '@eslint/object-schema@3.0.5': + resolution: {integrity: sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@eslint/plugin-kit@0.7.3': + resolution: {integrity: sha512-IkO+/KEUvwbVpiURZg+P7zF74z5Jxe0UgJxVni+RtoHQ6IZieXaO02kmadomap/q+l6bc/jdPGGqTjhuZnuz1Q==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@exodus/bytes@1.16.0': + resolution: {integrity: sha512-IcpW84uEn3N7ETtNZMlxKhfl6Pec8rUNGOTBtWbK1FKhJxIFAptZyVrvVRVBimAJxJCgc3PxepxkdWWG4DVzfA==} + 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 + + '@floating-ui/core@1.8.0': + resolution: {integrity: sha512-0CIZ5itps/8x7BG8dEIhs53BvCUH2PCoogtakwRTut+Arm58sJooJ0AuZhLw2HJYIR5cMLNPBSS728sPho2khQ==} + + '@floating-ui/dom@1.8.0': + resolution: {integrity: sha512-yXSrzeHZBTZadLOlfyhCkJHNeLJnHRnRInwdZ40L7ZiaAtrBwoYlsDrX3v5zB1Utk7CLfzcOVnVVWoXEky7Ceg==} + + '@floating-ui/react-dom@2.1.9': + resolution: {integrity: sha512-JDjEFGCpImxDCA7JJKviA0M9+RtmJdj0m/NVU5IMgBK+AmZouAQQ7/+2GLH0GXXY0YMw9oXPB8hKdbPYg5QLYg==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + + '@floating-ui/utils@0.2.12': + resolution: {integrity: sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww==} + + '@humanfs/core@0.19.2': + resolution: {integrity: sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==} + engines: {node: '>=18.18.0'} + + '@humanfs/node@0.16.8': + resolution: {integrity: sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==} + engines: {node: '>=18.18.0'} + + '@humanfs/types@0.15.0': + resolution: {integrity: sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==} + engines: {node: '>=18.18.0'} + + '@humanwhocodes/module-importer@1.0.1': + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + + '@humanwhocodes/retry@0.4.3': + resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} + engines: {node: '>=18.18'} + + '@internationalized/date@3.12.4': + resolution: {integrity: sha512-M1dEn4c1U1HsSlaVR8upZtSqvXrTkHDfv18H01uCSJyjVLDxnBR38v/fMxecmlwXKR4i9HeZcmgQAPE6A+aGJQ==} + + '@internationalized/number@3.6.9': + resolution: {integrity: sha512-GgWy2caO/+hV9sT/oCTWGHvbjmJMPT9kkXzoKRLnkEq7adghhR7UprhvY9sXrKfrp3NcDfhF6QQHXR1KfQbB8g==} + + '@internationalized/string@3.2.10': + resolution: {integrity: sha512-PDx6//vHSpRnHfxqMqto11zQvhsaU74O3mKv2F/0eicGZcl9NLjQmGlbHz/LsJh5tLKp4A4L7ZVTzN1/MmMTvA==} + + '@javaabu/forms@https://codeload.github.com/Javaabu/react-forms/tar.gz/41eb14a07158a6dedb62bcc4b022507d3910c3e0': + resolution: {tarball: https://codeload.github.com/Javaabu/react-forms/tar.gz/41eb14a07158a6dedb62bcc4b022507d3910c3e0} + version: 0.0.0 + engines: {node: '>=20.19'} + peerDependencies: + '@inertiajs/react': ^2.0.0 || ^3.0.0 + '@javaabu/onu': ^0.2.1 + '@testing-library/react': ^16.0.0 + react: ^18.3.0 || ^19.0.0 + react-dom: ^18.3.0 || ^19.0.0 + peerDependenciesMeta: + '@inertiajs/react': + optional: true + '@javaabu/onu': + optional: true + '@testing-library/react': + optional: true + + '@javaabu/i18n@https://codeload.github.com/Javaabu/i18n/tar.gz/76730212d6f0f1d87a78635ed6e005dd5713cbba': + resolution: {tarball: https://codeload.github.com/Javaabu/i18n/tar.gz/76730212d6f0f1d87a78635ed6e005dd5713cbba} + version: 0.0.0 + engines: {node: '>=20.19'} + peerDependencies: + react: ^18.0.0 || ^19.0.0 + vue: ^3.3.0 + peerDependenciesMeta: + react: + optional: true + vue: + optional: true + + '@javaabu/onu@0.2.1': + resolution: {integrity: sha512-GE4takAQwNQqD9ZmQooH7KMBrLUoiR4UzJsDCGr6cLYigoLF22tjRD3CzrdVGVl+SAhz2jjwOozxzyvAyTNmxA==} + peerDependencies: + '@deck.gl/aggregation-layers': ^9.4.0 + '@deck.gl/core': ^9.4.0 + '@deck.gl/google-maps': ^9.4.0 + '@deck.gl/layers': ^9.4.0 + '@fontsource-variable/geist': ^5.3.0 + '@fontsource-variable/inter': ^5.3.0 + '@fontsource-variable/jetbrains-mono': ^5.3.0 + '@fontsource-variable/noto-sans-thaana': ^5.3.0 + '@fontsource-variable/source-serif-4': ^5.3.0 + '@fortawesome/fontawesome-svg-core': ^7.3.1 + '@fortawesome/react-fontawesome': ^3.5.0 + '@fullcalendar/core': ^6.1.21 + '@fullcalendar/daygrid': ^6.1.21 + '@fullcalendar/interaction': ^6.1.21 + '@fullcalendar/list': ^6.1.21 + '@fullcalendar/react': ^6.1.21 + '@fullcalendar/timegrid': ^6.1.21 + '@googlemaps/markerclusterer': ^2.6.2 + '@iconify/react': ^6.0.2 + '@tanstack/react-query': ^5.104.1 + '@tiptap/core': ^3.31.4 + '@tiptap/extension-image': ^3.31.4 + '@tiptap/extension-placeholder': ^3.31.4 + '@tiptap/extension-table': ^3.31.4 + '@tiptap/extension-text-align': ^3.31.4 + '@tiptap/pm': ^3.31.4 + '@tiptap/react': ^3.31.4 + '@tiptap/starter-kit': ^3.31.4 + '@tiptap/static-renderer': ^3.31.4 + '@tiptap/suggestion': ^3.31.4 + '@turf/turf': ^7.4.0 + '@vis.gl/react-google-maps': ^1.10.1 + barcode-detector: ^3.2.2 + media-chrome: ^4.19.3 + react: ^19.0.0 + react-dom: ^19.0.0 + react-easy-crop: ^6.2.4 + react-hook-form: ^7.89.0 + recharts: ^3.10.1 + shiki: ^4.5.0 + signature_pad: ^5.1.4 + tailwindcss: ^4.0.0 + terra-draw: ^1.37.0 + terra-draw-google-maps-adapter: ^1.7.0 + write-excel-file: ^4.1.1 + yet-another-react-lightbox: ^3.32.2 + peerDependenciesMeta: + '@deck.gl/aggregation-layers': + optional: true + '@deck.gl/core': + optional: true + '@deck.gl/google-maps': + optional: true + '@deck.gl/layers': + optional: true + '@fontsource-variable/geist': + optional: true + '@fontsource-variable/inter': + optional: true + '@fontsource-variable/jetbrains-mono': + optional: true + '@fontsource-variable/noto-sans-thaana': + optional: true + '@fontsource-variable/source-serif-4': + optional: true + '@fortawesome/fontawesome-svg-core': + optional: true + '@fortawesome/react-fontawesome': + optional: true + '@fullcalendar/core': + optional: true + '@fullcalendar/daygrid': + optional: true + '@fullcalendar/interaction': + optional: true + '@fullcalendar/list': + optional: true + '@fullcalendar/react': + optional: true + '@fullcalendar/timegrid': + optional: true + '@googlemaps/markerclusterer': + optional: true + '@iconify/react': + optional: true + '@tiptap/core': + optional: true + '@tiptap/extension-image': + optional: true + '@tiptap/extension-placeholder': + optional: true + '@tiptap/extension-table': + optional: true + '@tiptap/extension-text-align': + optional: true + '@tiptap/pm': + optional: true + '@tiptap/react': + optional: true + '@tiptap/starter-kit': + optional: true + '@tiptap/static-renderer': + optional: true + '@tiptap/suggestion': + optional: true + '@turf/turf': + optional: true + '@vis.gl/react-google-maps': + optional: true + barcode-detector: + optional: true + media-chrome: + optional: true + react-easy-crop: + optional: true + recharts: + optional: true + shiki: + optional: true + signature_pad: + optional: true + terra-draw: + optional: true + terra-draw-google-maps-adapter: + optional: true + write-excel-file: + optional: true + yet-another-react-lightbox: + optional: true + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.6.0': + resolution: {integrity: sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@keyv/bigmap@1.3.1': + resolution: {integrity: sha512-WbzE9sdmQtKy8vrNPa9BRnwZh5UF4s1KTmSK0KUVLo3eff5BlQNNWDnFOouNpKfPKDnms9xynJjsMYjMaT/aFQ==} + engines: {node: '>= 18'} + peerDependencies: + keyv: ^5.6.0 + + '@keyv/serialize@1.1.1': + resolution: {integrity: sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA==} + + '@oxc-project/types@0.156.0': + resolution: {integrity: sha512-1Duyg48TdJNdyhEB8Xn1L36bT9Pv6GUtlKjHxOys8xkaF/rQq3XJE+bAurSK/AgIz5mGFsb77cn9Pb20+WCScw==} + + '@preact/signals-core@1.14.5': + resolution: {integrity: sha512-vcRv7Ityz5JVmeiVqIluLFzT/PDS/AlhHUpw3/aUWqcVUs6bDGIPXE/6RSC4evBNWYvnniozg5Syfj33al3gkg==} + + '@radix-ui/number@1.1.3': + resolution: {integrity: sha512-Road2bidD0uu/1BGDOWNdPI06g0lIRy6IF9GZcIrDK2KGItfor8IQwQa+yM2ERgHM1MmHxaxpTzk0/Jp42lNfA==} + + '@radix-ui/primitive@1.2.0': + resolution: {integrity: sha512-KABY+VMIRPJL+Vyzv/335FEhXXD/xDv8BMb1kl48/aXJZMH0LtHCTx5ga5RnxaEP3nZHtmTLf1P9vanmFX46bw==} + + '@radix-ui/react-accessible-icon@1.2.0': + resolution: {integrity: sha512-BB4+HHkFl3HipEAhm/mLhrpXV+2kRX7MvY4AaImUcQVeWQt5KhV1JEByNs8NGfJOcr95RcQGijt9+OrCo68xGA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-accordion@1.3.1': + resolution: {integrity: sha512-lCZ6sbGS/7WO0j84yx4srIgOf0MRJ/sZ13RonADXxvKBym8xrxNCq5f6DibGcr38sIlwPk8/52uYdXeOPsbEDw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-alert-dialog@1.2.1': + resolution: {integrity: sha512-MWC4N7RY+qsbNrFNo7yQ9ZbdoZEW+B5DtpWxJfUnVCqcQxxqqg3qis3RMPTDbCNaTIdHJSn5XDkl9ZdVWXknLg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-arrow@1.1.17': + resolution: {integrity: sha512-67jjcVvxjXCCStnTW3Z+M7jmaFMuMR7M2JgpDzH4w7JSRrikmzhjfYgP/09ECb8n7whnsgh1ifJ+Ie96qEj+pQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-aspect-ratio@1.2.0': + resolution: {integrity: sha512-gHEU+Lx8DlT/FYIjZQvvBJrFSwUngXTjh7FeL9QvJaNALDLez6pgDj2axCUihBjtPBy82LN0gNb25+sTcSXNmA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-avatar@1.3.1': + resolution: {integrity: sha512-ssZOkfQNkFpYVmCPQlr978xSaU7moSrGXdwg4j2Xs2WlGl/1KsSZtfpK+WnLnx0Rwll1BAh6oUrAy/uR31SBvQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-checkbox@1.4.1': + resolution: {integrity: sha512-KWtwhh+98ML3seZ9vO9yRY2OwaQG90dCiDAxjpSNY2AaZ3Hik3B0dsk6m0ILywo/no3A9/SozadyPMNweVT8yw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-collapsible@1.2.1': + resolution: {integrity: sha512-fSzLb/qy1wuZSxQBcxpW0hNXYHDMWiIXDGyJGdA4+FSLknOWZsNRrTCUavxb4PI8b8lvHo/CysO6NVi5zLpo5g==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-collection@1.2.0': + resolution: {integrity: sha512-+yiQzqcc7Sy7/5s0jrMCjpPRFB3538VNrXPWdTLZhE/SCfgEws4aI6lHWN/vvcnyhHndROWBzqmqB4QBrITZ9Q==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-compose-refs@1.1.5': + resolution: {integrity: sha512-+48PbAAbq3didjJxa+OaWY2ZwgAKsNiRGyeHKszblZMQ+kcpd9pAaT11cMkGEie0vsOi3QdeTE6d5Fe3Gn61kA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-context-menu@2.4.1': + resolution: {integrity: sha512-Pddn3Z8lio3/ChxyXaEP3jHTTo1LTHCWwF8+mG1chQlWijP+ttVHBKwezrQTiV6Mj670ojvumqvuGh+mdv33iQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-context@1.3.0': + resolution: {integrity: sha512-v5AYEocMISkbJ+1fTXzZpiFv7NRr3D2WiYbLIKf+FkmJ8VkP6d503eV2n34MFussHXY1pcoq3yfJiQH77dc4Mg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-dialog@1.3.1': + resolution: {integrity: sha512-+0E4qvVTwjAWra9sEhJ+9BTsvHE00zkUuNw1VGh7S+Gc13dfYk+S1PVQdchWDj+XYHhAai7Z7IpzXVDCGsDplg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-direction@1.2.0': + resolution: {integrity: sha512-FtRA52a/r+LKStFT+neLUm+8vSe7wbzkbQGGeU8ixFiwC8NU14Evoq9ATECqL3eGbcUzJR4fyfA4KkK/q6dfqg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-dismissable-layer@1.2.1': + resolution: {integrity: sha512-KIfDcAsx+oVylFZ4xryVKusJUrLLhTQmfnJW4kwk4Km5myvhHCrj04hnWWUC166+xD4DHdH8tQGylwYX1h6+Pg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-dropdown-menu@2.2.1': + resolution: {integrity: sha512-XsHcmYG2AcqnvTqrOXXWcMrhquEIlqucizuDH1MV/qMRW+DndpBCMvNeTRXOxqmG1H5j89M/+I0/34Zdd+zDcQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-focus-guards@1.2.0': + resolution: {integrity: sha512-MPm5R0iRGUDrDnPNxmLBkv+X7pN4XKzt+MbWJEuJQsH3NFH4omrbpXpucyYCCKTSt+na/ZxGhEWQrWTZ0285cA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-focus-scope@1.3.1': + resolution: {integrity: sha512-ugeMhbXt4y5S2T1GnIqbWDKEN962PDwnX1fN0eKOOgf5f63Dh2mn8F1FUySlgW0iF6GiVsRF32DzuDIWTUyQJg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-form@0.3.0': + resolution: {integrity: sha512-KnmrCM27m7Jo1R8S6cFDPrEeq20Jfn5dqde0SZrJr2lVXlj6/QZSUCyFoYh1d8ti0uUrMNK5Lx6dUDuINT+f4Q==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-hover-card@1.2.1': + resolution: {integrity: sha512-4QooHxgmpCKRjj1Pft+hETKidYcrbPKcVwMGtk2Oa5DcWY2IH1fHuQt9LG6q5hUQBBV2MhQxSvtx6B/C1xfiTw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-id@1.1.4': + resolution: {integrity: sha512-TMQp2llA+RYn7JcjnrMnz7wN4pcVttPZnRZo52PLQsoLVKzNlVwUeHmfePgTgRluXFvlD3GD5g5MOVVTJCO0qA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-label@2.1.17': + resolution: {integrity: sha512-xmj7MN/drx1oUAXkfiFIvyDmGNUIN/omGobvA5oAoNqu9zO2MkISYGZOkBcqSzs/UZXXwV+/3ACRE7q7mGcmkA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-menu@2.2.1': + resolution: {integrity: sha512-G34BsU90SBj5foYpz8pQqq/Q0Dlfyyl6KitWEfRwXC0QvfwhWNAwS49DWP2+rRJ0iwv20Z2Qx7Sk+T03bGo5JA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-menubar@1.2.1': + resolution: {integrity: sha512-bRTYtS9JITV2dUYqYZb7+N5SooQBkZrNRejfmyrSKe3Y57aXhKgx//kBUP0kY7pEpsWR/U1AHsAJRzjwCrK3PA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-navigation-menu@1.4.1': + resolution: {integrity: sha512-XPPZjkeyCSMNNvt0FyDv/PUauG7Ow4GwHQ/XZSwW36F/TS7rR4HFTsXH8ewLnXTOLn3x96/pmTPNQIVoTX/86g==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-one-time-password-field@0.2.1': + resolution: {integrity: sha512-KtHdcgnaAK6rohQ/MMJoGYcbgSRJsIuGBdHrFeTB1/9SaKlDs14oSOniW6BL6M43HqvgCO3u4WuVdFLkuc6sqw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-password-toggle-field@0.2.1': + resolution: {integrity: sha512-vu4+ZoJAaFjdI7l1lDSZRs+4dEv83Jmu7lyzajPrAeQzX74y84n0rUNWbWSgN9XB5NE8IY86Z02kGH6oRrzR+w==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-popover@1.3.1': + resolution: {integrity: sha512-ZNxvXRx5W2Wos4fXCXb5lpdqN0Z5chf+mStOUOTr4ik7d+62MRvoe9rzCA/eGNg36lxXqSrKEKmwuZEcUrIstw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-popper@1.4.0': + resolution: {integrity: sha512-yCWRvLeGv9Pz3+efwHqGPp70mZ56r3kfIgQVF56Oj2aAa4uP/Tb+IE59wNcc+758rctqbCKx4LbK8II/r7Bn0Q==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-portal@1.2.0': + resolution: {integrity: sha512-StX7xXnaRU41VvFtMm9zlhVGz8XF6GmWWTXftkQaY+tqVkbs/R9UmAp5mLzHHapk5hM9RMZQvO/3usyIP/g42g==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-presence@1.1.11': + resolution: {integrity: sha512-Z7/atlkab36Z8hm+V8zYc79r0W+tiOB1o0tun1By94JyadkekLQmtEAnBSP9XKXBNQsStoStS4+gX10JpS5akA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-primitive@2.2.0': + resolution: {integrity: sha512-HCTSPMNqv55Qk0IX4geJbpyXazmqulcimwuyjxTkKi29v+QZbG2Zho9BnbBq4YS2PF+KlNb2CXoLz66uUmblkg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-progress@1.2.1': + resolution: {integrity: sha512-yQBiM6J3YM8Ums1orkEFpbznK47geKxa0igikzcaS1xHgdba3DdAQBsGbCTt5BVfR8zhEvgxQ0poGcINaD3lvw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-radio-group@1.5.1': + resolution: {integrity: sha512-QbyVT1ddnjqB9H+CUYKkNWdU9fIK3QHCYtjf7mS6ekSqiuXkWtWn5UZW9mhA8qtl3vrVyIsmR9gePstAtKrqkw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-roving-focus@1.2.1': + resolution: {integrity: sha512-UwraORzVUeEPMclhO1p0vz0qI+TbdDLO9i+FiOqhJyHKwdpCRCck9yVEscUnAMIzCiuNoQa8D4A9bIpOPXGt6Q==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-scroll-area@1.4.0': + resolution: {integrity: sha512-CFrLqHNejWq5FhKAcj9V/WPGRsol76HBQto81NdOBVFEOqIEkJkKIJt0Z824A/9hHLCtoiR/Kw3euRgzXP2pxg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-select@2.4.1': + resolution: {integrity: sha512-888ZwW0Md6GyptuqYABRX9nLKgoYfcooCyBBvVaX5Jp3e+8Jp2i4Zo/Uy50Hm+Ysn2JfloQXstW4U36w8teVtg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-separator@1.2.0': + resolution: {integrity: sha512-1jUAap0wZEqVP05Ou7kJgRxe/W+Qftng+rUNWGa0qkKbv9r5pWTG3E7a3ELKo2eJoHKMECnzuYrZpSyO1YgcYQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-slider@1.6.1': + resolution: {integrity: sha512-IRNRR90moPqxfwCJ0XbMJIxo70W0/1ZExT/sNCxEHMaPx0SF9HoQrcPEC75+H28cFKa0b+KH63anfLYJmwKacw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-slot@1.5.0': + resolution: {integrity: sha512-tyYd9ddPXAysAbdbD9eYxgwpCSv6f3PncqVSNKQFSR5lcJVLz3cb/pANDoYrLiHBpXv7skortyUgGUWsYuPPFQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-switch@1.4.1': + resolution: {integrity: sha512-NpBGwc40rY02ZmSvuyeKk0Bvc8ZsNeTMi4qHvJ+ZH4EiYCv0zIXEgdCW4bVdzgIUeP9xxbLJSxKOUfqITYNmkA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-tabs@1.2.1': + resolution: {integrity: sha512-hRoJlode+hg5ZNlR+3tkfkUliIybdTpD5lkAvHQ9yaXseGf7XVO9Ib2GPEIOuaJJbo7YWYbNpDDRfo8Dhf6/xA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-toast@1.3.1': + resolution: {integrity: sha512-iQvL0J7XK4HtJNGkkWMyYrboNseI7vtotFeUkIno8YNxVA9oM7B6//fh0Mc6TSKI96+zioLWi98lQLso88yJmQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-toggle-group@1.2.1': + resolution: {integrity: sha512-O8Ldx5ao4p4KoW+DO8/PmkOe5mdKNi3F2SseqjVj88wvDGDHe60cUmO0Zm/uHsKrvNgQGV8Efpuh95Tht/h4vw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-toggle@1.2.1': + resolution: {integrity: sha512-+3MUq+WyiBplXYqQ0EIYlq8R4sLQ0T5NbOK7DMdDLCKO93pe2uE0DUJTWWfTBprpJJXxfoF3eMW0xpakVZrYsg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-toolbar@1.2.1': + resolution: {integrity: sha512-Cm8m7JTEeUdbfA6T9xgyu8HfOpyvSx8pB/wj0DlEuWaJe9FilIs4Az9wci/lU9RbthV/UkltszMw5WnnD+m/Ug==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-tooltip@1.4.1': + resolution: {integrity: sha512-NxLFGSFtZ78zVfcoOhTwR1xwqo5qx1Jh8Pw6FMZ9McCaqfRzz7/gIRahs2Oy5XlPgQ+8QkUcKbN5B0U44J/kCg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-use-callback-ref@1.1.5': + resolution: {integrity: sha512-Y5GBf4NKeADoXI1geiMlMuMoURT3k9MizzzKhGb/m55H94aGigZ01PIpkvsO6lML13BA6XYAXSJ6gza7vTn5nw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-controllable-state@1.3.0': + resolution: {integrity: sha512-TAcBI7bw2ljYCbv4+zuTSd6zICu8pRt6SDN2vJ/Z06K2MXh6I9WYFe/A9ovlFDtR5HVHGm5iDxDoABm/++dbKg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-effect-event@0.0.5': + resolution: {integrity: sha512-7cshFL8HGS/7HEiHH+9kL9HBwp2sa9yX18Knwek6KYWmXwM7pegMgta2AXMQKI+rq3JnfSj9x8wYqFMTdG1Jgg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-escape-keydown@1.1.6': + resolution: {integrity: sha512-9cRullnQRv+kqnRmFlmh0WC3kAPuGmHiQ9VwsXdrYk//sBhgCKEttcpPEPgIQbyx6TGUmooVEHYTyohwz/lSJg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-is-hydrated@0.1.3': + resolution: {integrity: sha512-umO/aJ+82CpOnhDZUTbILCQf7kU/g0iv+oGs/Q8jw7IkhWBzaEP4sA268PhFAJTFetbwp3ICc6ktpI4TqtxcIw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-layout-effect@1.1.4': + resolution: {integrity: sha512-K20DkRkUwDnxEYMBPcg3Y6voLkEy5p5QQmszZgLngKKiC7dzBR/aEuK3w1qlx2JWDUNH6FluahYdgR3BP+QbYw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-previous@1.1.4': + resolution: {integrity: sha512-XoSLhbRbqxFtgJoi2fNHA3C6pDlY34x508vUpUGoFZfvePfHXHbE1lC4FYFMnJWgiCRroSTw6fOsXQoVS9RwZg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-rect@1.1.4': + resolution: {integrity: sha512-cSOCh6JlkmfjLyNcLiu2nB4v+nm+dkZ+Q5KHWk/soo4U7ZLiEQFKHK9/YmtBHjfCEaU43IBKQOc4/uJmCaiCTQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-size@1.1.5': + resolution: {integrity: sha512-V1McDQkMGzgivwqdu7u96APs7oi0Jvj9LTzgq3WWrDZqxWdAqU9yZnZQqk5OizGFR8badUQwZsrlao7MOlWYOA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-visually-hidden@1.2.13': + resolution: {integrity: sha512-feijiN1oPk4rck4u9Dm9tsU4mT6Wp9oeEEhaz1WUEs1VuqLf8xzsFG+Qk+uWq6TnMH9O+WRECRg0NftyP9E4Fw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/rect@1.1.3': + resolution: {integrity: sha512-JtyZR+mqgBibTo8xea3B6ZRmzZiM/YeVBtUkas6zMuXjAlfIFIW2FgqeM9eLyvEaYX66vr6DJMK+4U6LV0KhNw==} + + '@react-types/shared@3.37.0': + resolution: {integrity: sha512-VNlZuifu4tHdxWPfHvoMfKsHA9kVTCGkduv5vKVTdinm8wHFJNhyVB5RzIMiRFj0q3RKoCbP68u2zaoBSiFueg==} + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + + '@rolldown/binding-android-arm-eabi@1.2.16': + resolution: {integrity: sha512-ia3DwAs0OsMGS7+IConptSiyk9pEXkOx/AbJ2n/lAGxH3QkWQus1t8d7gnAT38FCU2sQu8lUbeR6AiZFv4JxaA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [android] + + '@rolldown/binding-android-arm64@1.2.16': + resolution: {integrity: sha512-kx5wBiMB6E7TEvpl2B9gjLU004BkvEm2Dr941BFjx/SontPwSzNuQUIoVKxHHoSjpFIfHMmcguyLOtyn0hObRg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@rolldown/binding-darwin-arm64@1.2.16': + resolution: {integrity: sha512-qslZrXnKJU4cUpjoAmtAo/KQM9e2IRMlmpig6Zpwj+8KWxW8v+nkAf9216Bsek9lOC1jvg2yCjygCFo2f83msQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@rolldown/binding-darwin-x64@1.2.16': + resolution: {integrity: sha512-FdF+y48ND13eAyM2mQn+85kvAqNlvRWROy2rhgA463X/0Gv4vGtazZtfaF1mRBLscYvtOlh4QnLjOWiFzX69OQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@rolldown/binding-freebsd-x64@1.2.16': + resolution: {integrity: sha512-ddjqFM5e95AgLbUPtKIS7V1KTVlA0RUgCVr5mn+/IEhsdy3pr+Vuj/HZ7nlMN2FyxIwUktOH1LEPIQXO1v9y1Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@rolldown/binding-linux-arm-gnueabihf@1.2.16': + resolution: {integrity: sha512-tHdKWbFkFBI687iim3LIyIvyL7j66VvQYkDPss5Stn/qyE2tWkR1YD0dXmXPFKKqT/o5ZEJVsaRsWS2meQwGOA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@rolldown/binding-linux-arm64-gnu@1.2.16': + resolution: {integrity: sha512-FJiLGAblPBMlBEdbOqw6kW6QcXDWAf/oJu7gYs9ZJZwmg+K+/zjFLxPTIY9izJUzxzc24FHvmseyA0IvJSl17w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + + '@rolldown/binding-linux-arm64-musl@1.2.16': + resolution: {integrity: sha512-hrJpblMsrQ58U9veWpbXzkhV5zStunds6HWLprqq1qauvgBr1E6P+24kLj6lSLI9vjRn/RYpjprhkHQPKGWpNg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + + '@rolldown/binding-linux-ppc64-gnu@1.2.16': + resolution: {integrity: sha512-qJd/dC6j6CwLG9/s/RP2nb1RZ0GIgj7Ioj7x/SYa3RzjVrlzrmH0fhIOpfvqQDeIsdP0loi+kHaMhJzY1PBIaA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + + '@rolldown/binding-linux-s390x-gnu@1.2.16': + resolution: {integrity: sha512-Vx7Xg5uUJuNcujJjI70OICaH0EYj80BtOLZTSN7uOYg8QOLlyKnaG0IW7Fn/J0IpJsaKzmDKMhOQv1JBM9m9vg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + + '@rolldown/binding-linux-x64-gnu@1.2.16': + resolution: {integrity: sha512-TNKhgE3uJTsTzatUSSYt0Kgo+Oa2uSlSjz4r1aEIea7HX6ULBym4fAlYUhG1i6pjNf2njfCUAfKwr6C3E9Xg1w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + + '@rolldown/binding-linux-x64-musl@1.2.16': + resolution: {integrity: sha512-7chXdpjXZ8qG0+mIxECN3cT9kvhZ3/oZhMX6Pp876P7CDrIy8nJswva6Mq357JR7eYXfuskwZxMeV1SzoZdHrA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + + '@rolldown/binding-openharmony-arm64@1.2.16': + resolution: {integrity: sha512-Kwjt+7uNTJK5/MDkGWGVO2KQPnj8yAV75dMpfwyHZrJSXWHrniOB0aUEWlVYf3H1hOqFLIStLdM5QXq6bnKmOQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@rolldown/binding-win32-arm64-msvc@1.2.16': + resolution: {integrity: sha512-/LH6WfjDRXXJz/p5xl4TmU+jpcnwcxbKEQBW/GZE0JHfN0lGIyTtsUEGaQ/q/GikkGJ2yLKe6pwm7AnEWO8pdw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@rolldown/binding-win32-x64-msvc@1.2.16': + resolution: {integrity: sha512-1lv1KvX0Z871zfk7vw+dnXMMnHNyDwMzWG8Kv3HxDXMOhzCX4SHc7HK319fInP0i+0p9D8brkfzL7pJOaGl+tw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@rolldown/pluginutils@1.0.1': + resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==} + + '@sindresorhus/slugify@3.0.1': + resolution: {integrity: sha512-N1oPvN6sytRKZCskv0L+vSyvnUYi9JplPXZ90lT6EBsijjHrUdoXttb0Lna6pw1TWMe8vlajQnLDtU9KYHqtDg==} + engines: {node: '>=20'} + + '@sindresorhus/transliterate@2.3.1': + resolution: {integrity: sha512-gVaaGtKYMYAMmI8buULVH3A2TXVJ98QiwGwI7ddrWGuGidGC2uRt4FHs22+8iROJ0QTzju9CuMjlVsrvpqsdhA==} + engines: {node: '>=20'} + + '@swc/helpers@0.5.24': + resolution: {integrity: sha512-6tcXo4kA8HgGO7AVmawB57RYi/piBPYVHVcO6dXjX+SZw8GoHmi262sp4v2Ha2kNP9Cx84qei27yT+fjS1lxgg==} + + '@tanstack/query-core@5.105.3': + resolution: {integrity: sha512-v1rbNy7MB/4Akth7CQH+CerV13K7Ok9hAusEsfz4sCjDdu3/kOSJPoWfoB+YjdEX1eBFBWKNAGE5nqZCgdlHDw==} + + '@tanstack/react-query@5.105.3': + resolution: {integrity: sha512-dj5hGWZmwoc9NG4enoXuYbiYwb5FxgJYPVv06eUgicMTfEMTRXn08Xv9D/r1V8TfjAv+BpxdvZFzJ1h3hSWZcQ==} + peerDependencies: + '@types/react': ^18 || ^19 + react: ^18 || ^19 + peerDependenciesMeta: + '@types/react': + optional: true + + '@tanstack/react-store@0.11.2': + resolution: {integrity: sha512-oj5O5DmiMWE+ih+SioX2ayC1kbK7d3fpLx0B75fPJr9nY3zU+hoW010yr64qXsY/9fyiLVE2bn5mGNV3owm8RQ==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + '@tanstack/react-table@9.3.1': + resolution: {integrity: sha512-E6Ya5TnY6EaGyZYIUqig/29Lr/gJSAubQkLJFqiWkUl5nRCr0Xr9hvJot+NiRTtG+TzGxHTiqUW3bnLtQxlHug==} + engines: {node: '>=20'} + peerDependencies: + react: '>=18' + + '@tanstack/react-virtual@3.14.26': + resolution: {integrity: sha512-HUDlfsrYH6ko5xH56L62Mcl/uzT+AGVNMHFuDNqDJMdZWc1AQa2oeVKNUvZ/RWBZQ4TYtyQerFrNvLnp39h65A==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + '@tanstack/store@0.11.2': + resolution: {integrity: sha512-sJ4mjol8uQsHV0gOJzzjwXfh2Fwm+Sz0+8deqiTm4jGbMdjzNSW+xZCFm0kUa870uhd8yi+DpKnZb5Kc4apa0Q==} + + '@tanstack/table-core@9.3.1': + resolution: {integrity: sha512-zQvlxSNi2qGM3L4jh85Z6hqMfdIR5qOfD9KHOU32nuKFJGN0RlJ7UvV1lsPb1fPVhpFYI0VP3jJGxLdLDpTKjQ==} + engines: {node: '>=20'} + + '@tanstack/virtual-core@3.19.11': + resolution: {integrity: sha512-/5iHb0RHBvj8Di59FDs4ESNm45jls33aZAi4zccVXZjc/6ZvwQP6nzEYeT6weXjTf9E6/vrp2JhYWRyO+ZP13A==} + + '@testing-library/dom@10.4.2': + resolution: {integrity: sha512-yzr2S9HyAIdhz2/6qHgbs665Q7PKVcDF05vsOlHPxG1mo36gKVesdYVeDLnXgfjJ03CrKRk08knc6+E/9m8v2Q==} + engines: {node: '>=18'} + + '@testing-library/jest-dom@7.0.1': + resolution: {integrity: sha512-oMDTC3oA+6CXSO2JZnvOI7CA6oVub6kij5ggk9ohwye5slmkwxYDXcPOVxgMw/RQlticjtO0C1RZkR97HgrWMw==} + engines: {node: '>=22', npm: '>=6', yarn: '>=1'} + peerDependencies: + '@testing-library/dom': '>=10 <11' + vitest: '>= 0.32' + peerDependenciesMeta: + vitest: + optional: true + + '@testing-library/react@16.3.3': + resolution: {integrity: sha512-Uo193NgQbPMz6lrrhtRQQFcMC6Re/ELLFbbuVL30WDlZxlpZf9/lMHTAVxPRLw1q1iu9OJmR1c2BLiENRstdBg==} + 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 + + '@testing-library/user-event@14.6.8': + resolution: {integrity: sha512-vgSSqnANMnkcYR/vUNDbwS7aM3OErRLNUG8vCVVlj4oc6cKffUrccX7UAS5AmJQQNZTSibi9z5GvvRZ0aYgUCQ==} + engines: {node: '>=12', npm: '>=6'} + peerDependencies: + '@testing-library/dom': '>=7.21.4' + + '@types/aria-query@5.0.4': + resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==} + + '@types/chai@5.2.3': + resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + + '@types/deep-eql@4.0.2': + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + + '@types/esrecurse@4.3.1': + resolution: {integrity: sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==} + + '@types/estree@1.0.10': + resolution: {integrity: sha512-lTyOMnOcmRBzdCv6/4zUZCrEhV0xrqQiUZBZtVcbmLHBXKLnN8q89v/wKRF/Q0HbwVsqbIG3o5HDoFl9sduM+g==} + + '@types/json-schema@7.0.15': + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + + '@types/node@26.6.6': + resolution: {integrity: sha512-MzQj+8Kx3XI40OmUAE6Qoh58ZMNstNNMwn2nfd3qWsfe7nDNSqJNzes4njhz6AHwrSHOffSLwXU6yb474Q4aog==} + + '@types/react-dom@19.3.0': + resolution: {integrity: sha512-ZI7bU42mZXXKHn/qNLEw2IrbiINU7X5+vfgdixBHkCNpYWXjKgfQ/P+uyGb5CjOLB9UcnTeg3rylQtV2hym44Q==} + peerDependencies: + '@types/react': ^19.3.0 + + '@types/react@19.3.0': + resolution: {integrity: sha512-N0rFCuH9YoxG9/m61l9MfpJKfmLOVU0em7ipIz6TRgSSkvReLB9vL85GB+yr8Bs5leqpvg96JSwF4ZS1s4viQg==} + + '@typescript-eslint/eslint-plugin@8.72.1': + resolution: {integrity: sha512-5CokTWXpmbylfFabsbvHC0CAw3XLaGnoa+nFJX5FR592h/LoZsENUE6p/rg5xvkKrFEXz6pYmgI29AgXc2N3BQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + '@typescript-eslint/parser': ^8.72.1 + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/parser@8.72.1': + resolution: {integrity: sha512-ZGwZP4OCfyA0AQTpEAMWz+rtX01DRhQWW5hB0ucKDEIX5U7jdmPSaMx94rQfaPAQzIYz2BimOgKOJtjfpRFQ9g==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/project-service@8.72.1': + resolution: {integrity: sha512-fAuVMkwdO/huI8nyo/WKnNKf6Gag93S56Rh3Ts5mGcavwSdaN8tRg1gCeIGX8PF+nzp1BjGfO4gHhGzW1J6jTg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/scope-manager@8.72.1': + resolution: {integrity: sha512-lC/t6eMCZZQAAKZEYGL6RDVSuFehpdmMT6h+nJIjIa6oylN/PwDgDVtEIbB7O9xFfZYtZAYLoLlDNWKJhtF+uQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/tsconfig-utils@8.72.1': + resolution: {integrity: sha512-e4D5R5vpeAEPBxT2BAzsFQL3sbwyeeeo5+aczgNSRK1V734jE7pTqaGFU+fXapWEQwnZbpFaTn32+Z8DwS6OTg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/type-utils@8.72.1': + resolution: {integrity: sha512-iY2wlmyrXJmx4yxUGP3aYCFF3cA/6ShddOKmLU2dx7LnEykbAM8WThlxZEfd1NddqceF6eJmZUayxx9oKIYNlg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/types@8.72.1': + resolution: {integrity: sha512-LmE5IhQet1BWOWEXqBrVtBPsMAOQ6Nzk35d+WLh3mk4EUF33sqlfxjio1MdYeSvKSG5skactKKuixBIKXhL4Rw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/typescript-estree@8.72.1': + resolution: {integrity: sha512-DAlUoUV93hIvrtIQLwCbmZQYVZwOMhiHJrOVUUIWicFZaIYjTRqlY3I/NZevfkLmSqkNHRu2qYDcq9lrn16oOQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/utils@8.72.1': + resolution: {integrity: sha512-5v5liypurJBFHCBNqh/RjsymAD+VWcE0djuGY/BUqp5fC3Qn+luWDEksjuTtWjPn6i5zQlUtkZCRG/CYG0yCkA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/visitor-keys@8.72.1': + resolution: {integrity: sha512-SB8xqipSzZHUePCCLNeYxqnxq83X61/qzNhjQGbTfjxGZIeOEP7tBgWYX05Tcd6CgE02WdTJnh5b8kMZJ/+bnQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@vitest/coverage-v8@5.1.0': + resolution: {integrity: sha512-OY9aShBQG5KOIw2zCo9fDoCHI9RHliNHJ//nM7m6jGg41Hyke9JOGWmeMlK74xYErsxMoK8mIB3gAeEyFVA7LA==} + peerDependencies: + '@vitest/browser': 5.1.0 + vitest: 5.1.0 + peerDependenciesMeta: + '@vitest/browser': + optional: true + + '@vitest/istanbul-lib-coverage@2.1.0': + resolution: {integrity: sha512-WOjuS+xcjSIy7ruYBQ+9BUcV7XZQ0FyVcbWbWxW9TRx/kLGx5SQaeFRIFDJxjNplb6YsT7SpKo2zuXXUo91OIQ==} + engines: {node: '>=22'} + + '@vitest/istanbul-lib-report@2.1.0': + resolution: {integrity: sha512-+ZFta25nWtGCOBG9/IET8w1RO5Oawsd6H8Uf6AJ9EA/StFuKVtKAEJEJ6uRt84OH+T4YkI0ASmIHAhRA+0KVxw==} + engines: {node: '>=22'} + + '@vitest/mocker@5.1.0': + resolution: {integrity: sha512-5umB9/jlWcqVS6fsrFmv5Gdnpu6wAUio/lEOwAmvB7v+lFo/IW2B9FhAD0p19GCULTgMzLCdUmuCDCnAzWgppQ==} + 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 + + '@vitest/spy@5.1.0': + resolution: {integrity: sha512-2DPO5OMHWKrBngC+fCmTQqemNF0gidRhp3lxbBmRhW+M1XJrfgPpqNqWVevXtn2f2ufD5ZaR1B6B641aAlcX5A==} + + acorn-jsx@5.3.2: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + + acorn@8.19.0: + resolution: {integrity: sha512-oJlA3XiRm7Cyk6qFD2Jn8ak9B7jSy0qy00ADO3+8dpT0LSjFihQYv4C02LFCSYJV3Q37xYwwRy5m+IpIiUzqWw==} + engines: {node: '>=0.4.0'} + hasBin: true + + ajv@6.15.0: + resolution: {integrity: sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-styles@5.2.0: + resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} + engines: {node: '>=10'} + + aria-hidden@1.2.6: + resolution: {integrity: sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==} + engines: {node: '>=10'} + + aria-query@5.3.0: + resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==} + + aria-query@5.3.2: + resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==} + engines: {node: '>= 0.4'} + + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + + ast-v8-to-istanbul@1.0.7: + resolution: {integrity: sha512-kFL68AG6ajd8fg248zwM9GQrUWEp79gsmjum34OEXjs4yHuUMZfYKwOLW9GMmB4oNvVrj+EAGxsP7ye2UR9UlA==} + + balanced-match@4.0.4: + resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} + engines: {node: 18 || 20 || >=22} + + baseline-browser-mapping@2.11.35: + resolution: {integrity: sha512-GNk8KX52CHqeNKGk8+7tZnQab/2k1t/BgrUHPKNdijzpEhF1saPQZHVKb6m5loSAxB39gyrTYw34Qf9CQ3u1qQ==} + engines: {node: '>=6.0.0'} + hasBin: true + + bidi-js@1.1.0: + resolution: {integrity: sha512-fX1Onk0tdVPC7obPWB5EbJ1z7NVhLq4m2xZLq2YXBkxzMXIGRpNMU88n0EPgWseKl12J7zXs7qrDxPK4sRs2fg==} + + brace-expansion@5.0.12: + resolution: {integrity: sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==} + engines: {node: 20 || >=22} + + browserslist@4.29.5: + resolution: {integrity: sha512-khTmKu5NGYTotqcYo8Bew2k4RAE9PuXRSFRXG2lQBrCEQHyeQ/vX6gKMxfokwywBsLiXZMnHsmWMG1pnO8J7uQ==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + cacheable@2.5.0: + resolution: {integrity: sha512-60cyAOytib/OzBw1JNSoSV/boK1AtHryDIjvVBk7XbN4ugfkM3+Sry7fEjNgPMGgOjuaZPAp8ruZ0Cxafwyq9g==} + + caniuse-lite@1.0.30001820: + resolution: {integrity: sha512-gfWD6MxqVCqAOwmODfZf7DchWvlrA+SRdNX7ceLfg3xRqe5ssjjpF7EgslC/0tiLiNTajdaNN/q8c/5u/7Gz8w==} + + chai@6.3.0: + resolution: {integrity: sha512-XWAtwJ6OHO+tj0EKCs0Y2UamnyOxseZWltU4x2U2wh8g4AigdjwvtUjvLP2tqkA/avxHEtzxNaqGq/YGNwckKg==} + engines: {node: '>=18'} + + class-variance-authority@0.7.1: + resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==} + + classnames@2.5.1: + resolution: {integrity: sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==} + + client-only@0.0.1: + resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==} + + clsx@2.1.1: + resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} + engines: {node: '>=6'} + + cmdk@1.1.1: + resolution: {integrity: sha512-Vsv7kFaXm+ptHDMZ7izaRsP70GgrW9NBNGswt9OZaVBLlE0SNpDq8eu/VGXyF9r7M0azK3Wy7OlYXsuyYLFzHg==} + peerDependencies: + react: ^18 || ^19 || ^19.0.0-rc + react-dom: ^18 || ^19 || ^19.0.0-rc + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + country-flag-icons@1.6.20: + resolution: {integrity: sha512-py8JiEKzjhYw6HPJ0L7SxLgCYim36UPRTZX43/kqGueUCZLSvnrqAiwW8HtQibur7mdkFQUkjOgdK+o/9FBtaw==} + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + css-tree@3.2.1: + resolution: {integrity: sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} + + css.escape@1.5.1: + resolution: {integrity: sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==} + + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + + data-urls@8.0.0: + resolution: {integrity: sha512-JQUCCK+/CAGbGGx618PlFZMXH81Jtj7t73CHv9dd/zVWvxgGT2zLFRjnKb+Ng46GIwcy+f7f88qd4duqllK+WQ==} + engines: {node: ^22.14.0 || >=24.0.0} + + date-fns@4.4.0: + resolution: {integrity: sha512-+1UMbeh68lH1SegH83CGWwpb6OHHbpSgr3+s5Eww5M4CAgswBpoWS0AjTOfEJ33HiYKz1hdj/KTFprzXHmq/6w==} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + decimal.js@10.6.0: + resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==} + + deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + + dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + detect-node-es@1.1.0: + resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==} + + dom-accessibility-api@0.5.16: + resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==} + + dom-accessibility-api@0.6.3: + resolution: {integrity: sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==} + + electron-to-chromium@1.5.475: + resolution: {integrity: sha512-S9tceHJbIsHH8VRwh9x+8lJO50SYwTdwVGdEjdz2FERyP59j1N6rdXOSklIusrhxt9GG/WN4V/kRgmDHl9MM5w==} + + embla-carousel-react@8.6.0: + resolution: {integrity: sha512-0/PjqU7geVmo6F734pmPqpyHqiM99olvyecY7zdweCw+6tKEXnrE90pBiBbMMU8s5tICemzpQ3hi5EpxzGW+JA==} + peerDependencies: + react: ^16.8.0 || ^17.0.1 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + + embla-carousel-reactive-utils@8.6.0: + resolution: {integrity: sha512-fMVUDUEx0/uIEDM0Mz3dHznDhfX+znCCDCeIophYb1QGVM7YThSWX+wz11zlYwWFOr74b4QLGg0hrGPJeG2s4A==} + peerDependencies: + embla-carousel: 8.6.0 + + embla-carousel@8.6.0: + resolution: {integrity: sha512-SjWyZBHJPbqxHOzckOfo8lHisEaJWmwd23XppYFYVh10bU66/Pn5tkVkbkCMZVdbUE5eTCI2nD8OyIP4Z+uwkA==} + + entities@9.0.0: + resolution: {integrity: sha512-oJFNDJg3kTPwAD1cNn2+WmUq4wFoo09npc3Ydf1D7ED4smRiWrsKnaLxykrTWgyGEcusc87P5dxlAleol9kpkg==} + engines: {node: '>=20.19.0'} + + es-module-lexer@2.3.2: + resolution: {integrity: sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==} + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + escape-string-regexp@5.0.0: + resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==} + engines: {node: '>=12'} + + eslint-plugin-react-hooks@7.1.1: + resolution: {integrity: sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g==} + 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 + + eslint-scope@9.1.2: + resolution: {integrity: sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint-visitor-keys@5.0.1: + resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + eslint@10.14.0: + resolution: {integrity: sha512-T0pNsxkb9IKmtQjhpK3d7nWOpFv+YHuMbTJnSVMoQblF0onvXfErrtp4V1jHtsQPan7dKaGVD0q1WCXnzMF31A==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + hasBin: true + peerDependencies: + jiti: '*' + peerDependenciesMeta: + jiti: + optional: true + + espree@11.2.0: + resolution: {integrity: sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + esquery@1.7.0: + resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==} + engines: {node: '>=0.10'} + + esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + + expect-type@1.4.0: + resolution: {integrity: sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==} + engines: {node: '>=12.0.0'} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + file-entry-cache@11.1.5: + resolution: {integrity: sha512-+PFTHITI08JIGhnNpGNI8T8inUpgZfk3GNEqfT9R2zZV2iFXg3CvqzSl/uEhs7TSGujYRELEANyDvS8Fj7+S7Q==} + + find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + + flat-cache@6.1.23: + resolution: {integrity: sha512-f++BY9pTk+983xK1FLzlLpmM0i0z+jHmx3QESGkURMXujQZz1k5wzwX6hjnQ8goaD0B+sYnDK1yZ6MTyZfUaqA==} + + flatted@3.4.4: + resolution: {integrity: sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + + get-nonce@1.0.1: + resolution: {integrity: sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==} + engines: {node: '>=6'} + + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + + globals@17.14.0: + resolution: {integrity: sha512-UtdkTVyAJePQv0J2M9Xe4L0ruNSDV314XiIJsADI2Zf40bcdAonE/D/OhAU8AT99oij9mhLByb+NvA3QWZYgCg==} + engines: {node: '>=18'} + + hashery@1.5.1: + resolution: {integrity: sha512-iZyKG96/JwPz1N55vj2Ie2vXbhu440zfUfJvSwEqEbeLluk7NnapfGqa7LH0mOsnDxTF85Mx8/dyR6HfqcbmbQ==} + engines: {node: '>=20'} + + hermes-estree@0.25.1: + resolution: {integrity: sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==} + + hermes-parser@0.25.1: + resolution: {integrity: sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==} + + hookified@1.15.1: + resolution: {integrity: sha512-MvG/clsADq1GPM2KGo2nyfaWVyn9naPiXrqIe4jYjXNZQt238kWyOGrsyc/DmRAQ+Re6yeo6yX/yoNCG5KAEVg==} + + hookified@2.2.0: + resolution: {integrity: sha512-p/LgFzRN5FeoD3DLS6bkUapeye6E4SI6yJs6KetENd18S+FBthqYq2amJUWpt5z0EQwwHemidjY5OqJGEKm5uA==} + + html-encoding-sniffer@7.0.0: + resolution: {integrity: sha512-UikN5yr7xsCDAq87Or5or0PAlD3HJJOKVzM05az588WnpDJ4Ux7a2A53Qi6gofGg2/EtvF/H4hCi/TXfCW4Y6w==} + engines: {node: ^22.13.0 || >=24.0.0} + + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + + ignore@7.0.14: + resolution: {integrity: sha512-wCBiZ77u1tuWde6G/nfOtLaCTYcFDU5yCzARzK10y+rLhzYoL7a7yEemcopcc2Mzow7gZOIjnV2ym1NYmdiJOQ==} + engines: {node: '>= 4'} + + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + + indent-string@4.0.0: + resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} + engines: {node: '>=8'} + + input-format@0.3.14: + resolution: {integrity: sha512-gHMrgrbCgmT4uK5Um5eVDUohuV9lcs95ZUUN9Px2Y0VIfjTzT2wF8Q3Z4fwLFm7c5Z2OXCm53FHoovj6SlOKdg==} + peerDependencies: + react: '>=18.1.0' + react-dom: '>=18.1.0' + peerDependenciesMeta: + react: + optional: true + react-dom: + optional: true + + input-otp@1.5.0: + resolution: {integrity: sha512-3AcfdW1sNG0FmSA5hHMBXG7jNW5CdcdKs8ln8JOmn6S2SRkoXoJx+2UwC+ZvjflfnOdHJx0SfQ9/YjlxVYLtGQ==} + peerDependencies: + react: ^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-potential-custom-element-name@1.0.1: + resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + js-tokens@10.0.0: + resolution: {integrity: sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==} + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + jsdom@30.1.3: + resolution: {integrity: sha512-hQMUPvRgN7KxsfIytDQji7bimuJeRQa6Qo5+iuiZQk5CafeRxLIA10/cG5YCBFNpD9RO3oqIiN4CO1Y44kxqXg==} + engines: {node: ^22.22.2 || ^24.15.0 || >=26.0.0} + peerDependencies: + canvas: ^3.2.3 + peerDependenciesMeta: + canvas: + optional: true + + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} + hasBin: true + + json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + + json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + keyv@5.6.0: + resolution: {integrity: sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw==} + + levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + + libphonenumber-js@1.13.15: + resolution: {integrity: sha512-AHdhEQeDFEJ4GKDAQ9AwxChy+v2kUILUB3TGQRpkXMkWePjXTU/WQEzWVQoIx/Gz8fE73Wr5SEuZCy2vk8jrMQ==} + + lightningcss-android-arm64@1.33.0: + resolution: {integrity: sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-darwin-arm64@1.33.0: + resolution: {integrity: sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.33.0: + resolution: {integrity: sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.33.0: + resolution: {integrity: sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.33.0: + resolution: {integrity: sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.33.0: + resolution: {integrity: sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-arm64-musl@1.33.0: + resolution: {integrity: sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-x64-gnu@1.33.0: + resolution: {integrity: sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-linux-x64-musl@1.33.0: + resolution: {integrity: sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-win32-arm64-msvc@1.33.0: + resolution: {integrity: sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.33.0: + resolution: {integrity: sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.33.0: + resolution: {integrity: sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==} + engines: {node: '>= 12.0.0'} + + locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + + loose-envify@1.4.0: + resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} + hasBin: true + + lru-cache@11.5.3: + resolution: {integrity: sha512-U4N8FgzmWxc8k1VH8Kr6lQg18U7Fjvby6wXHVRX/ZZ7IwWbRMgrRbP0Wrb5q5NVinryp4SQampHKdvtecItxUg==} + engines: {node: 20 || >=22} + + lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + + lucide-react@1.68.0: + resolution: {integrity: sha512-9i7NKWm2QlOprMhcwkhBOlIt9zB7cp2GIyFY+oMue0EsprolUFxva5rkdBbyfs4k1eejpa0JRnRgVOGiart+Xg==} + peerDependencies: + '@types/react': '*' + react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + + lz-string@1.5.0: + resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==} + hasBin: true + + magic-string@1.4.5: + resolution: {integrity: sha512-oLLC2+YvQUfAa+vxoufE/yc1icb87M9iXIRqAoR/2EhE/8cb3SF/+Gi0c2By1B+5Vz2e2vQtUvHiu1+63Znh+w==} + + magicast@0.5.5: + resolution: {integrity: sha512-UicdXN8zQ3JHlxVq+28afMXPr1z7WNY6+7EJnzTdQWkTAlMLF5fNCCKxJHBQwGaNGR11581EiQmQzx73+MvszA==} + + mdn-data@2.27.1: + resolution: {integrity: sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==} + + min-indent@1.0.1: + resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} + engines: {node: '>=4'} + + minimatch@10.2.6: + resolution: {integrity: sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==} + engines: {node: 18 || 20 || >=22} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + nanoid@3.3.20: + resolution: {integrity: sha512-uKdg2G3GNCKQn9byYOpxbGqrT2fGO5KRt5J/8b3pok8rT6qxGWF6hxMyJiEYtAf+FVyYuD9hRaDqX5uPFYJ4ZQ==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + + node-releases@2.0.63: + resolution: {integrity: sha512-6li2QanLsAXpIH2R6j77MzRyvOyrvdTqlKtO4CgBdHGNvN99ZZfyxszHc5o5+IZBWD5u6Hzwg83JoGXmqZNG9Q==} + engines: {node: '>=18'} + + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + + obug@3.0.0: + resolution: {integrity: sha512-5vvB5+W7ePv+p3uqxi+RcW1XAzLW0/hxt3/4X4Lc4qHudzOhmBiBwOY6DRob4WnanAEGvNcLjF+KNOufrUoEQw==} + engines: {node: '>=12.20.0'} + + optionator@0.9.4: + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} + engines: {node: '>= 0.8.0'} + + p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + + p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + + parse5@8.1.0: + resolution: {integrity: sha512-sq3qBFpee5jeocbFbO48VHlGr8sT7mbXS0vxlKN6sottBtJzrOG3998sr0llSZcQAGgfpdmEYAIZE0P9z0dE8w==} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@4.0.7: + resolution: {integrity: sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==} + engines: {node: '>=12'} + + postcss@8.5.32: + resolution: {integrity: sha512-LJlKCCAUFKOuZxipUdYw1NzIITEsPcgyOG9LkvRJvuKj/bRzbuRY0ARXvL0pWGpsZvn8jku+LMep2eU0seHp8g==} + engines: {node: ^10 || ^12 || >=14} + + prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + + prettier@3.9.10: + resolution: {integrity: sha512-skGHTH1Yb9QvaTC4hWnRTzEYtCpApF0UMBPz+t/0ZPzP8ihefo/btKd+QA7pUE0fEwA1SbS/NmhyEcstdXOIdA==} + engines: {node: '>=14'} + hasBin: true + + pretty-format@27.5.1: + resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==} + engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + + prop-types@15.8.1: + resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + qified@0.10.1: + resolution: {integrity: sha512-+Owyggi9IxT1ePKGafcI87ubSmxol6smwJ+RAHDQlx9+9cPwFWDiKFFCPuWhr9ignlGpZ9vDQLw67N4dcTVFEA==} + engines: {node: '>=20'} + + radix-ui@1.8.1: + resolution: {integrity: sha512-HPfpVGizO9xANlsFSauruLhRe/3+iOt3vPX0MXYle/UQi3gorQrSYtBcdiv0BZYewcl2rvL59qrIuLFMGAmUrw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + react-aria-components@1.22.1: + resolution: {integrity: sha512-UTHaYHOaM71bOCjGPF50DcLywlrwAodE63IP64LB4kO+OizatQEL8TesVEOM8+TkBeSgHoflJ45388IlJfa7aw==} + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + + react-aria@3.53.1: + resolution: {integrity: sha512-dikisyOjJd3D2fFrIDWmVWzqiLz5B86GzZv1zxs7xI4dVc0JIz/1Wa52I20gYvVL2WXeFXlygYsle/QcOMhtcg==} + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + + react-day-picker@10.0.2: + resolution: {integrity: sha512-QetiKtZWE9Uaw/pM/SYNv6WPYOTRnSJ2puL0GgwkmUFMIwiY0dtjH1W4B7DOVXVNQM0ZOClGpH/heOfBSzWeiA==} + engines: {node: '>=18'} + peerDependencies: + '@types/react': '>=16.8.0' + react: '>=16.8.0' + peerDependenciesMeta: + '@types/react': + optional: true + + react-dom@19.3.1: + resolution: {integrity: sha512-Y0bARz53lhEC1NMCASs0/j+faB/zqt3R8hpvg1e8+61ZVJ4UL0UdC4cBbXfZMcIuhrfgeILTLe+ZeBCavQPtKQ==} + peerDependencies: + react: ^19.3.1 + + react-hook-form@7.91.0: + resolution: {integrity: sha512-5BEBUnCBwa28AFXYTYmOaB/+CSpJ/+CWVASHEvIGUzFdHyUiPHeRhG7r5hOZ8nZi0yP7pbHdFNXO/p31EKTJFQ==} + engines: {node: '>=18.0.0'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17 || ^18 || ^19 + peerDependenciesMeta: + '@types/react': + optional: true + + react-is@16.13.1: + resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} + + react-is@17.0.2: + resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} + + react-phone-number-input@3.4.18: + resolution: {integrity: sha512-EmPhJFSxQB7CTmLAlQfGAQQPF+WmJECU08gjyUt50zd2iESb4ETsm6z+BRfUEjH+TXct4EWvJvub+OztJxb/Cg==} + peerDependencies: + react: '>=16.8' + react-dom: '>=16.8' + + react-remove-scroll-bar@2.3.8: + resolution: {integrity: sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + + react-remove-scroll@2.7.2: + resolution: {integrity: sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + react-stately@3.51.0: + resolution: {integrity: sha512-YJcmkAXu2XOQP97hO/qz/cC3qv9kni3oVhwBlxWr358BHePJUpY15XwL3YSRMKxYt4EkslPqAz11JRirMfANGw==} + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + + react-style-singleton@2.2.3: + resolution: {integrity: sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + react@19.3.1: + resolution: {integrity: sha512-QNhWdt7nrgVWWQG58QuGsAR3jv3Hbe4mb4vz7w2csnHavciZJgda1CPNaldF6eVxv90ud3jUXpKlHL69gO38yw==} + engines: {node: '>=0.10.0'} + + redent@3.0.0: + resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==} + engines: {node: '>=8'} + + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + + rolldown@1.2.16: + resolution: {integrity: sha512-bWvlMpFKiTfDKfnK7FC2MPgPeWCf/Kpwsq49ApdW+FQaBZb6O6dZ4MAPbPUjp7hYQrw/RobqCzn64BRrQecGHw==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + + saxes@6.0.0: + resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} + engines: {node: '>=v12.22.7'} + + scheduler@0.28.0: + resolution: {integrity: sha512-juorfCmIkIw8tT+p5BXSm6PJjQF/ycEYmKyzURCIt/RaZIhL+PulbQ9Yu2z1HdOJDdqDTlxA1+xKBmHXJsczAw==} + + semver@6.3.2: + resolution: {integrity: sha512-GYrCADwpu6yV5OAvchkQ27Z5wwMxWAEMg5NNWjyHh/5DboFFqbdbvYNIvZouQpxXyeMPoTOh0vdPYKlf0ZswdQ==} + hasBin: true + + semver@7.8.6: + resolution: {integrity: sha512-9lpSSU4Mly3mrtruwLs1eLK6+gzbzqeBFwRNjIjE+9LuDpWdYctrQlQ3ej1Ilgi7gknpsTRnPoLP8DpCHqHIJw==} + engines: {node: '>=10'} + hasBin: true + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + sonner@2.0.8: + resolution: {integrity: sha512-UM/ByIoFra8yzV75n1o0Puu0bw5U/9UNnDacrJNspekBewIfsQ3D6ez1nvlWpt7aTsO6rujQtifBpycwIivqlg==} + peerDependencies: + '@types/react': ^18.0.0 || ^19.0.0 + react: ^18.0.0 || ^19.0.0 || ^19.0.0-rc + react-dom: ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + source-map-js@1.2.2: + resolution: {integrity: sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==} + engines: {node: '>=0.10.0'} + + std-env@4.3.0: + resolution: {integrity: sha512-OtU/EgQ1kIm5KwqQpBC6ZEMXrZRui11w8zgfTWp8cdO9B8OaPsbA8bTHO2P+HNo1VlUTGMVBwPhydu6poeXiag==} + + strip-indent@3.0.0: + resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==} + engines: {node: '>=8'} + + tailwind-merge@3.7.0: + resolution: {integrity: sha512-XPPUyAc+cvspz3lHTcR/QgPfW2A0lv/xQNIjX3HGhLR+Nq2lHaLq5MtTesHn8GUr3W3DguT2KT5x3NVgRtYwmA==} + + tailwindcss@4.3.4: + resolution: {integrity: sha512-PCJZTFFdG+sL3gvnP1Do9o6GbkvH2q0p04xSs3f7cSvmAeK+sttG+/7ncK1fdRwKFfC7n+wDDJaOMBSefkI9ow==} + + tinybench@6.2.3: + resolution: {integrity: sha512-Al01C2JIBnO2CXCAPbpAl6sUiOZHeThGtHYNTa51JRw9PyWOCdr+9Ong05R4qg/Sk3+H645E8ov1h2c58ityNw==} + engines: {node: '>=20.0.0'} + + tinyexec@1.3.1: + resolution: {integrity: sha512-GCvB3aoys96IuDFBMcTB46JOR6mdMtAToqwiW8JlWhsoh1mhHi/xn9ss/Dg7N555GiJyEt2qzoG/NHCwM6h1EA==} + engines: {node: '>=18'} + + tinyglobby@0.2.17: + resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} + engines: {node: '>=12.0.0'} + + tinyrainbow@3.2.0: + resolution: {integrity: sha512-LgO3D9yZJjApUiuUfl9iFAwrtaX4+lok3wJIqttGoKCHlWUqHqbQpnxCf82L8FjgKsh4iGo78hqJwgL8F6To2A==} + engines: {node: '>=14.0.0'} + + tldts-core@7.4.20: + resolution: {integrity: sha512-C630TWXZCZmLq0F2bW4QqwnaaxCQ/svpAqvJwnC4JvgEM5ohhBnGsEzTLSOXcN+xjz+kEaHS5uoWIw2klnqd0A==} + + tldts@7.4.19: + resolution: {integrity: sha512-48dH6aSpQnpbzDUc4vIQZ1VjB8tkK+e8c1yx05/SZK+rkssgjEOlxDd/yU8/a4vGYQ9vRgJr9dwMq9TJ0b3X2Q==} + hasBin: true + + tough-cookie@6.0.2: + resolution: {integrity: sha512-exgYmnmL/sJpR3upZfXG5PoatXQii55xAiXGXzY+sROLZ/Y+SLcp9PgJNI9Vz37HpQ74WvDcLT8eqm+kV3FzrA==} + engines: {node: '>=16'} + + tr46@7.0.0: + resolution: {integrity: sha512-BcDSDW+SIGWVOns1Zm6Yg91q9AEj7eaID0QDY9oIqJD1A8dscNwMcl9Sms2DeuWCNov1X0/oXanHGYQBSLwZsg==} + engines: {node: ^22.14.0 || >=24.0.0} + + ts-api-utils@2.5.0: + resolution: {integrity: sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==} + engines: {node: '>=18.12'} + peerDependencies: + typescript: '>=4.8.4' + + tslib@2.9.1: + resolution: {integrity: sha512-a4Ux9VKuIMHsOS7ENbEq6hK2ZGYIT27Rtl0E4eD9KqwxhKiQeMZE/9xb3IdYwWaSOyIBQLExhQgXDTYoiCxnWA==} + + tw-animate-css@1.4.0: + resolution: {integrity: sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ==} + + type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + + typescript-eslint@8.72.1: + resolution: {integrity: sha512-3Li9FfPmQp45nxEtFD1ZCrYFI3cYK5kpyNtmm1H7lMagm8u9sy6PTjy3ytwQZs7KNukOyKFHAZ6z6GjYFeaeXw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + typescript@6.0.3: + resolution: {integrity: sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==} + engines: {node: '>=14.17'} + hasBin: true + + undici-types@8.9.0: + resolution: {integrity: sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg==} + + undici@8.12.3: + resolution: {integrity: sha512-+fdndUTwBP86cVjihTp21hZHo7d6oj1c/47XPA3lL8mM0XHs+C4EkcjmgRsFnFk2f/UONGDy9ItBItmXsLOQHw==} + engines: {node: '>=22.19.0'} + + update-browserslist-db@1.3.4: + resolution: {integrity: sha512-t8wonIOdQyOjtPd2VktlBu6QjTVTks6v8bSi7kd0r2Vll8TK1JYDZtRRdbmCHOAMphkZgrzK+L+vXK4pyZ8t4A==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + uqr@0.1.3: + resolution: {integrity: sha512-0rjE8iEJe4YmT9TOhwsZtqCMRLc5DXZUI2UEYUUg63ikBkqqE5EYWaI0etFe/5KUcmcYwLih2RND1kq+hrUJXA==} + + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + + use-callback-ref@1.3.3: + resolution: {integrity: sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + use-sidecar@1.1.3: + resolution: {integrity: sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + use-sync-external-store@1.7.0: + resolution: {integrity: sha512-6L+EeigHMQhdaIPNIFUKwfWJSwWFQ8gJbJ2DLOs5sDIegTwR9fRxvnM3uciHKjIZhFz+KAv2emhWMRvDmMcY8A==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + vaul@1.1.2: + resolution: {integrity: sha512-ZFkClGpWyI2WUQjdLJ/BaGuV6AVQiJ3uELGk3OYtP+B6yCO7Cmn9vPFXVJkRaGkOJu3m8bQMgtyzNHixULceQA==} + peerDependencies: + react: ^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc + + vite@8.3.5: + resolution: {integrity: sha512-z1YpjieJ9g0ebTmQpLBv/oq9E3F9EIlOe+7LahW8SrE3LTiG3udBRUXQOlYXP9b+IC2q2phHeVxj9xGlw34OEQ==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + 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 + + vitest@5.1.0: + resolution: {integrity: sha512-MzmNFnrwa8v8lF1bLxw3UJ4EREwVOVb75VqJZmUDRjJ6EluK/x9th7qTbaeDk41WxE1GxWqmTXcdsfobfH5sOA==} + engines: {node: ^22.12.0 || ^24.0.0 || >=26.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@opentelemetry/api': ^1.9.0 + '@types/node': ^22.0.0 || >=24.0.0 + '@vitest/browser-playwright': 5.1.0 + '@vitest/browser-preview': 5.1.0 + '@vitest/browser-webdriverio': ^5.0.0-beta.5 || >=5.0.0 + '@vitest/coverage-istanbul': 5.1.0 + '@vitest/coverage-v8': 5.1.0 + '@vitest/ui': 5.1.0 + 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 + + w3c-xmlserializer@6.0.0: + resolution: {integrity: sha512-4Nsy8K5Tr6SPDH9jhKJOHf7ChDrc1zufZTVSF7x72hwuEXBqxqk9G6cK+K2NRUtB3iELRJqjXb4JPDMBjMTl2Q==} + engines: {node: ^22.22.2 || ^24.15.0 || >=26.0.0} + + webidl-conversions@8.0.1: + resolution: {integrity: sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==} + engines: {node: '>=20'} + + whatwg-mimetype@5.0.0: + resolution: {integrity: sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==} + engines: {node: '>=20'} + + whatwg-url@17.3.0: + resolution: {integrity: sha512-KwOAJrpf+GTgBvcBQJcP38sMF0DsQKdwtG+TxM2pW44P6E5k0hDbWrDmyuJDIF0Uef0aQq0ZoJs+5oI+k8ddhg==} + engines: {node: ^22.14.0 || >=24.0.0} + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + why-is-node-running@3.2.1: + resolution: {integrity: sha512-Tb2FUhB4vUsGQlfSquQLYkApkuPAFQXGFzxWKHHumVz2dK+X1RUm/HnID4+TfIGYJ1kTcwOaCk/buYCEJr6YjQ==} + engines: {node: '>=20.11'} + hasBin: true + + word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} + + xml-name-validator@5.0.0: + resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==} + engines: {node: '>=18'} + + xmlchars@2.2.0: + resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} + + yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + + zod-validation-error@4.0.2: + resolution: {integrity: sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==} + engines: {node: '>=18.0.0'} + peerDependencies: + zod: ^3.25.0 || ^4.0.0 + + zod@4.6.7: + resolution: {integrity: sha512-g8W2D2kU6a8untc053fX1uVBKzM7yqAxW/3zwk83+6RyF2+SKOuV+c0wBDhUwzV1PaGapmZH+rmcUzPf+3CGwg==} + +snapshots: + + '@adobe/css-tools@4.5.0': {} + + '@asamuzakjp/css-color@7.1.9': + dependencies: + '@csstools/css-calc': 3.4.3(@csstools/css-parser-algorithms@4.0.2(@csstools/css-tokenizer@4.0.2))(@csstools/css-tokenizer@4.0.2) + '@csstools/css-color-parser': 4.2.7(@csstools/css-parser-algorithms@4.0.2(@csstools/css-tokenizer@4.0.2))(@csstools/css-tokenizer@4.0.2) + '@csstools/css-parser-algorithms': 4.0.2(@csstools/css-tokenizer@4.0.2) + '@csstools/css-tokenizer': 4.0.2 + lru-cache: 11.5.3 + + '@asamuzakjp/dom-selector@10.0.5': + dependencies: + bidi-js: 1.1.0 + css-tree: 3.2.1 + is-potential-custom-element-name: 1.0.1 + lru-cache: 11.5.3 + + '@babel/code-frame@7.29.7': + dependencies: + '@babel/helper-validator-identifier': 7.29.7 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/compat-data@7.29.7': {} + + '@babel/core@7.29.7': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/generator': 7.29.12 + '@babel/helper-compilation-targets': 7.29.7 + '@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7) + '@babel/helpers': 7.29.10 + '@babel/parser': 7.29.12 + '@babel/template': 7.29.7 + '@babel/traverse': 7.29.12 + '@babel/types': 7.29.11 + '@jridgewell/remapping': 2.3.5 + convert-source-map: 2.0.0 + debug: 4.4.3 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.2 + transitivePeerDependencies: + - supports-color + + '@babel/generator@7.29.12': + dependencies: + '@babel/parser': 7.29.12 + '@babel/types': 7.29.11 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + jsesc: 3.1.0 + + '@babel/helper-compilation-targets@7.29.7': + dependencies: + '@babel/compat-data': 7.29.7 + '@babel/helper-validator-option': 7.29.7 + browserslist: 4.29.5 + lru-cache: 5.1.1 + semver: 6.3.2 + + '@babel/helper-globals@7.29.7': {} + + '@babel/helper-module-imports@7.29.7': + dependencies: + '@babel/traverse': 7.29.12 + '@babel/types': 7.29.11 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-module-imports': 7.29.7 + '@babel/helper-validator-identifier': 7.29.7 + '@babel/traverse': 7.29.12 + transitivePeerDependencies: + - supports-color + + '@babel/helper-string-parser@7.29.11': {} + + '@babel/helper-validator-identifier@7.29.7': {} + + '@babel/helper-validator-option@7.29.7': {} + + '@babel/helpers@7.29.10': + dependencies: + '@babel/template': 7.29.7 + '@babel/types': 7.29.11 + + '@babel/parser@7.29.12': + dependencies: + '@babel/types': 7.29.11 + + '@babel/runtime@7.29.10': {} + + '@babel/template@7.29.7': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/parser': 7.29.12 + '@babel/types': 7.29.11 + + '@babel/traverse@7.29.12': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/generator': 7.29.12 + '@babel/helper-globals': 7.29.7 + '@babel/parser': 7.29.12 + '@babel/template': 7.29.7 + '@babel/types': 7.29.11 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + '@babel/types@7.29.11': + dependencies: + '@babel/helper-string-parser': 7.29.11 + '@babel/helper-validator-identifier': 7.29.7 + + '@bcoe/v8-coverage@1.0.2': {} + + '@bramus/specificity@2.4.3': + dependencies: + css-tree: 3.2.1 + + '@cacheable/memory@2.2.0': + dependencies: + '@cacheable/utils': 2.5.0 + '@keyv/bigmap': 1.3.1(keyv@5.6.0) + hookified: 1.15.1 + keyv: 5.6.0 + + '@cacheable/utils@2.5.0': + dependencies: + hashery: 1.5.1 + keyv: 5.6.0 + + '@csstools/color-helpers@6.1.2': {} + + '@csstools/css-calc@3.4.3(@csstools/css-parser-algorithms@4.0.2(@csstools/css-tokenizer@4.0.2))(@csstools/css-tokenizer@4.0.2)': + dependencies: + '@csstools/css-parser-algorithms': 4.0.2(@csstools/css-tokenizer@4.0.2) + '@csstools/css-tokenizer': 4.0.2 + + '@csstools/css-color-parser@4.2.7(@csstools/css-parser-algorithms@4.0.2(@csstools/css-tokenizer@4.0.2))(@csstools/css-tokenizer@4.0.2)': + dependencies: + '@csstools/color-helpers': 6.1.2 + '@csstools/css-calc': 3.4.3(@csstools/css-parser-algorithms@4.0.2(@csstools/css-tokenizer@4.0.2))(@csstools/css-tokenizer@4.0.2) + '@csstools/css-parser-algorithms': 4.0.2(@csstools/css-tokenizer@4.0.2) + '@csstools/css-tokenizer': 4.0.2 + + '@csstools/css-parser-algorithms@4.0.2(@csstools/css-tokenizer@4.0.2)': + dependencies: + '@csstools/css-tokenizer': 4.0.2 + + '@csstools/css-syntax-patches-for-csstree@1.1.16(css-tree@3.2.1)': + optionalDependencies: + css-tree: 3.2.1 + + '@csstools/css-tokenizer@4.0.2': {} + + '@date-fns/tz@1.5.0': {} + + '@dnd-kit/abstract@0.5.3': + dependencies: + '@dnd-kit/geometry': 0.5.3 + '@dnd-kit/state': 0.5.3 + tslib: 2.9.1 + + '@dnd-kit/collision@0.5.3': + dependencies: + '@dnd-kit/abstract': 0.5.3 + '@dnd-kit/geometry': 0.5.3 + tslib: 2.9.1 + + '@dnd-kit/dom@0.5.3': + dependencies: + '@dnd-kit/abstract': 0.5.3 + '@dnd-kit/collision': 0.5.3 + '@dnd-kit/geometry': 0.5.3 + '@dnd-kit/state': 0.5.3 + tslib: 2.9.1 + + '@dnd-kit/geometry@0.5.3': + dependencies: + '@dnd-kit/state': 0.5.3 + tslib: 2.9.1 + + '@dnd-kit/helpers@0.5.3': + dependencies: + '@dnd-kit/abstract': 0.5.3 + tslib: 2.9.1 + + '@dnd-kit/react@0.5.3(react-dom@19.3.1(react@19.3.1))(react@19.3.1)': + dependencies: + '@dnd-kit/abstract': 0.5.3 + '@dnd-kit/dom': 0.5.3 + '@dnd-kit/state': 0.5.3 + react: 19.3.1 + react-dom: 19.3.1(react@19.3.1) + tslib: 2.9.1 + + '@dnd-kit/state@0.5.3': + dependencies: + '@preact/signals-core': 1.14.5 + tslib: 2.9.1 + + '@eslint-community/eslint-utils@4.10.1(eslint@10.14.0)': + dependencies: + eslint: 10.14.0 + eslint-visitor-keys: 3.4.3 + + '@eslint-community/regexpp@4.12.2': {} + + '@eslint/config-array@0.23.5': + dependencies: + '@eslint/object-schema': 3.0.5 + debug: 4.4.3 + minimatch: 10.2.6 + transitivePeerDependencies: + - supports-color + + '@eslint/config-helpers@0.7.0': + dependencies: + '@eslint/core': 1.2.1 + + '@eslint/core@1.2.1': + dependencies: + '@types/json-schema': 7.0.15 + + '@eslint/js@10.0.1(eslint@10.14.0)': + optionalDependencies: + eslint: 10.14.0 + + '@eslint/object-schema@3.0.5': {} + + '@eslint/plugin-kit@0.7.3': + dependencies: + '@eslint/core': 1.2.1 + levn: 0.4.1 + + '@exodus/bytes@1.16.0': {} + + '@floating-ui/core@1.8.0': + dependencies: + '@floating-ui/utils': 0.2.12 + + '@floating-ui/dom@1.8.0': + dependencies: + '@floating-ui/core': 1.8.0 + '@floating-ui/utils': 0.2.12 + + '@floating-ui/react-dom@2.1.9(react-dom@19.3.1(react@19.3.1))(react@19.3.1)': + dependencies: + '@floating-ui/dom': 1.8.0 + react: 19.3.1 + react-dom: 19.3.1(react@19.3.1) + + '@floating-ui/utils@0.2.12': {} + + '@humanfs/core@0.19.2': + dependencies: + '@humanfs/types': 0.15.0 + + '@humanfs/node@0.16.8': + dependencies: + '@humanfs/core': 0.19.2 + '@humanfs/types': 0.15.0 + '@humanwhocodes/retry': 0.4.3 + + '@humanfs/types@0.15.0': {} + + '@humanwhocodes/module-importer@1.0.1': {} + + '@humanwhocodes/retry@0.4.3': {} + + '@internationalized/date@3.12.4': + dependencies: + '@swc/helpers': 0.5.24 + + '@internationalized/number@3.6.9': + dependencies: + '@swc/helpers': 0.5.24 + + '@internationalized/string@3.2.10': + dependencies: + '@swc/helpers': 0.5.24 + + '@javaabu/forms@https://codeload.github.com/Javaabu/react-forms/tar.gz/41eb14a07158a6dedb62bcc4b022507d3910c3e0(@javaabu/onu@0.2.1(@tanstack/react-query@5.105.3(@types/react@19.3.0)(react@19.3.1))(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react-hook-form@7.91.0(@types/react@19.3.0)(react@19.3.1))(react@19.3.1)(tailwindcss@4.3.4))(@testing-library/react@16.3.3(@testing-library/dom@10.4.2)(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1))(react-dom@19.3.1(react@19.3.1))(react@19.3.1)': + dependencies: + '@javaabu/i18n': https://codeload.github.com/Javaabu/i18n/tar.gz/76730212d6f0f1d87a78635ed6e005dd5713cbba(react@19.3.1) + react: 19.3.1 + react-dom: 19.3.1(react@19.3.1) + optionalDependencies: + '@javaabu/onu': 0.2.1(@tanstack/react-query@5.105.3(@types/react@19.3.0)(react@19.3.1))(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react-hook-form@7.91.0(@types/react@19.3.0)(react@19.3.1))(react@19.3.1)(tailwindcss@4.3.4) + '@testing-library/react': 16.3.3(@testing-library/dom@10.4.2)(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + transitivePeerDependencies: + - vue + + '@javaabu/i18n@https://codeload.github.com/Javaabu/i18n/tar.gz/76730212d6f0f1d87a78635ed6e005dd5713cbba(react@19.3.1)': + optionalDependencies: + react: 19.3.1 + + '@javaabu/onu@0.2.1(@tanstack/react-query@5.105.3(@types/react@19.3.0)(react@19.3.1))(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react-hook-form@7.91.0(@types/react@19.3.0)(react@19.3.1))(react@19.3.1)(tailwindcss@4.3.4)': + dependencies: + '@dnd-kit/dom': 0.5.3 + '@dnd-kit/helpers': 0.5.3 + '@dnd-kit/react': 0.5.3(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + '@internationalized/date': 3.12.4 + '@sindresorhus/slugify': 3.0.1 + '@tanstack/react-query': 5.105.3(@types/react@19.3.0)(react@19.3.1) + '@tanstack/react-table': 9.3.1(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + '@tanstack/react-virtual': 3.14.26(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + class-variance-authority: 0.7.1 + clsx: 2.1.1 + cmdk: 1.1.1(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + date-fns: 4.4.0 + embla-carousel-react: 8.6.0(react@19.3.1) + input-otp: 1.5.0(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + lucide-react: 1.68.0(@types/react@19.3.0)(react@19.3.1) + radix-ui: 1.8.1(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + react: 19.3.1 + react-aria-components: 1.22.1(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + react-day-picker: 10.0.2(@types/react@19.3.0)(react@19.3.1) + react-dom: 19.3.1(react@19.3.1) + react-hook-form: 7.91.0(@types/react@19.3.0)(react@19.3.1) + react-phone-number-input: 3.4.18(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + sonner: 2.0.8(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + tailwind-merge: 3.7.0 + tailwindcss: 4.3.4 + tw-animate-css: 1.4.0 + uqr: 0.1.3 + vaul: 1.1.2(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + transitivePeerDependencies: + - '@types/react' + - '@types/react-dom' + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.6.0 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.6.0': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.6.0 + + '@keyv/bigmap@1.3.1(keyv@5.6.0)': + dependencies: + hashery: 1.5.1 + hookified: 1.15.1 + keyv: 5.6.0 + + '@keyv/serialize@1.1.1': {} + + '@oxc-project/types@0.156.0': {} + + '@preact/signals-core@1.14.5': {} + + '@radix-ui/number@1.1.3': {} + + '@radix-ui/primitive@1.2.0': {} + + '@radix-ui/react-accessible-icon@1.2.0(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1)': + dependencies: + '@radix-ui/react-visually-hidden': 1.2.13(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + react: 19.3.1 + react-dom: 19.3.1(react@19.3.1) + optionalDependencies: + '@types/react': 19.3.0 + '@types/react-dom': 19.3.0(@types/react@19.3.0) + + '@radix-ui/react-accordion@1.3.1(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1)': + dependencies: + '@radix-ui/primitive': 1.2.0 + '@radix-ui/react-collapsible': 1.2.1(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + '@radix-ui/react-collection': 1.2.0(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.3.0)(react@19.3.1) + '@radix-ui/react-context': 1.3.0(@types/react@19.3.0)(react@19.3.1) + '@radix-ui/react-direction': 1.2.0(@types/react@19.3.0)(react@19.3.1) + '@radix-ui/react-id': 1.1.4(@types/react@19.3.0)(react@19.3.1) + '@radix-ui/react-primitive': 2.2.0(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + '@radix-ui/react-use-controllable-state': 1.3.0(@types/react@19.3.0)(react@19.3.1) + react: 19.3.1 + react-dom: 19.3.1(react@19.3.1) + optionalDependencies: + '@types/react': 19.3.0 + '@types/react-dom': 19.3.0(@types/react@19.3.0) + + '@radix-ui/react-alert-dialog@1.2.1(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1)': + dependencies: + '@radix-ui/primitive': 1.2.0 + '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.3.0)(react@19.3.1) + '@radix-ui/react-context': 1.3.0(@types/react@19.3.0)(react@19.3.1) + '@radix-ui/react-dialog': 1.3.1(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + '@radix-ui/react-primitive': 2.2.0(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + react: 19.3.1 + react-dom: 19.3.1(react@19.3.1) + optionalDependencies: + '@types/react': 19.3.0 + '@types/react-dom': 19.3.0(@types/react@19.3.0) + + '@radix-ui/react-arrow@1.1.17(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1)': + dependencies: + '@radix-ui/react-primitive': 2.2.0(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + react: 19.3.1 + react-dom: 19.3.1(react@19.3.1) + optionalDependencies: + '@types/react': 19.3.0 + '@types/react-dom': 19.3.0(@types/react@19.3.0) + + '@radix-ui/react-aspect-ratio@1.2.0(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1)': + dependencies: + '@radix-ui/react-primitive': 2.2.0(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + react: 19.3.1 + react-dom: 19.3.1(react@19.3.1) + optionalDependencies: + '@types/react': 19.3.0 + '@types/react-dom': 19.3.0(@types/react@19.3.0) + + '@radix-ui/react-avatar@1.3.1(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1)': + dependencies: + '@radix-ui/primitive': 1.2.0 + '@radix-ui/react-context': 1.3.0(@types/react@19.3.0)(react@19.3.1) + '@radix-ui/react-primitive': 2.2.0(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + '@radix-ui/react-use-callback-ref': 1.1.5(@types/react@19.3.0)(react@19.3.1) + '@radix-ui/react-use-is-hydrated': 0.1.3(@types/react@19.3.0)(react@19.3.1) + '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.3.0)(react@19.3.1) + react: 19.3.1 + react-dom: 19.3.1(react@19.3.1) + optionalDependencies: + '@types/react': 19.3.0 + '@types/react-dom': 19.3.0(@types/react@19.3.0) + + '@radix-ui/react-checkbox@1.4.1(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1)': + dependencies: + '@radix-ui/primitive': 1.2.0 + '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.3.0)(react@19.3.1) + '@radix-ui/react-context': 1.3.0(@types/react@19.3.0)(react@19.3.1) + '@radix-ui/react-presence': 1.1.11(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + '@radix-ui/react-primitive': 2.2.0(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + '@radix-ui/react-use-controllable-state': 1.3.0(@types/react@19.3.0)(react@19.3.1) + '@radix-ui/react-use-size': 1.1.5(@types/react@19.3.0)(react@19.3.1) + react: 19.3.1 + react-dom: 19.3.1(react@19.3.1) + optionalDependencies: + '@types/react': 19.3.0 + '@types/react-dom': 19.3.0(@types/react@19.3.0) + + '@radix-ui/react-collapsible@1.2.1(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1)': + dependencies: + '@radix-ui/primitive': 1.2.0 + '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.3.0)(react@19.3.1) + '@radix-ui/react-context': 1.3.0(@types/react@19.3.0)(react@19.3.1) + '@radix-ui/react-id': 1.1.4(@types/react@19.3.0)(react@19.3.1) + '@radix-ui/react-presence': 1.1.11(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + '@radix-ui/react-primitive': 2.2.0(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + '@radix-ui/react-use-controllable-state': 1.3.0(@types/react@19.3.0)(react@19.3.1) + '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.3.0)(react@19.3.1) + react: 19.3.1 + react-dom: 19.3.1(react@19.3.1) + optionalDependencies: + '@types/react': 19.3.0 + '@types/react-dom': 19.3.0(@types/react@19.3.0) + + '@radix-ui/react-collection@1.2.0(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.3.0)(react@19.3.1) + '@radix-ui/react-context': 1.3.0(@types/react@19.3.0)(react@19.3.1) + '@radix-ui/react-primitive': 2.2.0(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + '@radix-ui/react-slot': 1.5.0(@types/react@19.3.0)(react@19.3.1) + '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.3.0)(react@19.3.1) + react: 19.3.1 + react-dom: 19.3.1(react@19.3.1) + optionalDependencies: + '@types/react': 19.3.0 + '@types/react-dom': 19.3.0(@types/react@19.3.0) + + '@radix-ui/react-compose-refs@1.1.5(@types/react@19.3.0)(react@19.3.1)': + dependencies: + react: 19.3.1 + optionalDependencies: + '@types/react': 19.3.0 + + '@radix-ui/react-context-menu@2.4.1(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1)': + dependencies: + '@radix-ui/primitive': 1.2.0 + '@radix-ui/react-context': 1.3.0(@types/react@19.3.0)(react@19.3.1) + '@radix-ui/react-menu': 2.2.1(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + '@radix-ui/react-primitive': 2.2.0(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + '@radix-ui/react-use-controllable-state': 1.3.0(@types/react@19.3.0)(react@19.3.1) + react: 19.3.1 + react-dom: 19.3.1(react@19.3.1) + optionalDependencies: + '@types/react': 19.3.0 + '@types/react-dom': 19.3.0(@types/react@19.3.0) + + '@radix-ui/react-context@1.3.0(@types/react@19.3.0)(react@19.3.1)': + dependencies: + react: 19.3.1 + optionalDependencies: + '@types/react': 19.3.0 + + '@radix-ui/react-dialog@1.3.1(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1)': + dependencies: + '@radix-ui/primitive': 1.2.0 + '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.3.0)(react@19.3.1) + '@radix-ui/react-context': 1.3.0(@types/react@19.3.0)(react@19.3.1) + '@radix-ui/react-dismissable-layer': 1.2.1(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + '@radix-ui/react-focus-guards': 1.2.0(@types/react@19.3.0)(react@19.3.1) + '@radix-ui/react-focus-scope': 1.3.1(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + '@radix-ui/react-id': 1.1.4(@types/react@19.3.0)(react@19.3.1) + '@radix-ui/react-portal': 1.2.0(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + '@radix-ui/react-presence': 1.1.11(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + '@radix-ui/react-primitive': 2.2.0(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + '@radix-ui/react-slot': 1.5.0(@types/react@19.3.0)(react@19.3.1) + '@radix-ui/react-use-controllable-state': 1.3.0(@types/react@19.3.0)(react@19.3.1) + '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.3.0)(react@19.3.1) + aria-hidden: 1.2.6 + react: 19.3.1 + react-dom: 19.3.1(react@19.3.1) + react-remove-scroll: 2.7.2(@types/react@19.3.0)(react@19.3.1) + optionalDependencies: + '@types/react': 19.3.0 + '@types/react-dom': 19.3.0(@types/react@19.3.0) + + '@radix-ui/react-direction@1.2.0(@types/react@19.3.0)(react@19.3.1)': + dependencies: + react: 19.3.1 + optionalDependencies: + '@types/react': 19.3.0 + + '@radix-ui/react-dismissable-layer@1.2.1(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1)': + dependencies: + '@radix-ui/primitive': 1.2.0 + '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.3.0)(react@19.3.1) + '@radix-ui/react-primitive': 2.2.0(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + '@radix-ui/react-use-callback-ref': 1.1.5(@types/react@19.3.0)(react@19.3.1) + '@radix-ui/react-use-effect-event': 0.0.5(@types/react@19.3.0)(react@19.3.1) + react: 19.3.1 + react-dom: 19.3.1(react@19.3.1) + optionalDependencies: + '@types/react': 19.3.0 + '@types/react-dom': 19.3.0(@types/react@19.3.0) + + '@radix-ui/react-dropdown-menu@2.2.1(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1)': + dependencies: + '@radix-ui/primitive': 1.2.0 + '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.3.0)(react@19.3.1) + '@radix-ui/react-context': 1.3.0(@types/react@19.3.0)(react@19.3.1) + '@radix-ui/react-id': 1.1.4(@types/react@19.3.0)(react@19.3.1) + '@radix-ui/react-menu': 2.2.1(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + '@radix-ui/react-primitive': 2.2.0(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + '@radix-ui/react-use-controllable-state': 1.3.0(@types/react@19.3.0)(react@19.3.1) + react: 19.3.1 + react-dom: 19.3.1(react@19.3.1) + optionalDependencies: + '@types/react': 19.3.0 + '@types/react-dom': 19.3.0(@types/react@19.3.0) + + '@radix-ui/react-focus-guards@1.2.0(@types/react@19.3.0)(react@19.3.1)': + dependencies: + react: 19.3.1 + optionalDependencies: + '@types/react': 19.3.0 + + '@radix-ui/react-focus-scope@1.3.1(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.3.0)(react@19.3.1) + '@radix-ui/react-primitive': 2.2.0(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + '@radix-ui/react-use-callback-ref': 1.1.5(@types/react@19.3.0)(react@19.3.1) + react: 19.3.1 + react-dom: 19.3.1(react@19.3.1) + optionalDependencies: + '@types/react': 19.3.0 + '@types/react-dom': 19.3.0(@types/react@19.3.0) + + '@radix-ui/react-form@0.3.0(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1)': + dependencies: + '@radix-ui/primitive': 1.2.0 + '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.3.0)(react@19.3.1) + '@radix-ui/react-context': 1.3.0(@types/react@19.3.0)(react@19.3.1) + '@radix-ui/react-id': 1.1.4(@types/react@19.3.0)(react@19.3.1) + '@radix-ui/react-label': 2.1.17(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + '@radix-ui/react-primitive': 2.2.0(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + react: 19.3.1 + react-dom: 19.3.1(react@19.3.1) + optionalDependencies: + '@types/react': 19.3.0 + '@types/react-dom': 19.3.0(@types/react@19.3.0) + + '@radix-ui/react-hover-card@1.2.1(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1)': + dependencies: + '@radix-ui/primitive': 1.2.0 + '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.3.0)(react@19.3.1) + '@radix-ui/react-context': 1.3.0(@types/react@19.3.0)(react@19.3.1) + '@radix-ui/react-dismissable-layer': 1.2.1(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + '@radix-ui/react-focus-scope': 1.3.1(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + '@radix-ui/react-popper': 1.4.0(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + '@radix-ui/react-portal': 1.2.0(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + '@radix-ui/react-presence': 1.1.11(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + '@radix-ui/react-primitive': 2.2.0(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + '@radix-ui/react-use-controllable-state': 1.3.0(@types/react@19.3.0)(react@19.3.1) + react: 19.3.1 + react-dom: 19.3.1(react@19.3.1) + optionalDependencies: + '@types/react': 19.3.0 + '@types/react-dom': 19.3.0(@types/react@19.3.0) + + '@radix-ui/react-id@1.1.4(@types/react@19.3.0)(react@19.3.1)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.3.0)(react@19.3.1) + react: 19.3.1 + optionalDependencies: + '@types/react': 19.3.0 + + '@radix-ui/react-label@2.1.17(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1)': + dependencies: + '@radix-ui/react-primitive': 2.2.0(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + react: 19.3.1 + react-dom: 19.3.1(react@19.3.1) + optionalDependencies: + '@types/react': 19.3.0 + '@types/react-dom': 19.3.0(@types/react@19.3.0) + + '@radix-ui/react-menu@2.2.1(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1)': + dependencies: + '@radix-ui/primitive': 1.2.0 + '@radix-ui/react-collection': 1.2.0(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.3.0)(react@19.3.1) + '@radix-ui/react-context': 1.3.0(@types/react@19.3.0)(react@19.3.1) + '@radix-ui/react-direction': 1.2.0(@types/react@19.3.0)(react@19.3.1) + '@radix-ui/react-dismissable-layer': 1.2.1(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + '@radix-ui/react-focus-guards': 1.2.0(@types/react@19.3.0)(react@19.3.1) + '@radix-ui/react-focus-scope': 1.3.1(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + '@radix-ui/react-id': 1.1.4(@types/react@19.3.0)(react@19.3.1) + '@radix-ui/react-popper': 1.4.0(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + '@radix-ui/react-portal': 1.2.0(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + '@radix-ui/react-presence': 1.1.11(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + '@radix-ui/react-primitive': 2.2.0(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + '@radix-ui/react-roving-focus': 1.2.1(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + '@radix-ui/react-slot': 1.5.0(@types/react@19.3.0)(react@19.3.1) + '@radix-ui/react-use-callback-ref': 1.1.5(@types/react@19.3.0)(react@19.3.1) + aria-hidden: 1.2.6 + react: 19.3.1 + react-dom: 19.3.1(react@19.3.1) + react-remove-scroll: 2.7.2(@types/react@19.3.0)(react@19.3.1) + optionalDependencies: + '@types/react': 19.3.0 + '@types/react-dom': 19.3.0(@types/react@19.3.0) + + '@radix-ui/react-menubar@1.2.1(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1)': + dependencies: + '@radix-ui/primitive': 1.2.0 + '@radix-ui/react-collection': 1.2.0(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.3.0)(react@19.3.1) + '@radix-ui/react-context': 1.3.0(@types/react@19.3.0)(react@19.3.1) + '@radix-ui/react-direction': 1.2.0(@types/react@19.3.0)(react@19.3.1) + '@radix-ui/react-id': 1.1.4(@types/react@19.3.0)(react@19.3.1) + '@radix-ui/react-menu': 2.2.1(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + '@radix-ui/react-primitive': 2.2.0(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + '@radix-ui/react-roving-focus': 1.2.1(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + '@radix-ui/react-use-controllable-state': 1.3.0(@types/react@19.3.0)(react@19.3.1) + react: 19.3.1 + react-dom: 19.3.1(react@19.3.1) + optionalDependencies: + '@types/react': 19.3.0 + '@types/react-dom': 19.3.0(@types/react@19.3.0) + + '@radix-ui/react-navigation-menu@1.4.1(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1)': + dependencies: + '@radix-ui/primitive': 1.2.0 + '@radix-ui/react-collection': 1.2.0(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.3.0)(react@19.3.1) + '@radix-ui/react-context': 1.3.0(@types/react@19.3.0)(react@19.3.1) + '@radix-ui/react-direction': 1.2.0(@types/react@19.3.0)(react@19.3.1) + '@radix-ui/react-dismissable-layer': 1.2.1(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + '@radix-ui/react-id': 1.1.4(@types/react@19.3.0)(react@19.3.1) + '@radix-ui/react-presence': 1.1.11(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + '@radix-ui/react-primitive': 2.2.0(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + '@radix-ui/react-slot': 1.5.0(@types/react@19.3.0)(react@19.3.1) + '@radix-ui/react-use-callback-ref': 1.1.5(@types/react@19.3.0)(react@19.3.1) + '@radix-ui/react-use-controllable-state': 1.3.0(@types/react@19.3.0)(react@19.3.1) + '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.3.0)(react@19.3.1) + '@radix-ui/react-use-previous': 1.1.4(@types/react@19.3.0)(react@19.3.1) + '@radix-ui/react-visually-hidden': 1.2.13(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + react: 19.3.1 + react-dom: 19.3.1(react@19.3.1) + optionalDependencies: + '@types/react': 19.3.0 + '@types/react-dom': 19.3.0(@types/react@19.3.0) + + '@radix-ui/react-one-time-password-field@0.2.1(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1)': + dependencies: + '@radix-ui/number': 1.1.3 + '@radix-ui/primitive': 1.2.0 + '@radix-ui/react-collection': 1.2.0(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.3.0)(react@19.3.1) + '@radix-ui/react-context': 1.3.0(@types/react@19.3.0)(react@19.3.1) + '@radix-ui/react-direction': 1.2.0(@types/react@19.3.0)(react@19.3.1) + '@radix-ui/react-primitive': 2.2.0(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + '@radix-ui/react-roving-focus': 1.2.1(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + '@radix-ui/react-use-controllable-state': 1.3.0(@types/react@19.3.0)(react@19.3.1) + '@radix-ui/react-use-effect-event': 0.0.5(@types/react@19.3.0)(react@19.3.1) + '@radix-ui/react-use-is-hydrated': 0.1.3(@types/react@19.3.0)(react@19.3.1) + '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.3.0)(react@19.3.1) + react: 19.3.1 + react-dom: 19.3.1(react@19.3.1) + optionalDependencies: + '@types/react': 19.3.0 + '@types/react-dom': 19.3.0(@types/react@19.3.0) + + '@radix-ui/react-password-toggle-field@0.2.1(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1)': + dependencies: + '@radix-ui/primitive': 1.2.0 + '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.3.0)(react@19.3.1) + '@radix-ui/react-context': 1.3.0(@types/react@19.3.0)(react@19.3.1) + '@radix-ui/react-id': 1.1.4(@types/react@19.3.0)(react@19.3.1) + '@radix-ui/react-primitive': 2.2.0(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + '@radix-ui/react-use-controllable-state': 1.3.0(@types/react@19.3.0)(react@19.3.1) + '@radix-ui/react-use-effect-event': 0.0.5(@types/react@19.3.0)(react@19.3.1) + '@radix-ui/react-use-is-hydrated': 0.1.3(@types/react@19.3.0)(react@19.3.1) + react: 19.3.1 + react-dom: 19.3.1(react@19.3.1) + optionalDependencies: + '@types/react': 19.3.0 + '@types/react-dom': 19.3.0(@types/react@19.3.0) + + '@radix-ui/react-popover@1.3.1(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1)': + dependencies: + '@radix-ui/primitive': 1.2.0 + '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.3.0)(react@19.3.1) + '@radix-ui/react-context': 1.3.0(@types/react@19.3.0)(react@19.3.1) + '@radix-ui/react-dismissable-layer': 1.2.1(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + '@radix-ui/react-focus-guards': 1.2.0(@types/react@19.3.0)(react@19.3.1) + '@radix-ui/react-focus-scope': 1.3.1(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + '@radix-ui/react-id': 1.1.4(@types/react@19.3.0)(react@19.3.1) + '@radix-ui/react-popper': 1.4.0(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + '@radix-ui/react-portal': 1.2.0(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + '@radix-ui/react-presence': 1.1.11(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + '@radix-ui/react-primitive': 2.2.0(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + '@radix-ui/react-slot': 1.5.0(@types/react@19.3.0)(react@19.3.1) + '@radix-ui/react-use-controllable-state': 1.3.0(@types/react@19.3.0)(react@19.3.1) + '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.3.0)(react@19.3.1) + aria-hidden: 1.2.6 + react: 19.3.1 + react-dom: 19.3.1(react@19.3.1) + react-remove-scroll: 2.7.2(@types/react@19.3.0)(react@19.3.1) + optionalDependencies: + '@types/react': 19.3.0 + '@types/react-dom': 19.3.0(@types/react@19.3.0) + + '@radix-ui/react-popper@1.4.0(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1)': + dependencies: + '@floating-ui/react-dom': 2.1.9(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + '@radix-ui/react-arrow': 1.1.17(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.3.0)(react@19.3.1) + '@radix-ui/react-context': 1.3.0(@types/react@19.3.0)(react@19.3.1) + '@radix-ui/react-primitive': 2.2.0(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + '@radix-ui/react-use-callback-ref': 1.1.5(@types/react@19.3.0)(react@19.3.1) + '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.3.0)(react@19.3.1) + '@radix-ui/react-use-rect': 1.1.4(@types/react@19.3.0)(react@19.3.1) + '@radix-ui/react-use-size': 1.1.5(@types/react@19.3.0)(react@19.3.1) + '@radix-ui/rect': 1.1.3 + react: 19.3.1 + react-dom: 19.3.1(react@19.3.1) + optionalDependencies: + '@types/react': 19.3.0 + '@types/react-dom': 19.3.0(@types/react@19.3.0) + + '@radix-ui/react-portal@1.2.0(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1)': + dependencies: + '@radix-ui/react-primitive': 2.2.0(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.3.0)(react@19.3.1) + react: 19.3.1 + react-dom: 19.3.1(react@19.3.1) + optionalDependencies: + '@types/react': 19.3.0 + '@types/react-dom': 19.3.0(@types/react@19.3.0) + + '@radix-ui/react-presence@1.1.11(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.3.0)(react@19.3.1) + react: 19.3.1 + react-dom: 19.3.1(react@19.3.1) + optionalDependencies: + '@types/react': 19.3.0 + '@types/react-dom': 19.3.0(@types/react@19.3.0) + + '@radix-ui/react-primitive@2.2.0(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1)': + dependencies: + '@radix-ui/react-slot': 1.5.0(@types/react@19.3.0)(react@19.3.1) + react: 19.3.1 + react-dom: 19.3.1(react@19.3.1) + optionalDependencies: + '@types/react': 19.3.0 + '@types/react-dom': 19.3.0(@types/react@19.3.0) + + '@radix-ui/react-progress@1.2.1(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1)': + dependencies: + '@radix-ui/react-context': 1.3.0(@types/react@19.3.0)(react@19.3.1) + '@radix-ui/react-primitive': 2.2.0(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + react: 19.3.1 + react-dom: 19.3.1(react@19.3.1) + optionalDependencies: + '@types/react': 19.3.0 + '@types/react-dom': 19.3.0(@types/react@19.3.0) + + '@radix-ui/react-radio-group@1.5.1(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1)': + dependencies: + '@radix-ui/primitive': 1.2.0 + '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.3.0)(react@19.3.1) + '@radix-ui/react-context': 1.3.0(@types/react@19.3.0)(react@19.3.1) + '@radix-ui/react-direction': 1.2.0(@types/react@19.3.0)(react@19.3.1) + '@radix-ui/react-presence': 1.1.11(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + '@radix-ui/react-primitive': 2.2.0(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + '@radix-ui/react-roving-focus': 1.2.1(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + '@radix-ui/react-use-controllable-state': 1.3.0(@types/react@19.3.0)(react@19.3.1) + '@radix-ui/react-use-size': 1.1.5(@types/react@19.3.0)(react@19.3.1) + react: 19.3.1 + react-dom: 19.3.1(react@19.3.1) + optionalDependencies: + '@types/react': 19.3.0 + '@types/react-dom': 19.3.0(@types/react@19.3.0) + + '@radix-ui/react-roving-focus@1.2.1(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1)': + dependencies: + '@radix-ui/primitive': 1.2.0 + '@radix-ui/react-collection': 1.2.0(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.3.0)(react@19.3.1) + '@radix-ui/react-context': 1.3.0(@types/react@19.3.0)(react@19.3.1) + '@radix-ui/react-direction': 1.2.0(@types/react@19.3.0)(react@19.3.1) + '@radix-ui/react-id': 1.1.4(@types/react@19.3.0)(react@19.3.1) + '@radix-ui/react-primitive': 2.2.0(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + '@radix-ui/react-use-callback-ref': 1.1.5(@types/react@19.3.0)(react@19.3.1) + '@radix-ui/react-use-controllable-state': 1.3.0(@types/react@19.3.0)(react@19.3.1) + '@radix-ui/react-use-is-hydrated': 0.1.3(@types/react@19.3.0)(react@19.3.1) + '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.3.0)(react@19.3.1) + react: 19.3.1 + react-dom: 19.3.1(react@19.3.1) + optionalDependencies: + '@types/react': 19.3.0 + '@types/react-dom': 19.3.0(@types/react@19.3.0) + + '@radix-ui/react-scroll-area@1.4.0(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1)': + dependencies: + '@radix-ui/number': 1.1.3 + '@radix-ui/primitive': 1.2.0 + '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.3.0)(react@19.3.1) + '@radix-ui/react-context': 1.3.0(@types/react@19.3.0)(react@19.3.1) + '@radix-ui/react-direction': 1.2.0(@types/react@19.3.0)(react@19.3.1) + '@radix-ui/react-presence': 1.1.11(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + '@radix-ui/react-primitive': 2.2.0(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + '@radix-ui/react-slot': 1.5.0(@types/react@19.3.0)(react@19.3.1) + '@radix-ui/react-use-callback-ref': 1.1.5(@types/react@19.3.0)(react@19.3.1) + '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.3.0)(react@19.3.1) + react: 19.3.1 + react-dom: 19.3.1(react@19.3.1) + optionalDependencies: + '@types/react': 19.3.0 + '@types/react-dom': 19.3.0(@types/react@19.3.0) + + '@radix-ui/react-select@2.4.1(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1)': + dependencies: + '@radix-ui/number': 1.1.3 + '@radix-ui/primitive': 1.2.0 + '@radix-ui/react-collection': 1.2.0(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.3.0)(react@19.3.1) + '@radix-ui/react-context': 1.3.0(@types/react@19.3.0)(react@19.3.1) + '@radix-ui/react-direction': 1.2.0(@types/react@19.3.0)(react@19.3.1) + '@radix-ui/react-dismissable-layer': 1.2.1(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + '@radix-ui/react-focus-guards': 1.2.0(@types/react@19.3.0)(react@19.3.1) + '@radix-ui/react-focus-scope': 1.3.1(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + '@radix-ui/react-id': 1.1.4(@types/react@19.3.0)(react@19.3.1) + '@radix-ui/react-popper': 1.4.0(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + '@radix-ui/react-portal': 1.2.0(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + '@radix-ui/react-presence': 1.1.11(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + '@radix-ui/react-primitive': 2.2.0(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + '@radix-ui/react-slot': 1.5.0(@types/react@19.3.0)(react@19.3.1) + '@radix-ui/react-use-callback-ref': 1.1.5(@types/react@19.3.0)(react@19.3.1) + '@radix-ui/react-use-controllable-state': 1.3.0(@types/react@19.3.0)(react@19.3.1) + '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.3.0)(react@19.3.1) + '@radix-ui/react-visually-hidden': 1.2.13(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + aria-hidden: 1.2.6 + react: 19.3.1 + react-dom: 19.3.1(react@19.3.1) + react-remove-scroll: 2.7.2(@types/react@19.3.0)(react@19.3.1) + optionalDependencies: + '@types/react': 19.3.0 + '@types/react-dom': 19.3.0(@types/react@19.3.0) + + '@radix-ui/react-separator@1.2.0(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1)': + dependencies: + '@radix-ui/react-primitive': 2.2.0(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + react: 19.3.1 + react-dom: 19.3.1(react@19.3.1) + optionalDependencies: + '@types/react': 19.3.0 + '@types/react-dom': 19.3.0(@types/react@19.3.0) + + '@radix-ui/react-slider@1.6.1(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1)': + dependencies: + '@radix-ui/number': 1.1.3 + '@radix-ui/primitive': 1.2.0 + '@radix-ui/react-collection': 1.2.0(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.3.0)(react@19.3.1) + '@radix-ui/react-context': 1.3.0(@types/react@19.3.0)(react@19.3.1) + '@radix-ui/react-direction': 1.2.0(@types/react@19.3.0)(react@19.3.1) + '@radix-ui/react-primitive': 2.2.0(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + '@radix-ui/react-use-controllable-state': 1.3.0(@types/react@19.3.0)(react@19.3.1) + '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.3.0)(react@19.3.1) + '@radix-ui/react-use-size': 1.1.5(@types/react@19.3.0)(react@19.3.1) + react: 19.3.1 + react-dom: 19.3.1(react@19.3.1) + optionalDependencies: + '@types/react': 19.3.0 + '@types/react-dom': 19.3.0(@types/react@19.3.0) + + '@radix-ui/react-slot@1.5.0(@types/react@19.3.0)(react@19.3.1)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.3.0)(react@19.3.1) + react: 19.3.1 + optionalDependencies: + '@types/react': 19.3.0 + + '@radix-ui/react-switch@1.4.1(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1)': + dependencies: + '@radix-ui/primitive': 1.2.0 + '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.3.0)(react@19.3.1) + '@radix-ui/react-context': 1.3.0(@types/react@19.3.0)(react@19.3.1) + '@radix-ui/react-primitive': 2.2.0(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + '@radix-ui/react-use-controllable-state': 1.3.0(@types/react@19.3.0)(react@19.3.1) + '@radix-ui/react-use-size': 1.1.5(@types/react@19.3.0)(react@19.3.1) + react: 19.3.1 + react-dom: 19.3.1(react@19.3.1) + optionalDependencies: + '@types/react': 19.3.0 + '@types/react-dom': 19.3.0(@types/react@19.3.0) + + '@radix-ui/react-tabs@1.2.1(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1)': + dependencies: + '@radix-ui/primitive': 1.2.0 + '@radix-ui/react-context': 1.3.0(@types/react@19.3.0)(react@19.3.1) + '@radix-ui/react-direction': 1.2.0(@types/react@19.3.0)(react@19.3.1) + '@radix-ui/react-id': 1.1.4(@types/react@19.3.0)(react@19.3.1) + '@radix-ui/react-presence': 1.1.11(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + '@radix-ui/react-primitive': 2.2.0(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + '@radix-ui/react-roving-focus': 1.2.1(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + '@radix-ui/react-use-controllable-state': 1.3.0(@types/react@19.3.0)(react@19.3.1) + react: 19.3.1 + react-dom: 19.3.1(react@19.3.1) + optionalDependencies: + '@types/react': 19.3.0 + '@types/react-dom': 19.3.0(@types/react@19.3.0) + + '@radix-ui/react-toast@1.3.1(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1)': + dependencies: + '@radix-ui/primitive': 1.2.0 + '@radix-ui/react-collection': 1.2.0(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.3.0)(react@19.3.1) + '@radix-ui/react-context': 1.3.0(@types/react@19.3.0)(react@19.3.1) + '@radix-ui/react-dismissable-layer': 1.2.1(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + '@radix-ui/react-portal': 1.2.0(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + '@radix-ui/react-presence': 1.1.11(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + '@radix-ui/react-primitive': 2.2.0(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + '@radix-ui/react-use-callback-ref': 1.1.5(@types/react@19.3.0)(react@19.3.1) + '@radix-ui/react-use-controllable-state': 1.3.0(@types/react@19.3.0)(react@19.3.1) + '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.3.0)(react@19.3.1) + '@radix-ui/react-visually-hidden': 1.2.13(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + react: 19.3.1 + react-dom: 19.3.1(react@19.3.1) + optionalDependencies: + '@types/react': 19.3.0 + '@types/react-dom': 19.3.0(@types/react@19.3.0) + + '@radix-ui/react-toggle-group@1.2.1(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1)': + dependencies: + '@radix-ui/primitive': 1.2.0 + '@radix-ui/react-context': 1.3.0(@types/react@19.3.0)(react@19.3.1) + '@radix-ui/react-direction': 1.2.0(@types/react@19.3.0)(react@19.3.1) + '@radix-ui/react-primitive': 2.2.0(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + '@radix-ui/react-roving-focus': 1.2.1(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + '@radix-ui/react-toggle': 1.2.1(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + '@radix-ui/react-use-controllable-state': 1.3.0(@types/react@19.3.0)(react@19.3.1) + react: 19.3.1 + react-dom: 19.3.1(react@19.3.1) + optionalDependencies: + '@types/react': 19.3.0 + '@types/react-dom': 19.3.0(@types/react@19.3.0) + + '@radix-ui/react-toggle@1.2.1(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1)': + dependencies: + '@radix-ui/primitive': 1.2.0 + '@radix-ui/react-primitive': 2.2.0(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + '@radix-ui/react-use-controllable-state': 1.3.0(@types/react@19.3.0)(react@19.3.1) + react: 19.3.1 + react-dom: 19.3.1(react@19.3.1) + optionalDependencies: + '@types/react': 19.3.0 + '@types/react-dom': 19.3.0(@types/react@19.3.0) + + '@radix-ui/react-toolbar@1.2.1(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1)': + dependencies: + '@radix-ui/primitive': 1.2.0 + '@radix-ui/react-context': 1.3.0(@types/react@19.3.0)(react@19.3.1) + '@radix-ui/react-direction': 1.2.0(@types/react@19.3.0)(react@19.3.1) + '@radix-ui/react-primitive': 2.2.0(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + '@radix-ui/react-roving-focus': 1.2.1(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + '@radix-ui/react-separator': 1.2.0(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + '@radix-ui/react-toggle-group': 1.2.1(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + react: 19.3.1 + react-dom: 19.3.1(react@19.3.1) + optionalDependencies: + '@types/react': 19.3.0 + '@types/react-dom': 19.3.0(@types/react@19.3.0) + + '@radix-ui/react-tooltip@1.4.1(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1)': + dependencies: + '@radix-ui/primitive': 1.2.0 + '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.3.0)(react@19.3.1) + '@radix-ui/react-context': 1.3.0(@types/react@19.3.0)(react@19.3.1) + '@radix-ui/react-dismissable-layer': 1.2.1(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + '@radix-ui/react-focus-scope': 1.3.1(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + '@radix-ui/react-id': 1.1.4(@types/react@19.3.0)(react@19.3.1) + '@radix-ui/react-popper': 1.4.0(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + '@radix-ui/react-portal': 1.2.0(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + '@radix-ui/react-presence': 1.1.11(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + '@radix-ui/react-primitive': 2.2.0(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + '@radix-ui/react-slot': 1.5.0(@types/react@19.3.0)(react@19.3.1) + '@radix-ui/react-use-controllable-state': 1.3.0(@types/react@19.3.0)(react@19.3.1) + '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.3.0)(react@19.3.1) + '@radix-ui/react-visually-hidden': 1.2.13(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + react: 19.3.1 + react-dom: 19.3.1(react@19.3.1) + optionalDependencies: + '@types/react': 19.3.0 + '@types/react-dom': 19.3.0(@types/react@19.3.0) + + '@radix-ui/react-use-callback-ref@1.1.5(@types/react@19.3.0)(react@19.3.1)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.3.0)(react@19.3.1) + react: 19.3.1 + optionalDependencies: + '@types/react': 19.3.0 + + '@radix-ui/react-use-controllable-state@1.3.0(@types/react@19.3.0)(react@19.3.1)': + dependencies: + '@radix-ui/primitive': 1.2.0 + '@radix-ui/react-use-effect-event': 0.0.5(@types/react@19.3.0)(react@19.3.1) + '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.3.0)(react@19.3.1) + react: 19.3.1 + optionalDependencies: + '@types/react': 19.3.0 + + '@radix-ui/react-use-effect-event@0.0.5(@types/react@19.3.0)(react@19.3.1)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.3.0)(react@19.3.1) + react: 19.3.1 + optionalDependencies: + '@types/react': 19.3.0 + + '@radix-ui/react-use-escape-keydown@1.1.6(@types/react@19.3.0)(react@19.3.1)': + dependencies: + '@radix-ui/react-use-callback-ref': 1.1.5(@types/react@19.3.0)(react@19.3.1) + react: 19.3.1 + optionalDependencies: + '@types/react': 19.3.0 + + '@radix-ui/react-use-is-hydrated@0.1.3(@types/react@19.3.0)(react@19.3.1)': + dependencies: + react: 19.3.1 + optionalDependencies: + '@types/react': 19.3.0 + + '@radix-ui/react-use-layout-effect@1.1.4(@types/react@19.3.0)(react@19.3.1)': + dependencies: + react: 19.3.1 + optionalDependencies: + '@types/react': 19.3.0 + + '@radix-ui/react-use-previous@1.1.4(@types/react@19.3.0)(react@19.3.1)': + dependencies: + react: 19.3.1 + optionalDependencies: + '@types/react': 19.3.0 + + '@radix-ui/react-use-rect@1.1.4(@types/react@19.3.0)(react@19.3.1)': + dependencies: + '@radix-ui/rect': 1.1.3 + react: 19.3.1 + optionalDependencies: + '@types/react': 19.3.0 + + '@radix-ui/react-use-size@1.1.5(@types/react@19.3.0)(react@19.3.1)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.3.0)(react@19.3.1) + react: 19.3.1 + optionalDependencies: + '@types/react': 19.3.0 + + '@radix-ui/react-visually-hidden@1.2.13(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1)': + dependencies: + '@radix-ui/react-primitive': 2.2.0(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + react: 19.3.1 + react-dom: 19.3.1(react@19.3.1) + optionalDependencies: + '@types/react': 19.3.0 + '@types/react-dom': 19.3.0(@types/react@19.3.0) + + '@radix-ui/rect@1.1.3': {} + + '@react-types/shared@3.37.0(react@19.3.1)': + dependencies: + react: 19.3.1 + + '@rolldown/binding-android-arm-eabi@1.2.16': + optional: true + + '@rolldown/binding-android-arm64@1.2.16': + optional: true + + '@rolldown/binding-darwin-arm64@1.2.16': + optional: true + + '@rolldown/binding-darwin-x64@1.2.16': + optional: true + + '@rolldown/binding-freebsd-x64@1.2.16': + optional: true + + '@rolldown/binding-linux-arm-gnueabihf@1.2.16': + optional: true + + '@rolldown/binding-linux-arm64-gnu@1.2.16': + optional: true + + '@rolldown/binding-linux-arm64-musl@1.2.16': + optional: true + + '@rolldown/binding-linux-ppc64-gnu@1.2.16': + optional: true + + '@rolldown/binding-linux-s390x-gnu@1.2.16': + optional: true + + '@rolldown/binding-linux-x64-gnu@1.2.16': + optional: true + + '@rolldown/binding-linux-x64-musl@1.2.16': + optional: true + + '@rolldown/binding-openharmony-arm64@1.2.16': + optional: true + + '@rolldown/binding-win32-arm64-msvc@1.2.16': + optional: true + + '@rolldown/binding-win32-x64-msvc@1.2.16': + optional: true + + '@rolldown/pluginutils@1.0.1': {} + + '@sindresorhus/slugify@3.0.1': + dependencies: + '@sindresorhus/transliterate': 2.3.1 + escape-string-regexp: 5.0.0 + + '@sindresorhus/transliterate@2.3.1': {} + + '@swc/helpers@0.5.24': {} + + '@tanstack/query-core@5.105.3': {} + + '@tanstack/react-query@5.105.3(@types/react@19.3.0)(react@19.3.1)': + dependencies: + '@tanstack/query-core': 5.105.3 + react: 19.3.1 + optionalDependencies: + '@types/react': 19.3.0 + + '@tanstack/react-store@0.11.2(react-dom@19.3.1(react@19.3.1))(react@19.3.1)': + dependencies: + '@tanstack/store': 0.11.2 + react: 19.3.1 + react-dom: 19.3.1(react@19.3.1) + use-sync-external-store: 1.7.0(react@19.3.1) + + '@tanstack/react-table@9.3.1(react-dom@19.3.1(react@19.3.1))(react@19.3.1)': + dependencies: + '@tanstack/react-store': 0.11.2(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + '@tanstack/table-core': 9.3.1 + react: 19.3.1 + transitivePeerDependencies: + - react-dom + + '@tanstack/react-virtual@3.14.26(react-dom@19.3.1(react@19.3.1))(react@19.3.1)': + dependencies: + '@tanstack/virtual-core': 3.19.11 + react: 19.3.1 + react-dom: 19.3.1(react@19.3.1) + + '@tanstack/store@0.11.2': {} + + '@tanstack/table-core@9.3.1': + dependencies: + '@tanstack/store': 0.11.2 + + '@tanstack/virtual-core@3.19.11': {} + + '@testing-library/dom@10.4.2': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/runtime': 7.29.10 + '@types/aria-query': 5.0.4 + aria-query: 5.3.0 + dom-accessibility-api: 0.5.16 + lz-string: 1.5.0 + picocolors: 1.1.1 + pretty-format: 27.5.1 + + '@testing-library/jest-dom@7.0.1(@testing-library/dom@10.4.2)(vitest@5.1.0)': + dependencies: + '@adobe/css-tools': 4.5.0 + '@testing-library/dom': 10.4.2 + aria-query: 5.3.2 + css.escape: 1.5.1 + dom-accessibility-api: 0.6.3 + picocolors: 1.1.1 + redent: 3.0.0 + optionalDependencies: + vitest: 5.1.0(@types/node@26.6.6)(@vitest/coverage-v8@5.1.0)(jsdom@30.1.3)(vite@8.3.5(@types/node@26.6.6)) + + '@testing-library/react@16.3.3(@testing-library/dom@10.4.2)(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1)': + dependencies: + '@babel/runtime': 7.29.10 + '@testing-library/dom': 10.4.2 + react: 19.3.1 + react-dom: 19.3.1(react@19.3.1) + optionalDependencies: + '@types/react': 19.3.0 + '@types/react-dom': 19.3.0(@types/react@19.3.0) + + '@testing-library/user-event@14.6.8(@testing-library/dom@10.4.2)': + dependencies: + '@testing-library/dom': 10.4.2 + + '@types/aria-query@5.0.4': {} + + '@types/chai@5.2.3': + dependencies: + '@types/deep-eql': 4.0.2 + assertion-error: 2.0.1 + + '@types/deep-eql@4.0.2': {} + + '@types/esrecurse@4.3.1': {} + + '@types/estree@1.0.10': {} + + '@types/json-schema@7.0.15': {} + + '@types/node@26.6.6': + dependencies: + undici-types: 8.9.0 + + '@types/react-dom@19.3.0(@types/react@19.3.0)': + dependencies: + '@types/react': 19.3.0 + + '@types/react@19.3.0': + dependencies: + csstype: 3.2.3 + + '@typescript-eslint/eslint-plugin@8.72.1(@typescript-eslint/parser@8.72.1(eslint@10.14.0)(typescript@6.0.3))(eslint@10.14.0)(typescript@6.0.3)': + dependencies: + '@eslint-community/regexpp': 4.12.2 + '@typescript-eslint/parser': 8.72.1(eslint@10.14.0)(typescript@6.0.3) + '@typescript-eslint/scope-manager': 8.72.1 + '@typescript-eslint/type-utils': 8.72.1(eslint@10.14.0)(typescript@6.0.3) + '@typescript-eslint/utils': 8.72.1(eslint@10.14.0)(typescript@6.0.3) + '@typescript-eslint/visitor-keys': 8.72.1 + eslint: 10.14.0 + ignore: 7.0.14 + natural-compare: 1.4.0 + ts-api-utils: 2.5.0(typescript@6.0.3) + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/parser@8.72.1(eslint@10.14.0)(typescript@6.0.3)': + dependencies: + '@typescript-eslint/scope-manager': 8.72.1 + '@typescript-eslint/types': 8.72.1 + '@typescript-eslint/typescript-estree': 8.72.1(typescript@6.0.3) + '@typescript-eslint/visitor-keys': 8.72.1 + debug: 4.4.3 + eslint: 10.14.0 + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/project-service@8.72.1(typescript@6.0.3)': + dependencies: + '@typescript-eslint/tsconfig-utils': 8.72.1(typescript@6.0.3) + '@typescript-eslint/types': 8.72.1 + debug: 4.4.3 + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/scope-manager@8.72.1': + dependencies: + '@typescript-eslint/types': 8.72.1 + '@typescript-eslint/visitor-keys': 8.72.1 + + '@typescript-eslint/tsconfig-utils@8.72.1(typescript@6.0.3)': + dependencies: + typescript: 6.0.3 + + '@typescript-eslint/type-utils@8.72.1(eslint@10.14.0)(typescript@6.0.3)': + dependencies: + '@typescript-eslint/types': 8.72.1 + '@typescript-eslint/typescript-estree': 8.72.1(typescript@6.0.3) + '@typescript-eslint/utils': 8.72.1(eslint@10.14.0)(typescript@6.0.3) + debug: 4.4.3 + eslint: 10.14.0 + ts-api-utils: 2.5.0(typescript@6.0.3) + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/types@8.72.1': {} + + '@typescript-eslint/typescript-estree@8.72.1(typescript@6.0.3)': + dependencies: + '@typescript-eslint/project-service': 8.72.1(typescript@6.0.3) + '@typescript-eslint/tsconfig-utils': 8.72.1(typescript@6.0.3) + '@typescript-eslint/types': 8.72.1 + '@typescript-eslint/visitor-keys': 8.72.1 + debug: 4.4.3 + minimatch: 10.2.6 + semver: 7.8.6 + tinyglobby: 0.2.17 + ts-api-utils: 2.5.0(typescript@6.0.3) + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/utils@8.72.1(eslint@10.14.0)(typescript@6.0.3)': + dependencies: + '@eslint-community/eslint-utils': 4.10.1(eslint@10.14.0) + '@typescript-eslint/scope-manager': 8.72.1 + '@typescript-eslint/types': 8.72.1 + '@typescript-eslint/typescript-estree': 8.72.1(typescript@6.0.3) + eslint: 10.14.0 + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/visitor-keys@8.72.1': + dependencies: + '@typescript-eslint/types': 8.72.1 + eslint-visitor-keys: 5.0.1 + + '@vitest/coverage-v8@5.1.0(vitest@5.1.0)': + dependencies: + '@bcoe/v8-coverage': 1.0.2 + '@vitest/istanbul-lib-coverage': 2.1.0 + '@vitest/istanbul-lib-report': 2.1.0 + ast-v8-to-istanbul: 1.0.7 + magicast: 0.5.5 + obug: 3.0.0 + std-env: 4.3.0 + tinyrainbow: 3.2.0 + vitest: 5.1.0(@types/node@26.6.6)(@vitest/coverage-v8@5.1.0)(jsdom@30.1.3)(vite@8.3.5(@types/node@26.6.6)) + + '@vitest/istanbul-lib-coverage@2.1.0': {} + + '@vitest/istanbul-lib-report@2.1.0': + dependencies: + '@vitest/istanbul-lib-coverage': 2.1.0 + + '@vitest/mocker@5.1.0(vite@8.3.5(@types/node@26.6.6))': + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + '@vitest/spy': 5.1.0 + estree-walker: 3.0.3 + magic-string: 1.4.5 + optionalDependencies: + vite: 8.3.5(@types/node@26.6.6) + + '@vitest/spy@5.1.0': {} + + acorn-jsx@5.3.2(acorn@8.19.0): + dependencies: + acorn: 8.19.0 + + acorn@8.19.0: {} + + ajv@6.15.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + + ansi-regex@5.0.1: {} + + ansi-styles@5.2.0: {} + + aria-hidden@1.2.6: + dependencies: + tslib: 2.9.1 + + aria-query@5.3.0: + dependencies: + dequal: 2.0.3 + + aria-query@5.3.2: {} + + assertion-error@2.0.1: {} + + ast-v8-to-istanbul@1.0.7: + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + estree-walker: 3.0.3 + js-tokens: 10.0.0 + + balanced-match@4.0.4: {} + + baseline-browser-mapping@2.11.35: {} + + bidi-js@1.1.0: + dependencies: + require-from-string: 2.0.2 + + brace-expansion@5.0.12: + dependencies: + balanced-match: 4.0.4 + + browserslist@4.29.5: + dependencies: + baseline-browser-mapping: 2.11.35 + caniuse-lite: 1.0.30001820 + electron-to-chromium: 1.5.475 + node-releases: 2.0.63 + update-browserslist-db: 1.3.4(browserslist@4.29.5) + + cacheable@2.5.0: + dependencies: + '@cacheable/memory': 2.2.0 + '@cacheable/utils': 2.5.0 + hookified: 1.15.1 + keyv: 5.6.0 + qified: 0.10.1 + + caniuse-lite@1.0.30001820: {} + + chai@6.3.0: {} + + class-variance-authority@0.7.1: + dependencies: + clsx: 2.1.1 + + classnames@2.5.1: {} + + client-only@0.0.1: {} + + clsx@2.1.1: {} + + cmdk@1.1.1(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1): + dependencies: + '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.3.0)(react@19.3.1) + '@radix-ui/react-dialog': 1.3.1(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + '@radix-ui/react-id': 1.1.4(@types/react@19.3.0)(react@19.3.1) + '@radix-ui/react-primitive': 2.2.0(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + react: 19.3.1 + react-dom: 19.3.1(react@19.3.1) + transitivePeerDependencies: + - '@types/react' + - '@types/react-dom' + + convert-source-map@2.0.0: {} + + country-flag-icons@1.6.20: {} + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + css-tree@3.2.1: + dependencies: + mdn-data: 2.27.1 + source-map-js: 1.2.2 + + css.escape@1.5.1: {} + + csstype@3.2.3: {} + + data-urls@8.0.0: + dependencies: + whatwg-mimetype: 5.0.0 + whatwg-url: 17.3.0 + transitivePeerDependencies: + - '@noble/hashes' + + date-fns@4.4.0: {} + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + decimal.js@10.6.0: {} + + deep-is@0.1.4: {} + + dequal@2.0.3: {} + + detect-libc@2.1.2: {} + + detect-node-es@1.1.0: {} + + dom-accessibility-api@0.5.16: {} + + dom-accessibility-api@0.6.3: {} + + electron-to-chromium@1.5.475: {} + + embla-carousel-react@8.6.0(react@19.3.1): + dependencies: + embla-carousel: 8.6.0 + embla-carousel-reactive-utils: 8.6.0(embla-carousel@8.6.0) + react: 19.3.1 + + embla-carousel-reactive-utils@8.6.0(embla-carousel@8.6.0): + dependencies: + embla-carousel: 8.6.0 + + embla-carousel@8.6.0: {} + + entities@9.0.0: {} + + es-module-lexer@2.3.2: {} + + escalade@3.2.0: {} + + escape-string-regexp@4.0.0: {} + + escape-string-regexp@5.0.0: {} + + eslint-plugin-react-hooks@7.1.1(eslint@10.14.0): + dependencies: + '@babel/core': 7.29.7 + '@babel/parser': 7.29.12 + eslint: 10.14.0 + hermes-parser: 0.25.1 + zod: 4.6.7 + zod-validation-error: 4.0.2(zod@4.6.7) + transitivePeerDependencies: + - supports-color + + eslint-scope@9.1.2: + dependencies: + '@types/esrecurse': 4.3.1 + '@types/estree': 1.0.10 + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-visitor-keys@3.4.3: {} + + eslint-visitor-keys@5.0.1: {} + + eslint@10.14.0: + dependencies: + '@eslint-community/eslint-utils': 4.10.1(eslint@10.14.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.8 + '@humanwhocodes/module-importer': 1.0.1 + '@humanwhocodes/retry': 0.4.3 + '@types/estree': 1.0.10 + ajv: 6.15.0 + cross-spawn: 7.0.6 + debug: 4.4.3 + 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.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 11.1.5 + find-up: 5.0.0 + glob-parent: 6.0.2 + ignore: 5.3.2 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + json-stable-stringify-without-jsonify: 1.0.1 + minimatch: 10.2.6 + natural-compare: 1.4.0 + optionator: 0.9.4 + transitivePeerDependencies: + - supports-color + + espree@11.2.0: + dependencies: + acorn: 8.19.0 + acorn-jsx: 5.3.2(acorn@8.19.0) + eslint-visitor-keys: 5.0.1 + + esquery@1.7.0: + dependencies: + estraverse: 5.3.0 + + esrecurse@4.3.0: + dependencies: + estraverse: 5.3.0 + + estraverse@5.3.0: {} + + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.10 + + esutils@2.0.3: {} + + expect-type@1.4.0: {} + + fast-deep-equal@3.1.3: {} + + fast-json-stable-stringify@2.1.0: {} + + fast-levenshtein@2.0.6: {} + + fdir@6.5.0(picomatch@4.0.7): + optionalDependencies: + picomatch: 4.0.7 + + file-entry-cache@11.1.5: + dependencies: + flat-cache: 6.1.23 + + find-up@5.0.0: + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + + flat-cache@6.1.23: + dependencies: + cacheable: 2.5.0 + flatted: 3.4.4 + hookified: 1.15.1 + + flatted@3.4.4: {} + + fsevents@2.3.3: + optional: true + + gensync@1.0.0-beta.2: {} + + get-nonce@1.0.1: {} + + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + + globals@17.14.0: {} + + hashery@1.5.1: + dependencies: + hookified: 1.15.1 + + hermes-estree@0.25.1: {} + + hermes-parser@0.25.1: + dependencies: + hermes-estree: 0.25.1 + + hookified@1.15.1: {} + + hookified@2.2.0: {} + + html-encoding-sniffer@7.0.0: + dependencies: + '@exodus/bytes': 1.16.0 + transitivePeerDependencies: + - '@noble/hashes' + + ignore@5.3.2: {} + + ignore@7.0.14: {} + + imurmurhash@0.1.4: {} + + indent-string@4.0.0: {} + + input-format@0.3.14(react-dom@19.3.1(react@19.3.1))(react@19.3.1): + dependencies: + prop-types: 15.8.1 + optionalDependencies: + react: 19.3.1 + react-dom: 19.3.1(react@19.3.1) + + input-otp@1.5.0(react-dom@19.3.1(react@19.3.1))(react@19.3.1): + dependencies: + react: 19.3.1 + react-dom: 19.3.1(react@19.3.1) + + is-extglob@2.1.1: {} + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-potential-custom-element-name@1.0.1: {} + + isexe@2.0.0: {} + + js-tokens@10.0.0: {} + + js-tokens@4.0.0: {} + + jsdom@30.1.3: + dependencies: + '@asamuzakjp/css-color': 7.1.9 + '@asamuzakjp/dom-selector': 10.0.5 + '@bramus/specificity': 2.4.3 + '@csstools/css-syntax-patches-for-csstree': 1.1.16(css-tree@3.2.1) + '@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.1.0 + saxes: 6.0.0 + tough-cookie: 6.0.2 + undici: 8.12.3 + w3c-xmlserializer: 6.0.0 + webidl-conversions: 8.0.1 + whatwg-mimetype: 5.0.0 + whatwg-url: 17.3.0 + xml-name-validator: 5.0.0 + transitivePeerDependencies: + - '@noble/hashes' + + jsesc@3.1.0: {} + + json-schema-traverse@0.4.1: {} + + json-stable-stringify-without-jsonify@1.0.1: {} + + json5@2.2.3: {} + + keyv@5.6.0: + dependencies: + '@keyv/serialize': 1.1.1 + + levn@0.4.1: + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + + libphonenumber-js@1.13.15: {} + + lightningcss-android-arm64@1.33.0: + optional: true + + lightningcss-darwin-arm64@1.33.0: + optional: true + + lightningcss-darwin-x64@1.33.0: + optional: true + + lightningcss-freebsd-x64@1.33.0: + optional: true + + lightningcss-linux-arm-gnueabihf@1.33.0: + optional: true + + lightningcss-linux-arm64-gnu@1.33.0: + optional: true + + lightningcss-linux-arm64-musl@1.33.0: + optional: true + + lightningcss-linux-x64-gnu@1.33.0: + optional: true + + lightningcss-linux-x64-musl@1.33.0: + optional: true + + lightningcss-win32-arm64-msvc@1.33.0: + optional: true + + lightningcss-win32-x64-msvc@1.33.0: + optional: true + + lightningcss@1.33.0: + dependencies: + detect-libc: 2.1.2 + 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 + + locate-path@6.0.0: + dependencies: + p-locate: 5.0.0 + + loose-envify@1.4.0: + dependencies: + js-tokens: 4.0.0 + + lru-cache@11.5.3: {} + + lru-cache@5.1.1: + dependencies: + yallist: 3.1.1 + + lucide-react@1.68.0(@types/react@19.3.0)(react@19.3.1): + dependencies: + react: 19.3.1 + optionalDependencies: + '@types/react': 19.3.0 + + lz-string@1.5.0: {} + + magic-string@1.4.5: + dependencies: + '@jridgewell/sourcemap-codec': 1.6.0 + + magicast@0.5.5: + dependencies: + '@babel/parser': 7.29.12 + '@babel/types': 7.29.11 + source-map-js: 1.2.2 + + mdn-data@2.27.1: {} + + min-indent@1.0.1: {} + + minimatch@10.2.6: + dependencies: + brace-expansion: 5.0.12 + + ms@2.1.3: {} + + nanoid@3.3.20: {} + + natural-compare@1.4.0: {} + + node-releases@2.0.63: {} + + object-assign@4.1.1: {} + + obug@3.0.0: {} + + optionator@0.9.4: + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + word-wrap: 1.2.5 + + p-limit@3.1.0: + dependencies: + yocto-queue: 0.1.0 + + p-locate@5.0.0: + dependencies: + p-limit: 3.1.0 + + parse5@8.1.0: + dependencies: + entities: 9.0.0 + + path-exists@4.0.0: {} + + path-key@3.1.1: {} + + picocolors@1.1.1: {} + + picomatch@4.0.7: {} + + postcss@8.5.32: + dependencies: + nanoid: 3.3.20 + picocolors: 1.1.1 + source-map-js: 1.2.2 + + prelude-ls@1.2.1: {} + + prettier@3.9.10: {} + + pretty-format@27.5.1: + dependencies: + ansi-regex: 5.0.1 + ansi-styles: 5.2.0 + react-is: 17.0.2 + + prop-types@15.8.1: + dependencies: + loose-envify: 1.4.0 + object-assign: 4.1.1 + react-is: 16.13.1 + + punycode@2.3.1: {} + + qified@0.10.1: + dependencies: + hookified: 2.2.0 + + radix-ui@1.8.1(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1): + dependencies: + '@radix-ui/primitive': 1.2.0 + '@radix-ui/react-accessible-icon': 1.2.0(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + '@radix-ui/react-accordion': 1.3.1(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + '@radix-ui/react-alert-dialog': 1.2.1(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + '@radix-ui/react-arrow': 1.1.17(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + '@radix-ui/react-aspect-ratio': 1.2.0(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + '@radix-ui/react-avatar': 1.3.1(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + '@radix-ui/react-checkbox': 1.4.1(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + '@radix-ui/react-collapsible': 1.2.1(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + '@radix-ui/react-collection': 1.2.0(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.3.0)(react@19.3.1) + '@radix-ui/react-context': 1.3.0(@types/react@19.3.0)(react@19.3.1) + '@radix-ui/react-context-menu': 2.4.1(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + '@radix-ui/react-dialog': 1.3.1(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + '@radix-ui/react-direction': 1.2.0(@types/react@19.3.0)(react@19.3.1) + '@radix-ui/react-dismissable-layer': 1.2.1(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + '@radix-ui/react-dropdown-menu': 2.2.1(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + '@radix-ui/react-focus-guards': 1.2.0(@types/react@19.3.0)(react@19.3.1) + '@radix-ui/react-focus-scope': 1.3.1(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + '@radix-ui/react-form': 0.3.0(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + '@radix-ui/react-hover-card': 1.2.1(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + '@radix-ui/react-label': 2.1.17(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + '@radix-ui/react-menu': 2.2.1(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + '@radix-ui/react-menubar': 1.2.1(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + '@radix-ui/react-navigation-menu': 1.4.1(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + '@radix-ui/react-one-time-password-field': 0.2.1(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + '@radix-ui/react-password-toggle-field': 0.2.1(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + '@radix-ui/react-popover': 1.3.1(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + '@radix-ui/react-popper': 1.4.0(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + '@radix-ui/react-portal': 1.2.0(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + '@radix-ui/react-presence': 1.1.11(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + '@radix-ui/react-primitive': 2.2.0(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + '@radix-ui/react-progress': 1.2.1(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + '@radix-ui/react-radio-group': 1.5.1(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + '@radix-ui/react-roving-focus': 1.2.1(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + '@radix-ui/react-scroll-area': 1.4.0(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + '@radix-ui/react-select': 2.4.1(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + '@radix-ui/react-separator': 1.2.0(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + '@radix-ui/react-slider': 1.6.1(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + '@radix-ui/react-slot': 1.5.0(@types/react@19.3.0)(react@19.3.1) + '@radix-ui/react-switch': 1.4.1(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + '@radix-ui/react-tabs': 1.2.1(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + '@radix-ui/react-toast': 1.3.1(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + '@radix-ui/react-toggle': 1.2.1(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + '@radix-ui/react-toggle-group': 1.2.1(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + '@radix-ui/react-toolbar': 1.2.1(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + '@radix-ui/react-tooltip': 1.4.1(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + '@radix-ui/react-use-callback-ref': 1.1.5(@types/react@19.3.0)(react@19.3.1) + '@radix-ui/react-use-controllable-state': 1.3.0(@types/react@19.3.0)(react@19.3.1) + '@radix-ui/react-use-effect-event': 0.0.5(@types/react@19.3.0)(react@19.3.1) + '@radix-ui/react-use-escape-keydown': 1.1.6(@types/react@19.3.0)(react@19.3.1) + '@radix-ui/react-use-is-hydrated': 0.1.3(@types/react@19.3.0)(react@19.3.1) + '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.3.0)(react@19.3.1) + '@radix-ui/react-use-size': 1.1.5(@types/react@19.3.0)(react@19.3.1) + '@radix-ui/react-visually-hidden': 1.2.13(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + react: 19.3.1 + react-dom: 19.3.1(react@19.3.1) + optionalDependencies: + '@types/react': 19.3.0 + '@types/react-dom': 19.3.0(@types/react@19.3.0) + + react-aria-components@1.22.1(react-dom@19.3.1(react@19.3.1))(react@19.3.1): + dependencies: + '@internationalized/date': 3.12.4 + '@internationalized/string': 3.2.10 + '@react-types/shared': 3.37.0(react@19.3.1) + '@swc/helpers': 0.5.24 + client-only: 0.0.1 + react: 19.3.1 + react-aria: 3.53.1(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + react-dom: 19.3.1(react@19.3.1) + react-stately: 3.51.0(react@19.3.1) + use-sync-external-store: 1.7.0(react@19.3.1) + + react-aria@3.53.1(react-dom@19.3.1(react@19.3.1))(react@19.3.1): + dependencies: + '@internationalized/date': 3.12.4 + '@internationalized/number': 3.6.9 + '@internationalized/string': 3.2.10 + '@react-types/shared': 3.37.0(react@19.3.1) + '@swc/helpers': 0.5.24 + aria-hidden: 1.2.6 + clsx: 2.1.1 + react: 19.3.1 + react-dom: 19.3.1(react@19.3.1) + react-stately: 3.51.0(react@19.3.1) + use-sync-external-store: 1.7.0(react@19.3.1) + + react-day-picker@10.0.2(@types/react@19.3.0)(react@19.3.1): + dependencies: + '@date-fns/tz': 1.5.0 + date-fns: 4.4.0 + react: 19.3.1 + optionalDependencies: + '@types/react': 19.3.0 + + react-dom@19.3.1(react@19.3.1): + dependencies: + react: 19.3.1 + scheduler: 0.28.0 + + react-hook-form@7.91.0(@types/react@19.3.0)(react@19.3.1): + dependencies: + react: 19.3.1 + optionalDependencies: + '@types/react': 19.3.0 + + react-is@16.13.1: {} + + react-is@17.0.2: {} + + react-phone-number-input@3.4.18(react-dom@19.3.1(react@19.3.1))(react@19.3.1): + dependencies: + classnames: 2.5.1 + country-flag-icons: 1.6.20 + input-format: 0.3.14(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + libphonenumber-js: 1.13.15 + prop-types: 15.8.1 + react: 19.3.1 + react-dom: 19.3.1(react@19.3.1) + + react-remove-scroll-bar@2.3.8(@types/react@19.3.0)(react@19.3.1): + dependencies: + react: 19.3.1 + react-style-singleton: 2.2.3(@types/react@19.3.0)(react@19.3.1) + tslib: 2.9.1 + optionalDependencies: + '@types/react': 19.3.0 + + react-remove-scroll@2.7.2(@types/react@19.3.0)(react@19.3.1): + dependencies: + react: 19.3.1 + react-remove-scroll-bar: 2.3.8(@types/react@19.3.0)(react@19.3.1) + react-style-singleton: 2.2.3(@types/react@19.3.0)(react@19.3.1) + tslib: 2.9.1 + use-callback-ref: 1.3.3(@types/react@19.3.0)(react@19.3.1) + use-sidecar: 1.1.3(@types/react@19.3.0)(react@19.3.1) + optionalDependencies: + '@types/react': 19.3.0 + + react-stately@3.51.0(react@19.3.1): + dependencies: + '@internationalized/date': 3.12.4 + '@internationalized/number': 3.6.9 + '@internationalized/string': 3.2.10 + '@react-types/shared': 3.37.0(react@19.3.1) + '@swc/helpers': 0.5.24 + react: 19.3.1 + use-sync-external-store: 1.7.0(react@19.3.1) + + react-style-singleton@2.2.3(@types/react@19.3.0)(react@19.3.1): + dependencies: + get-nonce: 1.0.1 + react: 19.3.1 + tslib: 2.9.1 + optionalDependencies: + '@types/react': 19.3.0 + + react@19.3.1: {} + + redent@3.0.0: + dependencies: + indent-string: 4.0.0 + strip-indent: 3.0.0 + + require-from-string@2.0.2: {} + + rolldown@1.2.16: + dependencies: + '@oxc-project/types': 0.156.0 + '@rolldown/pluginutils': 1.0.1 + optionalDependencies: + '@rolldown/binding-android-arm-eabi': 1.2.16 + '@rolldown/binding-android-arm64': 1.2.16 + '@rolldown/binding-darwin-arm64': 1.2.16 + '@rolldown/binding-darwin-x64': 1.2.16 + '@rolldown/binding-freebsd-x64': 1.2.16 + '@rolldown/binding-linux-arm-gnueabihf': 1.2.16 + '@rolldown/binding-linux-arm64-gnu': 1.2.16 + '@rolldown/binding-linux-arm64-musl': 1.2.16 + '@rolldown/binding-linux-ppc64-gnu': 1.2.16 + '@rolldown/binding-linux-s390x-gnu': 1.2.16 + '@rolldown/binding-linux-x64-gnu': 1.2.16 + '@rolldown/binding-linux-x64-musl': 1.2.16 + '@rolldown/binding-openharmony-arm64': 1.2.16 + '@rolldown/binding-win32-arm64-msvc': 1.2.16 + '@rolldown/binding-win32-x64-msvc': 1.2.16 + + saxes@6.0.0: + dependencies: + xmlchars: 2.2.0 + + scheduler@0.28.0: {} + + semver@6.3.2: {} + + semver@7.8.6: {} + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + sonner@2.0.8(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1): + dependencies: + react: 19.3.1 + react-dom: 19.3.1(react@19.3.1) + optionalDependencies: + '@types/react': 19.3.0 + + source-map-js@1.2.2: {} + + std-env@4.3.0: {} + + strip-indent@3.0.0: + dependencies: + min-indent: 1.0.1 + + tailwind-merge@3.7.0: {} + + tailwindcss@4.3.4: {} + + tinybench@6.2.3: {} + + tinyexec@1.3.1: {} + + tinyglobby@0.2.17: + dependencies: + fdir: 6.5.0(picomatch@4.0.7) + picomatch: 4.0.7 + + tinyrainbow@3.2.0: {} + + tldts-core@7.4.20: {} + + tldts@7.4.19: + dependencies: + tldts-core: 7.4.20 + + tough-cookie@6.0.2: + dependencies: + tldts: 7.4.19 + + tr46@7.0.0: + dependencies: + punycode: 2.3.1 + + ts-api-utils@2.5.0(typescript@6.0.3): + dependencies: + typescript: 6.0.3 + + tslib@2.9.1: {} + + tw-animate-css@1.4.0: {} + + type-check@0.4.0: + dependencies: + prelude-ls: 1.2.1 + + typescript-eslint@8.72.1(eslint@10.14.0)(typescript@6.0.3): + dependencies: + '@typescript-eslint/eslint-plugin': 8.72.1(@typescript-eslint/parser@8.72.1(eslint@10.14.0)(typescript@6.0.3))(eslint@10.14.0)(typescript@6.0.3) + '@typescript-eslint/parser': 8.72.1(eslint@10.14.0)(typescript@6.0.3) + '@typescript-eslint/typescript-estree': 8.72.1(typescript@6.0.3) + '@typescript-eslint/utils': 8.72.1(eslint@10.14.0)(typescript@6.0.3) + eslint: 10.14.0 + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + + typescript@6.0.3: {} + + undici-types@8.9.0: {} + + undici@8.12.3: {} + + update-browserslist-db@1.3.4(browserslist@4.29.5): + dependencies: + browserslist: 4.29.5 + escalade: 3.2.0 + picocolors: 1.1.1 + + uqr@0.1.3: {} + + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + + use-callback-ref@1.3.3(@types/react@19.3.0)(react@19.3.1): + dependencies: + react: 19.3.1 + tslib: 2.9.1 + optionalDependencies: + '@types/react': 19.3.0 + + use-sidecar@1.1.3(@types/react@19.3.0)(react@19.3.1): + dependencies: + detect-node-es: 1.1.0 + react: 19.3.1 + tslib: 2.9.1 + optionalDependencies: + '@types/react': 19.3.0 + + use-sync-external-store@1.7.0(react@19.3.1): + dependencies: + react: 19.3.1 + + vaul@1.1.2(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1): + dependencies: + '@radix-ui/react-dialog': 1.3.1(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + react: 19.3.1 + react-dom: 19.3.1(react@19.3.1) + transitivePeerDependencies: + - '@types/react' + - '@types/react-dom' + + vite@8.3.5(@types/node@26.6.6): + dependencies: + lightningcss: 1.33.0 + picomatch: 4.0.7 + postcss: 8.5.32 + rolldown: 1.2.16 + tinyglobby: 0.2.17 + optionalDependencies: + '@types/node': 26.6.6 + fsevents: 2.3.3 + + vitest@5.1.0(@types/node@26.6.6)(@vitest/coverage-v8@5.1.0)(jsdom@30.1.3)(vite@8.3.5(@types/node@26.6.6)): + dependencies: + '@types/chai': 5.2.3 + '@vitest/mocker': 5.1.0(vite@8.3.5(@types/node@26.6.6)) + chai: 6.3.0 + es-module-lexer: 2.3.2 + expect-type: 1.4.0 + magic-string: 1.4.5 + obug: 3.0.0 + picomatch: 4.0.7 + std-env: 4.3.0 + tinybench: 6.2.3 + tinyexec: 1.3.1 + tinyglobby: 0.2.17 + vite: 8.3.5(@types/node@26.6.6) + why-is-node-running: 3.2.1 + optionalDependencies: + '@types/node': 26.6.6 + '@vitest/coverage-v8': 5.1.0(vitest@5.1.0) + jsdom: 30.1.3 + transitivePeerDependencies: + - msw + + w3c-xmlserializer@6.0.0: + dependencies: + xml-name-validator: 5.0.0 + + webidl-conversions@8.0.1: {} + + whatwg-mimetype@5.0.0: {} + + whatwg-url@17.3.0: + dependencies: + '@exodus/bytes': 1.16.0 + tr46: 7.0.0 + webidl-conversions: 8.0.1 + transitivePeerDependencies: + - '@noble/hashes' + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + why-is-node-running@3.2.1: {} + + word-wrap@1.2.5: {} + + xml-name-validator@5.0.0: {} + + xmlchars@2.2.0: {} + + yallist@3.1.1: {} + + yocto-queue@0.1.0: {} + + zod-validation-error@4.0.2(zod@4.6.7): + dependencies: + zod: 4.6.7 + + zod@4.6.7: {} diff --git a/resources/js/components/LanguageSwitcher.tsx b/resources/js/components/LanguageSwitcher.tsx new file mode 100644 index 0000000..1b96fc5 --- /dev/null +++ b/resources/js/components/LanguageSwitcher.tsx @@ -0,0 +1,37 @@ +import { NavTabs } from "@javaabu/forms" + +import { useTranslatable } from "../context" +import type { TranslatableLocale } from "../types" + +export interface LanguageSwitcherProps { + /** The link for a language. Default: the current URL with `?lang=` (read by the `language` middleware). */ + url?: (locale: TranslatableLocale) => string + className?: string +} + +/** The current URL with `?lang=code`. */ +export function switchUrl(locale: string): string { + if (typeof window === "undefined") return `?lang=${encodeURIComponent(locale)}` + const url = new URL(window.location.href) + url.searchParams.set("lang", locale) + return url.toString() +} + +/** ``: a link per active language. Hidden with fewer than two. */ +export function LanguageSwitcher({ url = (locale) => switchUrl(locale.code), className }: LanguageSwitcherProps) { + const { locales, current } = useTranslatable() + + if (locales.length < 2) return null + + return ( +
+ ({ + title: {locale.native_name}, + url: url(locale), + active: locale.code === current, + }))} + /> +
+ ) +} diff --git a/resources/js/components/TranslatableField.tsx b/resources/js/components/TranslatableField.tsx new file mode 100644 index 0000000..73bbc2f --- /dev/null +++ b/resources/js/components/TranslatableField.tsx @@ -0,0 +1,136 @@ +import * as React from "react" +import { + errorsFor, + Input, + selectErrorBag, + Tabs, + Textarea, + useFormsConfig, + useFormState, + type InputProps, + type TextareaProps, +} from "@javaabu/forms" + +import { localeInputs, useTranslatable } from "../context" +import { useTranslatableStrings } from "../strings" +import type { LocaleInput } from "../types" + +type SharedFieldProps = Omit & + Pick + +export interface TranslatableFieldProps extends SharedFieldProps { + /** The field: `title` gives `title_en`, `title_dv`, … */ + name: string + label?: string + /** `text` (default), `textarea`, or any `Input` type. */ + type?: string + /** `Translatable::inputs()` from the server. Default: built from the shared languages. */ + inputs?: LocaleInput[] | null + /** The model's `lang`, when `inputs` isn't given. Default: the current locale. */ + primary?: string + /** Require every locale, not only the primary one. */ + requireAll?: boolean + /** `tabs` (default) or `stacked`. */ + layout?: "tabs" | "stacked" + className?: string +} + +/** The names of the inputs that have errors, from the surrounding `Form` or `FormsProvider errors`. */ +function useInputsWithErrors(inputs: LocaleInput[]): Set { + const form = useFormState() + const { errors: provided, router } = useFormsConfig() + const page = router?.usePageErrors?.() + const errors = form ? form.errors : selectErrorBag(page ?? provided) + + return new Set(inputs.filter((input) => errorsFor(errors, input.name).length > 0).map((input) => input.name)) +} + +/** + * ``: one `@javaabu/forms` field per locale, in tabs or stacked. + * Without translations (no provider, translations off, or no inputs) it renders a plain field. + */ +export function TranslatableField({ + name, + label, + type = "text", + inputs: given, + primary, + requireAll = false, + layout = "tabs", + required, + className, + ...props +}: TranslatableFieldProps) { + const shared = useTranslatable() + const t = useTranslatableStrings() + const inputs = given ?? (shared.enabled ? localeInputs(name, shared.locales, primary ?? shared.current) : []) + const withErrors = useInputsWithErrors(inputs) + + const field = ( + fieldName: string, + fieldLabel: string | undefined, + extra: Record, + isRequired?: boolean, + ) => + type === "textarea" ? ( +